Source code for x84.bbs.lightbar

""" Lightbar package for x/84. """

# local imports
from x84.bbs.ansiwin import AnsiWindow
from x84.bbs.session import getterminal
from x84.bbs.output import decode_pipe, echo


#: default command-key mapping.
NETHACK_KEYSET = {'home': [u'y', '0'],
                  'end': [u'n', 'G'],
                  'pgup': [u'h', u'b'],
                  'pgdown': [u'l', u'f'],
                  'up': [u'k'],
                  'down': [u'j'],
                  'enter': [u'\r'],
                  'exit': [u'q', u'Q', unichr(27), ],
                  }


[docs]class Lightbar(AnsiWindow): """ This Windowing class offers a classic 'lightbar' interface. Instantiate with yloc, xloc, height, and width, then call the update method with a list of unicode strings. send keycodes to process_keystroke () to interactive with the 'lightbar'. """ # pylint: disable=R0902 # Too many instance attributes (15/7) # pylint: disable=R0904 # Too many public methods (29/20) def __init__(self, *args, **kwargs): """ Class initializer. Initialize a lightbar of height, width, y and x, and position. :param int width: width of window. :param int height: height of window. :param int yloc: y-location of window. :param int xloc: x-location of window. :param dict colors: color theme, only key value of ``highlight`` is used. :param dict glyphs: bordering window character glyphs. :param dict keyset: command keys, global ``NETHACK_KEYSET`` is used by default, augmented by application keys such as home, end, pgup, etc. :param list content: Lightbar content as list of tuples, an empty list is used by default. Tuples must be in form of ``(key, str)``. ``key`` may have any suitable significance for the caller. ``str``, however, must be of a unicode terminal sequence. """ self._selected = False self._quit = False self._vitem_idx = self._vitem_shift = -1 self.content = kwargs.pop('content', list()) pos = kwargs.pop('position', (0, 0)) or (0, 0) self.init_keystrokes( keyset=kwargs.pop('keyset', NETHACK_KEYSET.copy())) AnsiWindow.__init__(self, *args, **kwargs) self.position = pos
[docs] def init_theme(self, colors=None, glyphs=None): """ Set color and bordering glyphs theme. """ colors = colors or {'highlight': getterminal().reverse_yellow} glyphs = glyphs or {'strip': u' $'} AnsiWindow.init_theme(self, colors, glyphs)
[docs] def init_keystrokes(self, keyset): """ Sets keyboard keys for various editing keystrokes. """ term = getterminal() self.keyset = keyset self.keyset['home'].append(term.KEY_HOME) self.keyset['end'].append(term.KEY_END) self.keyset['pgup'].append(term.KEY_PGUP) self.keyset['pgdown'].append(term.KEY_PGDOWN) self.keyset['up'].append(term.KEY_UP) self.keyset['down'].append(term.KEY_DOWN) self.keyset['enter'].append(term.KEY_ENTER) self.keyset['exit'].append(term.KEY_ESCAPE)
[docs] def update(self, keyed_uchars=None): """ Replace content with with sequence of (key, str). ``key`` may have any suitable significance for the caller. ``str``, however, must be of a unicode terminal sequence. """ if keyed_uchars is None: keyed_uchars = (None, u'',) self.content = list(keyed_uchars) self.position = (self.vitem_idx, self.vitem_shift)
[docs] def refresh_row(self, row): """ Return string sequence suitable for refreshing current selection. Return unicode byte sequence suitable for moving to location ypos of window-relative row, and displaying any valid entry there, or using glyphs['erase'] if out of bounds. Strings are ansi color safe, and will be trimmed using glyphs['strip'] if their displayed width is wider than window. """ term = getterminal() pos = self.pos(self.ypadding + row, self.xpadding) entry = self.vitem_shift + row if entry >= len(self.content): # out-of-bounds; disp_erase = self.glyphs.get('erase', u' ') * self.visible_width return u''.join((pos, disp_erase,)) def fit_row(ucs): """ Strip a unicode row to fit window boundry, if necessary """ column = self.visible_width - 1 wrapped = term.wrap(ucs, column) if len(wrapped) > 1: marker = self.glyphs.get('strip', u' $') marker_column = self.visible_width - len(marker) wrapped = term.wrap(ucs, marker_column) ucs = term.ljust(wrapped[0].rstrip(), marker_column) + marker return ucs return term.ljust(ucs, column) # allow ucs data with '\r\n', to accomidate soft and hardbreaks; just # don't display them, really wrecks up cusor positioning. ucs = self.content[entry][1].strip(u'\r\n') # highlighted entry; strip of ansi sequences, use color 'highlight' # trim and append '$ ' if it cannot fit, if entry == self.index: ucs = term.strip_seqs(ucs) if term.length(ucs) > self.visible_width: ucs = fit_row(ucs) return u''.join((pos, term.normal, self.colors.get('highlight', u''), self.align(ucs), term.normal,)) # unselected entry; retain ansi sequences, decode any pipe characters, # trim and append '$ ' if it cannot fit ucs = decode_pipe(ucs) if term.length(ucs) > self.visible_width: ucs = fit_row(ucs) return u''.join((pos, self.colors.get('lowlight', u''), self.align(ucs), term.normal,))
[docs] def fixate(self): """ Return string sequence suitable for "fixating" cursor position. """ return self.pos(self.ypadding + self.vitem_idx, self.xpadding + self.visible_width)
[docs] def refresh(self): """ Return string sequence suitable for refreshing lightbar. """ return u''.join(self.refresh_row(ypos) for ypos in range(max(self.visible_bottom, self.visible_height)))
[docs] def refresh_quick(self): """ Redraw only the 'dirty' portions after a 'move' has occurred. """ if self.moved: if (self._vitem_lastshift != self.vitem_shift): # page shift, refresh entire page return self.refresh() if self._vitem_lastidx != self.vitem_idx: # unhighlight last selection, highlight new return (self.refresh_row(self._vitem_lastidx) + self.refresh_row(self.vitem_idx)) else: # just highlight new .. return (self.refresh_row(self.vitem_idx)) return u''
[docs] def process_keystroke(self, keystroke): """ Process the keystroke and return string to refresh. :param blessed.keyboard.Keystroke keystroke: input from ``inkey()``. :rtype: str :returns: string sequence suitable for refresh. """ self._moved = False self._selected = False self._vitem_lastidx = self.vitem_idx self._vitem_lastshift = self.vitem_shift rstr = u'' if (keystroke in self.keyset['home'] or keystroke.code in self.keyset['home']): rstr = self.move_home() elif (keystroke in self.keyset['end'] or keystroke.code in self.keyset['end']): rstr = self.move_end() elif (keystroke in self.keyset['pgup'] or keystroke.code in self.keyset['pgup']): rstr = self.move_pageup() elif (keystroke in self.keyset['pgdown'] or keystroke.code in self.keyset['pgdown']): rstr = self.move_pagedown() elif (keystroke in self.keyset['up'] or keystroke.code in self.keyset['up']): rstr = self.move_up() elif (keystroke in self.keyset['down'] or keystroke.code in self.keyset['down']): rstr = self.move_down() elif (keystroke in self.keyset['enter'] or keystroke.code in self.keyset['enter']): self.selected = True elif (keystroke in self.keyset['exit'] or keystroke.code in self.keyset['exit']): self._quit = True return rstr
[docs] def read(self): """ Reads input until the ENTER or ESCAPE key is pressed (Blocking). Returns selection content, or None when canceled. """ self._selected = False self._quit = False echo(self.refresh()) term = getterminal() while not (self.selected or self.quit): echo(self.process_keystroke(term.inkey())) if self.quit: return None return self.selection[0]
@property def quit(self): """ Whether a 'quit' character has been handled, such as escape. """ return self._quit @property def index(self): """ Selected index of self.content. """ return self.vitem_shift + self.vitem_idx @property def at_bottom(self): """ Whether current selection is pointed at final entry. """ return self.index == len(self.content) - 1 @property def at_top(self): """ Whether current selection is pointed at the first entry. """ return self.index == 0 @property def selection(self): """ Selected content of self.content by index. """ return (self.content[self.index] if self.index >= 0 and self.index < len(self.content) else (None, None)) @property def selected(self): """ Whether carriage return was detected by process_keystroke. """ return self._selected @selected.setter def selected(self, value): # pylint: disable=C0111 # Missing docstring # this setter should only be used to reset to 'False' for recycling assert isinstance(value, bool) self._selected = value @property def last_index(self): """ Previously selected index of self.content. """ return self._vitem_lastshift + self._vitem_lastidx @property def position(self): """ Tuple pair (row, page). ``row`` is the index from top of window, and 'page' is number of page items scrolled. """ return (self.vitem_idx, self.vitem_shift) @position.setter def position(self, pos_tuple): # pylint: disable=C0111 # Missing docstring self.vitem_idx, self.vitem_shift = pos_tuple self._chk_bounds() @property def visible_content(self): """ Returns visible content only. """ _, shift = self.position return self.content[shift:shift + self.visible_height] @property def visible_bottom(self): """ Visible bottom-most item of lightbar. """ if self.vitem_shift + (self.visible_height - 1) > len(self.content): return len(self.content) else: return self.visible_height @property def vitem_idx(self): """ Relative visible item index within view. Index of selected item relative by index to only the length of the list that is visible, without accounting for scrolled content. """ return self._vitem_idx @vitem_idx.setter def vitem_idx(self, value): # pylint: disable=C0111 # Missing docstring if self._vitem_idx != value: self._vitem_lastidx = self._vitem_idx self._moved = True self._vitem_idx = value @property def vitem_shift(self): """ Index of top-most item in viewable window, non-zero when scrolled. This value effectively represents the number of items not in view due to paging. """ return self._vitem_shift @vitem_shift.setter def vitem_shift(self, value): # pylint: disable=C0111 # Missing docstring if self._vitem_shift != value: self._vitem_lastshift = self._vitem_shift self._moved = True self._vitem_shift = value def _chk_bounds(self): """ Shift pages and selection until selection is within bounds. """ # if selected item is out of range of new list, then scroll to last # page, and move selection to end of screen, if self.vitem_shift and (self.index + 1) > len(self.content): self.vitem_shift = len(self.content) - self.visible_height + 1 self.vitem_idx = self.visible_height - 2 # if we are a shifted window, shift 1 line up while keeping our # lightbar position until the bottom-most item is within visable range. while (self.vitem_shift and self.vitem_shift + self.visible_height - 1 > len(self.content)): self.vitem_shift -= 1 self.vitem_idx += 1 # When a window is not shiftable, ensure selection is less than # total items. (truncate to last item) while self.vitem_idx > 0 and self.index >= len(self.content): self.vitem_idx -= 1
[docs] def move_down(self): """ Move selection down one row, return string suitable for refresh. """ if self.at_bottom: # bounds check return u'' if self.vitem_idx + 1 < self.visible_bottom: # move down 1 row self.vitem_idx += 1 elif self.vitem_idx < len(self.content): # scroll down 1 row self.vitem_shift += 1 return self.refresh_quick()
[docs] def goto(self, index): """ Move selection to given index. """ assert index >= 0 and index < len(self.content) row, shift = self.position while (row + shift) < index: if row < (self.visible_height - 1): row += 1 else: shift += 1 while (row + shift) > index: if row > 0: row -= 1 else: shift -= 1 self.position = row, shift return self.refresh_quick()
[docs] def move_up(self): """ Move selection up one row, return string suitable for refresh. """ if self.at_top: # bounds check return u'' elif self.vitem_idx >= 1: # move up 1 row self.vitem_idx -= 1 elif self.vitem_shift > 0: # scroll up 1 row self.vitem_shift -= 1 return self.refresh_quick()
[docs] def move_pagedown(self): """ Move selection down one page, return string suitable for refresh. """ if len(self.content) < self.visible_height: # move to last entry if self.vitem_idx == len(self.content) - 1: return u'' # already at end self.vitem_idx = len(self.content) - 1 elif (self.vitem_shift + self.visible_height < (len(self.content) - self.visible_height)): # previous page self.vitem_shift = self.vitem_shift + self.visible_height elif self.vitem_shift != len(self.content) - self.visible_height: # shift window to last page self.vitem_shift = len(self.content) - self.visible_height else: # already at last page, goto end return self.move_end() return self.refresh_quick()
[docs] def move_pageup(self): """ Move selection up one page, return string suitable for refresh. """ if len(self.content) < self.visible_height - 1: self.vitem_idx = 0 if self.vitem_shift - self.visible_height > 0: # previous page self.vitem_shift = self.vitem_shift - self.visible_height elif self.vitem_shift > 0: # shift window to first page self.vitem_shift = 0 else: # already at first page, goto home return self.move_home() return self.refresh_quick()
[docs] def move_home(self): """ Move selection to first row, return string suitable for refresh. """ if (0, 0) == (self.vitem_idx, self.vitem_shift): return u'' # already at home self.vitem_idx = 0 self.vitem_shift = 0 return self.refresh_quick()
[docs] def move_end(self): """ Move selection to final row, return string suitable for refresh. """ if len(self.content) < self.visible_height: if self.vitem_idx == len(self.content) - 1: return u'' # already at end self.vitem_idx = len(self.content) - 1 else: self.vitem_shift = len(self.content) - self.visible_height self.vitem_idx = self.visible_height - 1 return self.refresh_quick()