def pos(self, yloc=None, xloc=None): """ Returns terminal sequence to move cursor to window-relative position. """ term = getterminal() return term.move((yloc if yloc is not None else 0) + self.yloc, (xloc if xloc is not None else 0) + self.xloc)
def __init__(self, height, width, yloc, xloc, colors=None, glyphs=None): """ Class constructor for base windowing class. :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. """ self.height = height self.width = width self.yloc = yloc self.xloc = xloc self.init_theme(colors, glyphs) self._xpadding = 1 self._ypadding = 1 self._alignment = 'left' self._moved = False if not self.isinview(): # https://github.com/jquast/x84/issues/161 warnings.warn( 'AnsiWindow(height={self.height}, width={self.width}, ' 'yloc={self.yloc}, xloc={self.xloc}) not in viewport ' 'Terminal(height={term.height}, width={term.width})' .format(self=self, term=getterminal()), FutureWarning)
def eol(self): """ Return True when no more input can be accepted (end of line). """ from x84.bbs.session import getterminal term = getterminal() return term.length(self.content) >= self.max_length
def isinview(self): """ Whether this window is in bounds of terminal dimensions. """ term = getterminal() return (self.xloc >= 0 and self.xloc + self.width <= term.width and self.yloc >= 0 and self.yloc + self.height <= term.height)
def __init__(self, *args, **kwargs): """ Class constructor. :param int width: width of window. :param int yloc: y-location of window. :param int xloc: x-location of window. :param int max_length: maximum length of input (may be larger than width). :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 ``PC_KEYSET`` is used by default. """ self._term = getterminal() self._horiz_shift = 0 self._horiz_pos = 0 # self._enable_scrolling = False self._horiz_lastshift = 0 self._scroll_pct = kwargs.pop('scroll_pct', 25.0) self._margin_pct = kwargs.pop('margin_pct', 10.0) self._carriage_returned = False self._max_length = kwargs.pop('max_length', 0) self._quit = False self._bell = False self.content = kwargs.pop('content', u'') self._input_length = self._term.length(self.content) # there are some flaws about how a 'height' of a window must be # '3', even though we only want 1; we must also offset (y, x) by # 1 and width by 2: issue #161. kwargs['height'] = 3 self.init_keystrokes(keyset=kwargs.pop('keyset', PC_KEYSET.copy())) AnsiWindow.__init__(self, *args, **kwargs)
def ansiwrap(ucs, width=70, **kwargs): """Wrap a single paragraph of Unicode Ansi sequences, returning a list of wrapped lines. """ warnings.warn('ansiwrap() deprecated, getterminal() now' 'supplies an equivalent .wrap() API') return getterminal().wrap(text=ucs, width=width, **kwargs)
def encode_pipe(ucs): """ encode_pipe(ucs) -> unicode Return new unicode terminal sequence, replacing EMCA-48 ANSI color sequences with their pipe-equivalent values. """ # TODO: Support all kinds of terminal color sequences, # such as kermit or avatar or some such .. something non-emca outp = u'' nxt = 0 term = getterminal() ANSI_COLOR = re.compile(r'\033\[(\d{2,3})m') for idx in range(0, len(ucs)): if idx == nxt: # at sequence, point beyond it, match = ANSI_COLOR.match(ucs[idx:]) if match: #nxt = idx + measure_length(ucs[idx:], term) nxt = idx + len(match.group(0)) # http://wiki.mysticbbs.com/mci_codes value = int(match.group(1)) - 30 if value >= 0 and value <= 60: outp += u'|%02d' % (value,) if nxt <= idx: # append non-sequence to outp, outp += ucs[idx] # point beyond next sequence, if any, # otherwise point to next character nxt = idx + 1 #measure_length(ucs[idx:], term) + 1 return outp
def process_keystroke(self, keystroke): """ Process the keystroke received by run method and return terminal sequence suitable for refreshing when that keystroke modifies the window. """ from x84.bbs.session import getterminal term = getterminal() self.moved = False rstr = u'' if keystroke in self.keyset['refresh']: rstr += self.refresh() elif keystroke in self.keyset['up']: rstr += self.move_up() elif keystroke in self.keyset['down']: rstr += self.move_down() elif keystroke in self.keyset['home']: rstr += self.move_home() elif keystroke in self.keyset['end']: rstr += self.move_end() elif keystroke in self.keyset['pgup']: rstr += self.move_pgup() elif keystroke in self.keyset['pgdown']: rstr += self.move_pgdown() elif keystroke in self.keyset['exit']: self._quit = True else: logger = logging.getLogger() logger.debug('unhandled, %r', keystroke) return rstr
def backspace(self): """ Remove character from end of content buffer, scroll as necessary. """ if 0 == len(self.content): return u'' from x84.bbs.session import getterminal term = getterminal() rstr = u'' # measured backspace erases over double-wide len_toss = term.length(self.content[-1]) len_move = len(self.content[-1]) self.content = self.content[:-1] if (self.is_scrolled and (self._horiz_pos < self.scroll_amt)): # shift left, self._horiz_shift -= self.scroll_amt self._horiz_pos += self.scroll_amt rstr += self.refresh() else: rstr += u''.join(( self.fixate(0), u'\b' * len_toss, u' ' * len_move, u'\b' * len_move,)) self._horiz_pos -= 1 return rstr
def __init__(self, cmd='/bin/uname', args=(), env=None, cp437=False): """ Class initializer. :param str cmd: full path of command to execute. :param tuple args: command arguments as tuple. :param bool cp437: When true, forces decoding of external program as codepage 437. This is the most common encoding used by DOS doors. :param dict env: Environment variables to extend to the sub-process. You should more than likely specify values for TERM, PATH, HOME, and LANG. """ self._session, self._term = getsession(), getterminal() self.cmd = cmd if isinstance(args, tuple): self.args = (self.cmd,) + args elif isinstance(args, list): self.args = [self.cmd, ] + args else: raise ValueError('args must be tuple or list') self.log = logging.getLogger(__name__) self.env = (env or {}).copy() self.env.update( {'LANG': env.get('LANG', 'en_US.UTF-8'), 'TERM': env.get('TERM', self._term.kind), 'PATH': env.get('PATH', get_ini('door', 'path')), 'HOME': env.get('HOME', os.getenv('HOME')), 'LINES': str(self._term.height), 'COLUMNS': str(self._term.width), }) self.cp437 = cp437 self._utf8_decoder = codecs.getincrementaldecoder('utf8')()
def title(self, ansi_text): """ Returns sequence that positions and displays unicode sequence 'ansi_text' at the title location of the window. """ term = getterminal() xloc = self.width / 2 - min(term.length(ansi_text) / 2, self.width / 2) return self.pos(0, max(0, xloc)) + ansi_text
def init_theme(self): """ Initialize color['highlight']. """ from x84.bbs.session import getterminal self.colors['highlight'] = getterminal().reverse_green self.glyphs['strip'] = u' $' # indicates content was stripped AnsiWindow.init_theme(self)
def init_theme(self, colors=None, glyphs=None): from x84.bbs.session import getterminal term = getterminal() colors = colors or { 'selected': term.reverse_yellow, 'unselected': term.bold_black, } AnsiWindow.init_theme(self, colors=colors, glyphs=glyphs)
def init_theme(self, colors=None, glyphs=None): """ Initialize color['highlight']. """ from x84.bbs.session import getterminal colors = colors or {'highlight': getterminal().reverse_yellow} glyphs = glyphs or {'strip': u' $'} AnsiWindow.init_theme(self, colors, glyphs)
def footer(self, ansi_text): """ Returns sequence that positions and displays unicode sequence 'ansi_text' at the bottom edge of the window. """ term = getterminal() xloc = self.width / 2 - min(term.length(ansi_text) / 2, self.width / 2) return self.pos(max(0, self.height - 1), max(0, xloc)) + ansi_text
def init_theme(self, colors=None, glyphs=None): AnsiWindow.init_theme(self, colors, glyphs) if 'highlight' not in self.colors: from x84.bbs.session import getterminal term = getterminal() self.colors['highlight'] = term.yellow_reverse if 'strip' not in self.glyphs: self.glyphs['strip'] = u'$ '
def isinview(self): """ Returns True if window is in view of the terminal window. """ term = getterminal() return (self.xloc >= 0 and self.xloc + self.width <= term.width and self.yloc >= 0 and self.yloc + self.height <= term.height)
def init_theme(self): """ Initialize colors['selected'] and colors['unselected']. """ from x84.bbs.session import getterminal term = getterminal() AnsiWindow.init_theme(self) self.colors['selected'] = term.reverse self.colors['unselected'] = term.normal
def init_keystrokes(self, keyset): """ Sets keyboard keys for various editing keystrokes. """ from x84.bbs.session import getterminal self.keyset = keyset term = getterminal() self.keyset['refresh'].append(term.KEY_REFRESH) self.keyset['left'].append(term.KEY_LEFT) self.keyset['right'].append(term.KEY_RIGHT) self.keyset['enter'].append(term.KEY_ENTER) self.keyset['exit'].append(term.KEY_ESCAPE)
def _content_wrap(self, ucs): """ Return word-wrapped text ``ucs`` that contains newlines. """ term = getterminal() lines = [] for line in ucs.splitlines(): if line.strip(): lines.extend(term.wrap(line, self.visible_width - 1)) else: lines.append(u'') return lines
def refresh(self): """ Returns unicode byts suitable for drawing line. No movement or positional sequences are returned. """ from x84.bbs.session import getterminal lightbar = u"".join((self.colors.get("highlight", u""), " " * self.width, "\b" * self.width)) content = self.hidden * len(Ansi(self.content)) if self.hidden else self.content return u"".join((lightbar, content, getterminal().cursor_visible))
def fixate(self, x_adjust=0): """ Return terminal sequence suitable for placing cursor at current position in window. Set x_adjust to -1 to position cursor 'on' the last character, or 0 for 'after' (default). """ from x84.bbs.session import getterminal xpos = self._xpadding + self._horiz_pos + x_adjust return self.pos(1, xpos) + getterminal().cursor_visible
def refresh_row(self, row): """ 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. """ from x84.bbs.session import getterminal from x84.bbs.output import decode_pipe 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,))
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['down'].append(term.KEY_ENTER) self.keyset['exit'].append(term.KEY_ESCAPE)
def align(self, text, width=None): """ Return ``text`` alignmnd to ``width`` using self.alignment. When None (default), the visible width of this window is used. """ term = getterminal() width = width if width is not None else (self.visible_width) return (term.rjust(text, width) if self.alignment == 'right' else term.ljust(text, width) if self.alignment == 'left' else term.center(text, width) )
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["down"].append(term.KEY_ENTER) self.keyset["exit"].append(term.KEY_ESCAPE)
def init_keystrokes(self, keyset): """ This initializer sets keyboard keys for backspace/exit. """ from x84.bbs.session import getterminal term = getterminal() self.keyset = keyset self.keyset['refresh'].append(term.KEY_REFRESH) self.keyset['backspace'].append(term.KEY_BACKSPACE) self.keyset['backspace'].append(term.KEY_DELETE) self.keyset['enter'].append(term.KEY_ENTER) self.keyset['exit'].append(term.KEY_ESCAPE)
def init_keystrokes(self): """ Merge curses-detected application keys into a VI_KEYSET-formatted keyset, for keys 'refresh', 'left', 'right', 'enter', and 'exit'. """ from x84.bbs.session import getterminal term = getterminal() self.keyset['refresh'].append(term.KEY_REFRESH) self.keyset['left'].append(term.KEY_LEFT) self.keyset['right'].append(term.KEY_RIGHT) self.keyset['enter'].append(term.KEY_ENTER) self.keyset['exit'].append(term.KEY_ESCAPE)
def read(self): """ Reads input until the ENTER or ESCAPE key is pressed (Blocking). """ from x84.bbs import getterminal from x84.bbs.output import echo self._selected = False self._quit = False echo(self.refresh()) term = getterminal() while not (self.selected or self.quit): echo(self.process_keystroke(term.inkey()) or u'') if self.quit: return None return self.selection
def refresh_row(self, row): """ Return unicode string suitable for refreshing pager row. :param int row: target row by visible index. :rtype: str """ term = getterminal() ucs = u"" if row < len(self.visible_content): ucs = self.visible_content[row] disp_position = self.pos(row + self.ypadding, self.xpadding) return u"".join((term.normal, disp_position, self.align(ucs), term.normal))
def init_keystrokes(self, keyset): """ This initializer sets keyboard keys for various editing keystrokes. """ from x84.bbs.session import getterminal 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)
def refresh(self): """ Returns unicode suitable for drawing edit line. No movement or positional sequences are returned. """ from x84.bbs.session import getterminal term = getterminal() disp_lightbar = u''.join( (term.normal, self.colors.get('highlight', u''), ' ' * self.width, '\b' * self.width)) content = self.content if self.hidden: content = self.hidden * term.length(self.content) return u''.join((disp_lightbar, content, term.cursor_visible))
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]
def init_theme(self): """ This initializer sets glyphs and colors appropriate for a "theme", override this method to create a common color and graphic set. """ session = getsession() term = getterminal() self.colors['normal'] = term.normal if term.number_of_colors != 0: self.colors['border'] = term.cyan # start with default 'ascii' self.glyphs = GLYPHSETS['ascii'].copy() # PC-DOS 'thin' on smart terminals if session.env.get('TERM') != 'unknown': self.glyphs = GLYPHSETS['thin'].copy()
def read(self): """ Reads input until the ENTER or ESCAPE key is pressed (Blocking). Allows backspacing. Returns unicode text, or None when canceled. """ echo(self.refresh()) self._quit = False self._carriage_returned = False term = getterminal() while not (self.quit or self.carriage_returned): inp = term.inkey() echo(self.process_keystroke(inp)) if not self.quit: return self.content return None
def refresh(self, start_row=0): """ Return unicode string suitable for refreshing pager window. :param int start_row: refresh from only visible row 'start_row' and downward. This can be useful if only the last line is modified; or in an 'insert' operation: only the last line need be refreshed. :rtype: str """ term = getterminal() return u''.join([term.normal] + [ self.refresh_row(row) for row in range(start_row, len(self.visible_content)) ] + [term.normal])
def init_theme(self, colors=None, glyphs=None): """ This initializer sets glyphs and colors appropriate for a "theme". """ # set defaults, term = getterminal() self.colors = { 'normal': term.normal, 'border': term.number_of_colors and term.cyan or term.normal, } self.glyphs = GLYPHSETS['thin'].copy() # allow user override if colors is not None: self.colors.update(colors) if glyphs is not None: self.glyphs.update(glyphs)
def refresh_row(self, row): """ Return unicode string suitable for refreshing pager row. :param int row: target row by visible index. :rtype: str """ term = getterminal() ucs = u'' if row < len(self.visible_content): ucs = self.visible_content[row] disp_position = self.pos(row + self.ypadding, self.xpadding) return u''.join((term.normal, disp_position, self.align(ucs), term.normal))
def decode_pipe(ucs): """ Return ucs containing 'pipe codes' with terminal color sequences. These are sometimes known as LORD codes, as they were used in the DOS Door game of the same name. Compliments :func:`encode_pipe`. :param str ucs: string containing 'pipe codes'. :rtype: str """ # simple optimization, no '|' ? exit early! if u'|' not in ucs: return ucs term = getterminal() outp = u'' ptr = 0 match = None ANSI_PIPE = re.compile(r'\|(\d{2,3}|\|)') for match in ANSI_PIPE.finditer(ucs): val = match.group(1) # allow escaping using a second pipe if val == u'|': outp += ucs[ptr:match.start() + 1] ptr = match.end() continue # 0..7 -> 7 while val.startswith('0'): val = val[1:] int_value = 0 if 0 == len(val) else int(val, 10) assert int_value >= 0 and int_value <= 256 # colors 0-7 and 16-256 are as-is term.color() # special accommodations for 8-15, some termcaps are ok # with term.color(11), whereas others have trouble, help # out by using dim color and bold attribute instead. attr = u'' if int_value == 7: attr = term.normal elif int_value < 7 or int_value >= 16: attr = term.normal + term.color(int_value) elif int_value <= 15: attr = term.normal + term.color(int_value - 8) + term.bold outp += ucs[ptr:match.start()] + attr ptr = match.end() outp = ucs if match is None else u''.join((outp, ucs[match.end():])) return u''.join((outp, term.normal))
def refresh(self): """ Returns unicode byts suitable for drawing line. No movement or positional sequences are returned. """ from x84.bbs.session import getterminal term = getterminal() lightbar = u''.join(( term.normal, self.colors.get('highlight', u''), ' ' * self.width, '\b' * self.width)) content = (self.hidden * len(Ansi(self.content)) if self.hidden else self.content) return u''.join(( lightbar, content, term.cursor_visible))
def __init__(self, cmd='/bin/uname', args=(), env=None, cp437=False, raw=False): """ Class initializer. :param str cmd: full path of command to execute. :param tuple args: command arguments as tuple. :param bool cp437: When true, forces decoding of external program as codepage 437. This is the most common encoding used by DOS doors. :param dict env: Environment variables to extend to the sub-process. You should more than likely specify values for TERM, PATH, HOME, and LANG. :param bool raw: Whether or not to use raw output """ self._session, self._term = getsession(), getterminal() self.cmd = cmd if isinstance(args, tuple): self.args = (self.cmd, ) + args elif isinstance(args, list): self.args = [ self.cmd, ] + args else: raise ValueError('args must be tuple or list') self.log = logging.getLogger(__name__) self.env = (env or {}).copy() self.env.update({ 'LANG': env.get('LANG', 'en_US.UTF-8'), 'TERM': env.get('TERM', self._term.kind), 'PATH': env.get('PATH', get_ini('door', 'path')), 'HOME': env.get('HOME', os.getenv('HOME')), 'LINES': str(self._term.height), 'COLUMNS': str(self._term.width), }) self.cp437 = cp437 self._utf8_decoder = codecs.getincrementaldecoder('utf8')() self.raw = raw
def decode_pipe(ucs): """ decode_pipe(ucs) -> unicode Return new terminal sequence, replacing 'pipe codes', such as u'|03' with this terminals equivalent attribute sequence. """ # simple optimization, no '|' ? exit early! if u'|' not in ucs: return ucs term = getterminal() outp = u'' ptr = 0 match = None ANSI_PIPE = re.compile(r'\|(\d{2,3}|\|)') for match in ANSI_PIPE.finditer(ucs): val = match.group(1) # allow escaping using a second pipe if val == u'|': outp += ucs[ptr:match.start() + 1] ptr = match.end() continue # 07 -> 7 while val.startswith('0'): val = val[1:] int_value = 0 if 0 == len(val) else int(val, 10) assert int_value >= 0 and int_value <= 256 # colors 0-7 and 16-256 are as-is term.color() # special accommodations for 8-15, some termcaps are ok # with term.color(11), whereas others have trouble, help # out by using dim color and bold attribute instead. attr = u'' if int_value == 7: attr = term.normal elif int_value < 7 or int_value >= 16: attr = term.normal + term.color(int_value) elif int_value <= 15: attr = term.normal + term.color(int_value - 8) + term.bold outp += ucs[ptr:match.start()] + attr ptr = match.end() outp = ucs if match is None else u''.join((outp, ucs[match.end():])) return u''.join((outp, term.normal))
def read(self): """ Reads input until the ENTER or ESCAPE key is pressed (Blocking). Allows backspacing. Returns unicode text, or None when canceled. """ from x84.bbs import getch from x84.bbs.output import echo from x84.bbs.session import getterminal term = getterminal() self._carriage_returned = False self._quit = False echo(self.refresh()) while not (self.quit or self.carriage_returned): inp = getch() echo(self.process_keystroke(inp)) echo(term.normal) if not self.quit: return self.content return None
def __init__(self, height, width, yloc, xloc, colors=None, glyphs=None): """ Constructor class for a simple Window. """ self.height = height self.width = width self.yloc = yloc self.xloc = xloc self.init_theme(colors, glyphs) self._xpadding = 1 self._ypadding = 1 self._alignment = 'left' self._moved = False assert self.isinview(), ( 'AnsiWindow(height={self.height}, width={self.width}, ' 'yloc={self.yloc}, xloc={self.xloc}) not in viewport ' 'Terminal(height={term.height}, width={term.width})' .format(self=self, term=getterminal()))
def __init__(self, height, width, yloc, xloc): """ Construct an ansi window. Its base purpose is to provide window-relativie positions using the pos() method. """ self.height = height self.width = width self.yloc = yloc self.xloc = xloc self._xpadding = 1 self._ypadding = 1 self._alignment = 'left' self._moved = False self.glyphs = dict() self.colors = dict() self.init_theme() term = getterminal() assert self.isinview(), ( 'AnsiWindow(height=%s, width=%s, yloc=%s, xloc=%s)' ' not in viewport Terminal(height=%s, width=%s)' % (height, width, yloc, xloc, term.height, term.width))
def process_keystroke(self, keystroke): """ Process the keystroke received by read method and take action. """ from x84.bbs.session import getterminal term = getterminal() self._quit = False if keystroke in self.keyset['refresh']: return u'\b' * term.length(self.content) + self.refresh() elif keystroke in self.keyset['backspace']: if len(self.content) != 0: len_toss = term.length(self.content[-1]) self.content = self.content[:-1] return u''.join(( u'\b' * len_toss, u' ' * len_toss, u'\b', )) elif keystroke in self.keyset['backword']: if len(self.content) != 0: ridx = self.content.rstrip().rfind(' ') + 1 toss = term.length(self.content[ridx:]) move = len(self.content[ridx:]) self.content = self.content[:ridx] return u''.join(( u'\b' * toss, u' ' * move, u'\b' * move, )) elif keystroke in self.keyset['enter']: self._carriage_returned = True elif keystroke in self.keyset['exit']: self._quit = True elif type(keystroke) is int: return u'' elif (ord(keystroke) >= ord(' ') and (term.length(self.content) < self.width or self.width is None)): self.content += keystroke return keystroke if not self.hidden else self.hidden return u''
def decode_pipe(self): """ S.decode_pipe() -> unicode Return new terminal sequence, replacing 'pipe codes', such as u'|03' with this terminals equivalent attribute sequence. """ term = getterminal() ucs = u'' ptr = 0 match = None for match in ANSI_PIPE.finditer(self): ucs_value = match.group(1) # allow escaping using a second pipe if match.start() and self[match.start() - 1] == '|': continue # 07 -> 7 while ucs_value.startswith('0'): ucs_value = ucs_value[1:] int_value = 0 if 0 == len(ucs_value) else int(ucs_value, 10) assert int_value >= 0 and int_value <= 256 # colors 0-7 and 16-256 are as-is term.color() # special accomidations for 8-15, some termcaps are ok # with term.color(11), whereas others have trouble, help # out by using dim color and bold attribute instead. attr = u'' if int_value <= 7 or int_value >= 16: attr = term.normal + term.color(int_value) elif int_value <= 15: attr = term.normal + term.color(int_value - 8) + term.bold ucs += self[ptr:match.start()] + attr ptr = match.end() if match is None: ucs = self else: ucs += self[match.end():] ptr = 0 return ''.join((ucs, term.normal))
def refresh(self): """ Return unicode sequence suitable for refreshing the entire line and placing the cursor. A strange by-product; if scrolling was not previously enabled, it is if wrapping must occur; this can happen if a non-scrolling editor was provided a very large .content buffer, then later .refresh()'d. -- essentially enabling infinate scrolling """ # reset position and detect new position from x84.bbs import getterminal term = getterminal() self._horiz_lastshift = self._horiz_shift self._horiz_shift = 0 self._horiz_pos = 0 for _count in range(term.length(self.content)): if (self._horiz_pos > (self.visible_width - self.scroll_amt)): self._horiz_shift += self.scroll_amt self._horiz_pos -= self.scroll_amt self.enable_scrolling = True self._horiz_pos += 1 if self._horiz_shift > 0: self._horiz_shift += len(self.glyphs['strip']) prnt = u''.join(( self.glyphs['strip'], self.content[self._horiz_shift:], )) else: prnt = self.content return u''.join(( self.pos(self.ypadding, self.xpadding), term.normal, self.colors.get('highlight', u''), self.align(prnt), self.fixate(), ))
def add(self, u_chr): """ Returns output sequence necessary to add a character to content buffer. An empty content buffer is returned if no data could be inserted. Sequences for re-displaying the full input line are returned when the character addition caused the window to scroll horizontally. Otherwise, the input is simply returned to be displayed ('local echo'). """ from x84.bbs import getterminal term = getterminal() if self.eol: # cannot input, at end of line! return u'' # append to input self.content += u_chr # return character appended as output, ensure .fixate() is used first! self._horiz_pos += 1 if self._horiz_pos >= (self.visible_width - self.margin_amt): # scrolling is required, return self.refresh() return term.normal + self.colors['highlight'] + u_chr
def __init__(self, width=None, content=u'', hidden=False, colors=None, glyphs=None, keyset=None): """ Class initializer. :param int width: the maximum input length. :param str content: given default content. :param str hidden: When non-False, a single 'mask' character for output. :param dict colors: optional dictionary containing key 'highlight'. :param dict glyphs: optional dictionary of window border characters. :param dict keyset: optional dictionary of line editing values. """ self._term = getterminal() self.content = content or u'' self.hidden = hidden self._width = width self._input_length = self._term.length(content) self._quit = False self._carriage_returned = False self.init_keystrokes(keyset=keyset or PC_KEYSET.copy()) self.init_theme(colors=colors, glyphs=glyphs)
def __init__(self, height, width, yloc, xloc, colors=None, glyphs=None): """ Class initializer for base windowing class. :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. :param dict glyphs: bordering window character glyphs. """ from x84.bbs.session import getterminal self._term = getterminal() self.height = height self.width = width self.yloc = yloc self.xloc = xloc self.init_theme(colors, glyphs) self._xpadding = 1 self._ypadding = 1 self._alignment = 'left' self._moved = False
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, ))
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)
def __new__(cls, object): warnings.warn('Ansi() deprecated, getterminal() now provides ' '.length(), .rjust(), .wrap(), etc.') new = Sequence.__new__(cls, object, getterminal()) return new
def showart(filepattern, encoding=None, auto_mode=True, center=False, poll_cancel=False, msg_cancel=None, force=False): """ Yield unicode sequences for any given ANSI Art (of art_encoding). Effort is made to parse SAUCE data, translate input to unicode, and trim artwork too large to display. If ``poll_cancel`` is not ``False``, represents time as float for each line to block for keypress -- if any is received, then iteration ends and ``msg_cancel`` is displayed as last line of art. If you provide no ``encoding``, the piece encoding will be based on either the encoding in the SAUCE record, the configured default or the default fallback ``CP437`` encoding. Alternate codecs are available if you provide the ``encoding`` argument. For example, if you want to show an Amiga style ASCII art file:: >>> from x84.bbs import echo, showart >>> for line in showart('test.asc', 'topaz'): ... echo(line) The ``auto_mode`` flag will, if set, only respect the selected encoding if the active session is UTF-8 capable. If ``center`` is set to ``True``, the piece will be centered respecting the current terminal's width. If ``force`` is set to true then the artwork will be displayed even if it's wider than the screen. """ # pylint: disable=R0913,R0914 # Too many arguments # Too many local variables term = getterminal() # When the given artfile pattern's folder is not absolute, nor relative to # our cwd, build a relative position of the folder by the calling module's # containing folder. This only works for subdirectories (like 'art/'). _folder = os.path.dirname(filepattern) if not (_folder.startswith(os.path.sep) or os.path.isdir(_folder)): # On occasion, after a general exception in a script, re-calling the # same script may cause yet another exception, HERE. The 2nd call is # fine though; this only would effect a developer. # # Just try again. caller_module = inspect.stack()[1][1] rel_folder = os.path.dirname(caller_module) if _folder: rel_folder = os.path.join(rel_folder, _folder) if os.path.isdir(rel_folder): filepattern = os.path.join(rel_folder, os.path.basename(filepattern)) # Open the piece try: filename = os.path.relpath(random.choice(glob.glob(filepattern))) except IndexError: filename = None if filename is None: yield u''.join(( term.bold_red(u'-- '), u'no files matching {0}'.format(filepattern), term.bold_red(u' --'), )) return file_basename = os.path.basename(filename) # Parse the piece parsed = SAUCE(filename) # If no explicit encoding is given, we go through a couple of steps to # resolve the possible file encoding: if encoding is None: # 1. See if the SAUCE record has a font we know about, it's in the # filler if parsed.record and parsed.filler_str in SAUCE_FONT_MAP: encoding = SAUCE_FONT_MAP[parsed.filler_str] # 2. Get the system default art encoding, # or fall-back to cp437 else: encoding = get_ini('system', 'art_utf8_codec') or 'cp437' # If auto_mode is enabled, we'll only use the input encoding on UTF-8 # capable terminals, because our codecs do not know how to "transcode" # between the various encodings. if auto_mode: def _decode(what): # pylint: disable=C0111 # Missing function docstring (col 8) session = getsession() if session.encoding == 'utf8': return what.decode(encoding) elif session.encoding == 'cp437': return what.decode('cp437') else: return what # If auto_mode is disabled, we'll just respect whatever input encoding was # selected before else: _decode = lambda what: what.decode(encoding) # For wide terminals, center piece on screen using cursor movement # when center=True. padding = u'' if center and term.width > 81: padding = term.move_x((term.width / 2) - 40) lines = _decode(parsed.data).splitlines() for idx, line in enumerate(lines): if poll_cancel is not False and term.inkey(poll_cancel): # Allow slow terminals to cancel by pressing a keystroke msg_cancel = msg_cancel or u''.join(( term.normal, term.bold_black(u'-- '), u'canceled {0} by input'.format(os.path.basename(filename)), term.bold_black(u' --'), )) yield u'\r\n' + term.center(msg_cancel).rstrip() + u'\r\n' return line_length = term.length(line.rstrip()) if force is False and not padding and term.width < line_length: # if the artwork is too wide and force=False, simply stop displaying it. msg_too_wide = u''.join(( term.normal, term.bold_black(u'-- '), (u'canceled {0}, too wide:: {1}'.format( file_basename, line_length)), term.bold_black(u' --'), )) yield (u'\r\n' + term.center(msg_too_wide).rstrip() + u'\r\n') return if idx == len(lines) - 1: # strip DOS end of file (^Z) line = line.rstrip('\x1a') if not line.strip(): break yield padding + line + u'\r\n' yield term.normal
def pageheight(self): """ Terminal height. """ return getterminal().height