# -*- coding: utf-8 -*-
"""
EvMore - pager mechanism
This is a pager for displaying long texts and allows stepping up and
down in the text (the name comes from the traditional 'more' unix
command).
To use, simply pass the text through the EvMore object:
::
from evennia.utils.evmore import EvMore
text = some_long_text_output()
EvMore(caller, text, always_page=False, session=None, justify_kwargs=None, **kwargs)
One can also use the convenience function msg from this module:
::
from evennia.utils import evmore
text = some_long_text_output()
evmore.msg(caller, text, always_page=False, session=None, justify_kwargs=None, **kwargs)
Where always_page decides if the pager is used also if the text is not long
enough to need to scroll, session is used to determine which session to relay
to and `justify_kwargs` are kwargs to pass to `utils.utils.justify` in order to
change the formatting of the text. The remaining `**kwargs` will be passed on to
the `caller.msg()` construct every time the page is updated.
----
"""
from django.conf import settings
from django.db.models.query import QuerySet
from django.core.paginator import Paginator
from evennia import Command, CmdSet
from evennia.commands import cmdhandler
from evennia.utils.utils import make_iter, inherits_from, justify
_CMD_NOMATCH = cmdhandler.CMD_NOMATCH
_CMD_NOINPUT = cmdhandler.CMD_NOINPUT
# we need to use NAWS for this
_SCREEN_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_SCREEN_HEIGHT = settings.CLIENT_DEFAULT_HEIGHT
_EVTABLE = None
# text
_DISPLAY = """{text}
(|wmore|n [{pageno}/{pagemax}] retur|wn|n|||wb|nack|||wt|nop|||we|nnd|||wq|nuit)"""
[docs]class CmdMore(Command):
"""
Manipulate the text paging
"""
key = _CMD_NOINPUT
aliases = ["quit", "q", "abort", "a", "next", "n", "back", "b", "top", "t", "end", "e"]
auto_help = False
[docs] def func(self):
"""
Implement the command
"""
more = self.caller.ndb._more
if not more and hasattr(self.caller, "account"):
more = self.caller.account.ndb._more
if not more:
self.caller.msg("Error in loading the pager. Contact an admin.")
return
cmd = self.cmdstring
if cmd in ("abort", "a", "q"):
more.page_quit()
elif cmd in ("back", "b"):
more.page_back()
elif cmd in ("top", "t", "look", "l"):
more.page_top()
elif cmd in ("end", "e"):
more.page_end()
else:
# return or n, next
more.page_next()
[docs]class CmdMoreLook(Command):
"""
Override look to display window and prevent OOCLook from firing
"""
key = "look"
aliases = ["l"]
auto_help = False
[docs] def func(self):
"""
Implement the command
"""
more = self.caller.ndb._more
if not more and hasattr(self.caller, "account"):
more = self.caller.account.ndb._more
if not more:
self.caller.msg("Error in loading the pager. Contact an admin.")
return
more.display()
[docs]class CmdSetMore(CmdSet):
"""
Stores the more command
"""
key = "more_commands"
priority = 110
[docs] def at_cmdset_creation(self):
self.add(CmdMore())
self.add(CmdMoreLook())
# resources for handling queryset inputs
[docs]def queryset_maxsize(qs):
return qs.count()
[docs]class EvMore:
"""
The main pager object.
"""
[docs] def __init__(
self,
caller,
inp,
always_page=False,
session=None,
justify=False,
justify_kwargs=None,
exit_on_lastpage=False,
exit_cmd=None,
page_formatter=str,
**kwargs,
):
"""
Initialization of the Evmore input handler.
Args:
caller (Object or Account): Entity reading the text.
inp (str, EvTable, Paginator or iterator): The text or data to put under paging.
- If a string, paginage normally. If this text contains
one or more \\\\f (backslash + f) format symbols, automatic
pagination and justification are force-disabled and
page-breaks will only happen after each \\\\f.
- If `EvTable`, the EvTable will be paginated with the same
setting on each page if it is too long. The table
decorations will be considered in the size of the page.
- Otherwise `inp` is converted to an iterator, where each step is
expected to be a line in the final display. Each line
will be run through `iter_callable`.
always_page (bool, optional): If `False`, the pager will only kick
in if `inp` is too big to fit the screen.
session (Session, optional): If given, this session will be used
to determine the screen width and will receive all output.
justify (bool, optional): If set, auto-justify long lines. This must be turned
off for fixed-width or formatted output, like tables. It's force-disabled
if `inp` is an EvTable.
justify_kwargs (dict, optional): Keywords for the justify function. Used only
if `justify` is True. If this is not set, default arguments will be used.
exit_on_lastpage (bool, optional): If reaching the last page without the
page being completely filled, exit pager immediately. If unset,
another move forward is required to exit. If set, the pager
exit message will not be shown.
exit_cmd (str, optional): If given, this command-string will be executed on
the caller when the more page exits. Note that this will be using whatever
cmdset the user had *before* the evmore pager was activated (so none of
the evmore commands will be available when this is run).
kwargs (any, any): These will be passed on to the `caller.msg` method.
Examples:
Basic use:
::
super_long_text = " ... "
EvMore(caller, super_long_text)
Paginated query data - this is an optimization to avoid fetching
database data until it's actually paged to.
::
from django.core.paginator import Paginator
query = ObjectDB.objects.all()
pages = Paginator(query, 10) # 10 objs per page
EvMore(caller, pages)
Automatic split EvTable over multiple EvMore pages
::
table = EvMore(*header, table=tabledata)
EvMore(caller, table)
Every page a separate EvTable (optimization for very large data sets)
::
from evennia import EvTable, EvMore
class TableEvMore(EvMore):
def init_pages(self, data):
pages = # depends on data type
super().init_pages(pages)
def page_formatter(self, page):
table = EvTable()
for line in page:
cols = # split raw line into columns
table.add_row(*cols)
return str(table)
TableEvMore(caller, pages)
"""
self._caller = caller
self._always_page = always_page
if not session:
# if not supplied, use the first session to
# determine screen size
sessions = caller.sessions.get()
if not sessions:
return
session = sessions[0]
self._session = session
self._justify = justify
self._justify_kwargs = justify_kwargs
self.exit_on_lastpage = exit_on_lastpage
self.exit_cmd = exit_cmd
self._exit_msg = "Exited |wmore|n pager."
self._kwargs = kwargs
self._data = None
self._pages = []
self._npos = 0
self._npages = 1
self._paginator = self.paginator_index
self._page_formatter = str
# set up individual pages for different sessions
height = max(4, session.protocol_flags.get("SCREENHEIGHT", {0: _SCREEN_HEIGHT})[0] - 4)
self.width = session.protocol_flags.get("SCREENWIDTH", {0: _SCREEN_WIDTH})[0]
# always limit number of chars to 10 000 per page
self.height = min(10000 // max(1, self.width), height)
# does initial parsing of input
self.init_pages(inp)
# kick things into gear
self.start()
# EvMore functional methods
[docs] def display(self, show_footer=True):
"""
Pretty-print the page.
"""
pos = 0
text = "[no content]"
if self._npages > 0:
pos = self._npos
text = self.page_formatter(self.paginator(pos))
if show_footer:
page = _DISPLAY.format(text=text, pageno=pos + 1, pagemax=self._npages)
else:
page = text
# check to make sure our session is still valid
sessions = self._caller.sessions.get()
if not sessions:
self.page_quit()
return
# this must be an 'is', not == check
if not any(ses for ses in sessions if self._session is ses):
self._session = sessions[0]
self._caller.msg(text=page, session=self._session, **self._kwargs)
[docs] def page_top(self):
"""
Display the top page
"""
self._npos = 0
self.display()
[docs] def page_end(self):
"""
Display the bottom page.
"""
self._npos = self._npages - 1
self.display()
[docs] def page_next(self):
"""
Scroll the text to the next page. Quit if already at the end
of the page.
"""
if self._npos >= self._npages - 1:
# exit if we are already at the end
self.page_quit()
else:
self._npos += 1
if self.exit_on_lastpage and self._npos >= (self._npages - 1):
self.display(show_footer=False)
self.page_quit(quiet=True)
else:
self.display()
[docs] def page_back(self):
"""
Scroll the text back up, at the most to the top.
"""
self._npos = max(0, self._npos - 1)
self.display()
[docs] def page_quit(self, quiet=False):
"""
Quit the pager
"""
del self._caller.ndb._more
if not quiet:
self._caller.msg(text=self._exit_msg, **self._kwargs)
self._caller.cmdset.remove(CmdSetMore)
if self.exit_cmd:
self._caller.execute_cmd(self.exit_cmd, session=self._session)
[docs] def start(self):
"""
Starts the pagination
"""
if self._npages <= 1 and not self._always_page:
# no need for paging; just pass-through.
self.display(show_footer=False)
else:
# go into paging mode
# first pass on the msg kwargs
self._caller.ndb._more = self
self._caller.cmdset.add(CmdSetMore)
# goto top of the text
self.page_top()
# default paginators - responsible for extracting a specific page number
[docs] def paginator_index(self, pageno):
"""Paginate to specific, known index"""
return self._data[pageno]
[docs] def paginator_slice(self, pageno):
"""
Paginate by slice. This is done with an eye on memory efficiency (usually for
querysets); to avoid fetching all objects at the same time.
"""
return self._data[pageno * self.height : pageno * self.height + self.height]
[docs] def paginator_django(self, pageno):
"""
Paginate using the django queryset Paginator API. Note that his is indexed from 1.
"""
return self._data.page(pageno + 1)
# default helpers to set up particular input types
[docs] def init_evtable(self, table):
"""The input is an EvTable."""
if table.height:
# enforced height of each paged table, plus space for evmore extras
self.height = table.height - 4
# convert table to string
text = str(table)
self._justify = False
self._justify_kwargs = None # enforce
self.init_str(text)
[docs] def init_queryset(self, qs):
"""The input is a queryset"""
nsize = qs.count() # we assume each will be a line
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
self._data = qs
[docs] def init_django_paginator(self, pages):
"""
The input is a django Paginator object.
"""
self._npages = pages.num_pages
self._data = pages
[docs] def init_iterable(self, inp):
"""The input is something other than a string - convert to iterable of strings"""
inp = make_iter(inp)
nsize = len(inp)
self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1)
self._data = inp
[docs] def init_f_str(self, text):
"""
The input contains \\\\f (backslash + f) markers. We use \\\\f to indicate
the user wants to enforce their line breaks on their own. If so, we do
no automatic line-breaking/justification at all.
"""
self._data = text.split("\f")
self._npages = len(self._data)
[docs] def init_str(self, text):
"""The input is a string"""
if self._justify:
# we must break very long lines into multiple ones. Note that this
# will also remove spurious whitespace.
justify_kwargs = self._justify_kwargs or {}
width = self._justify_kwargs.get("width", self.width)
justify_kwargs["width"] = width
justify_kwargs["align"] = self._justify_kwargs.get("align", "l")
justify_kwargs["indent"] = self._justify_kwargs.get("indent", 0)
lines = []
for line in text.split("\n"):
if len(line) > width:
lines.extend(justify(line, **justify_kwargs).split("\n"))
else:
lines.append(line)
else:
# no justification. Simple division by line
lines = text.split("\n")
self._data = [
"\n".join(lines[i : i + self.height]) for i in range(0, len(lines), self.height)
]
self._npages = len(self._data)
# Hooks for customizing input handling and formatting (override in a child class)
[docs] def init_pages(self, inp):
"""
Initialize the pagination. By default, will analyze input type to determine
how pagination automatically.
Args:
inp (any): Incoming data to be paginated. By default, handles pagination of
strings, querysets, django.Paginator, EvTables and any iterables with strings.
Notes:
If overridden, this method must perform the following actions:
- read and re-store `self._data` (the incoming data set) if needed
for pagination to work.
- set `self._npages` to the total number of pages. Default is 1.
- set `self._paginator` to a callable that will take a page number 1...N and return
the data to display on that page (not any decorations or next/prev buttons). If only
wanting to change the paginator, override `self.paginator` instead.
- set `self._page_formatter` to a callable that will receive the
page from `self._paginator` and format it with one element per
line. Default is `str`. Or override `self.page_formatter`
directly instead.
By default, helper methods are called that perform these actions
depending on supported inputs.
"""
if inherits_from(inp, "evennia.utils.evtable.EvTable"):
# an EvTable
self.init_evtable(inp)
self._paginator = self.paginator_index
elif isinstance(inp, QuerySet):
# a queryset
self.init_queryset(inp)
self._paginator = self.paginator_slice
elif isinstance(inp, Paginator):
self.init_django_paginator(inp)
self._paginator = self.paginator_django
elif not isinstance(inp, str):
# anything else not a str
self.init_iterable(inp)
self._paginator = self.paginator_slice
elif "\f" in inp:
# string with \f line-break markers in it
self.init_f_str(inp)
self._paginator = self.paginator_index
else:
# a string
self.init_str(inp)
self._paginator = self.paginator_index
[docs] def paginator(self, pageno):
"""
Paginator. The data operated upon is in `self._data`.
Args:
pageno (int): The page number to view, from 0...N-1
Returns:
str: The page to display (without any decorations, those are added
by EvMore).
"""
return self._paginator(pageno)
[docs] def page_formatter(self, page):
"""
Page formatter. Every page passes through this method. Override
it to customize behavior per-page. A common use is to generate a new
EvTable for every page (this is more efficient than to generate one huge
EvTable across many pages and feed it into EvMore all at once).
Args:
page (any): A piece of data representing one page to display. This must
Returns:
str: A ready-formatted page to display. Extra footer with help about
switching to the next/prev page will be added automatically
"""
return self._page_formatter(page)
# helper function
[docs]def msg(
caller,
text="",
always_page=False,
session=None,
justify=False,
justify_kwargs=None,
exit_on_lastpage=True,
**kwargs,
):
"""
EvMore-supported version of msg, mimicking the normal msg method.
Args:
caller (Object or Account): Entity reading the text.
text (str, EvTable or iterator): The text or data to put under paging.
- If a string, paginage normally. If this text contains
one or more \\\\f (backslash + f) format symbol, automatic pagination is disabled
and page-breaks will only happen after each \\\\f.
- If `EvTable`, the EvTable will be paginated with the same
setting on each page if it is too long. The table
decorations will be considered in the size of the page.
- Otherwise `text` is converted to an iterator, where each step is
is expected to be a line in the final display, and each line
will be run through repr().
always_page (bool, optional): If `False`, the
pager will only kick in if `text` is too big
to fit the screen.
session (Session, optional): If given, this session will be used
to determine the screen width and will receive all output.
justify (bool, optional): If set, justify long lines in output. Disable for
fixed-format output, like tables.
justify_kwargs (dict, bool or None, optional): If given, this should
be valid keyword arguments to the utils.justify() function. If False,
no justification will be done.
exit_on_lastpage (bool, optional): Immediately exit pager when reaching the last page.
use_evtable (bool, optional): If True, each page will be rendered as an
EvTable. For this to work, `text` must be an iterable, where each element
is the table (list of list) to render on that page.
evtable_args (tuple, optional): The args to use for EvTable on each page.
evtable_kwargs (dict, optional): The kwargs to use for EvTable on each
page (except `table`, which is supplied by EvMore per-page).
kwargs (any, optional): These will be passed on
to the `caller.msg` method.
"""
EvMore(
caller,
text,
always_page=always_page,
session=session,
justify=justify,
justify_kwargs=justify_kwargs,
exit_on_lastpage=exit_on_lastpage,
**kwargs,
)