Source code for evennia.utils.evmore

# -*- 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:

```python
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 to avoid
having to set up the `EvMenu` object manually:

```python
    from evennia.utils import evmore

    text = some_long_text_output()
    evmore.msg(caller, text, always_page=False, session=None, justify_kwargs=None, **kwargs)
```

The `always_page` argument  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 evennia import Command, CmdSet
from evennia.commands import cmdhandler
from evennia.utils.ansi import ANSIString
from evennia.utils.utils import make_iter, inherits_from, justify, dedent

_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

_LBR = ANSIString("\n")

# 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(object): """ The main pager object """
[docs] def __init__( self, caller, text, always_page=False, session=None, justify=False, justify_kwargs=None, exit_on_lastpage=False, exit_cmd=None, page_formatter=str, **kwargs, ): """ EvMore pager 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, paginate normally. If this text contains one or more `\\\\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 `text` 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 `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, auto-justify long lines. This must be turned off for fixed-width or formatted output, like tables. It's force-disabled if `text` is an EvTable. justify_kwargs (dict, optional): Keywords for the justifiy 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). page_formatter (callable, optional): If given, this function will be passed the contents of each extracted page. This is useful when paginating data consisting something other than a string or a list of strings. Especially queryset data is likely to always need this argument specified. Note however, that all size calculations assume this function to return one single line per element on the page! kwargs (any, optional): These will be passed on to the `caller.msg` method. Examples: ```python super_long_text = " ... " EvMore(caller, super_long_text) from django.core.paginator import Paginator query = ObjectDB.objects.all() pages = Paginator(query, 10) # 10 objs per page EvMore(caller, pages) # will repr() each object per line, 10 to a page multi_page_table = [ [[..],[..]], ...] EvMore(caller, multi_page_table, use_evtable=True, evtable_args=("Header1", "Header2"), evtable_kwargs={"align": "r", "border": "tablecols"}) ``` """ 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._page_formatter = page_formatter self._kwargs = kwargs self._data = None self._paginator = None self._pages = [] self._npages = 1 self._npos = 0 # 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) if inherits_from(text, "evennia.utils.evtable.EvTable"): # an EvTable self.init_evtable(text) elif isinstance(text, QuerySet): # a queryset self.init_queryset(text) elif not isinstance(text, str): # anything else not a str self.init_iterable(text) elif "\f" in text: # string with \f line-break markers in it self.init_f_str(text) else: # a string self.init_str(text) # kick things into gear self.start()
# page formatter
[docs] def format_page(self, page): """ Page formatter. Uses the page_formatter callable by default. This allows to easier override the class if needed. """ return self._page_formatter(page)
# 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]
# inits for different 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 self._paginator = self.paginator_slice
[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 self._paginator = self.paginator_slice
[docs] def init_f_str(self, text): """ The input contains `\\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. Args: text (str): The string to format with f-markers. """ self._data = text.split("\f") self._npages = len(self._data) self._paginator = self.paginator_index
[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 = [ _LBR.join(lines[i : i + self.height]) for i in range(0, len(lines), self.height) ] self._npages = len(self._data) self._paginator = self.paginator_index
# display helpers and navigation
[docs] def display(self, show_footer=True): """ Pretty-print the page. """ pos = self._npos text = self.format_page(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()
# 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. """ EvMore( caller, text, always_page=always_page, session=session, justify=justify, justify_kwargs=justify_kwargs, exit_on_lastpage=exit_on_lastpage, **kwargs, )
msg.__doc__ += dedent(EvMore.__init__.__doc__)