Source code for console.screen

# -*- coding: future_fstrings -*-
'''
    .. console - Comprehensive utility library for ANSI terminals.
    .. © 2018, Mike Miller - Released under the LGPL, version 3+.

    This module generates ANSI character codes to manage terminal screens and
    move the cursor around, via a Screen class.

    For the cursor and view "move to" instruction,
    Screen classes default to standard (x, y) coordinate order and also use
    0-based locations as does Python curses.
    This means the coordinates of the the ``cup`` and ``hvp`` instructions
    will also have 1 added to each value on output.

    If you'd prefer a (y, x) coordinate order as in the ANSI/Curses sequences,
    pass the parameter swap=False on initialization.

    Context-managers with contextlib inspired by:

    .. code-block:: text

        blessings.__init__.py
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

        A thin, practical wrapper around terminal coloring, styling, and
        positioning.

        :copyright: Copyright 2011-2018, Erik Rose
        :license: MIT License (MIT)

'''
import sys
from contextlib import contextmanager

from . import ansi_capable as _ansi_capable, using_terminfo
from .constants import CSI, ESC, RIS
from .detection import get_position as _get_position, TermStack


# Mapping of convenience names to terminfo capabilities,
# using verb_object form:
NAME_TO_TERMINFO_MAP = dict(
    clear               = 'ed',
    clear_line          = 'el',
    delete_char         = 'dch',
    delete_line         = 'dl',
    erase_char          = 'ech',
    insert_line         = 'il',
    reset               = 'rs1',

    move_to             = 'cup',
    move_x              = 'hpa',
    move_y              = 'vpa',
    move_up             = 'cuu',
    move_down           = 'cud',
    move_right          = 'cuf',
    move_forward        = 'cuf',
    move_left           = 'cub',
    move_backward       = 'cub',
    scroll_down         = 'sd',
    scroll_up           = 'su',

    hide_cursor         = 'civis',
    show_cursor         = 'cnorm',
    save_cursor         = 'sc',     # color as well
    save_position       = 'sc',     # alias
    restore_cursor      = 'rc',     # alias
    restore_position    = 'rc',

    enable_alt_screen   = 'smcup',
    disable_alt_screen  = 'rmcup',
)


class _ContextMixin:
    ''' Various Blessings-inspired context handlers are defined here. '''

    # these don't have terminfo names to associate with
    enable_flash = CSI + '?5h'      # terminfo cap name, only single "flash"
    disable_flash = CSI + '?5l'

    # https://cirw.in/blog/bracketed-paste
    enable_bracketed_paste = CSI + '?2004h'
    disable_bracketed_paste = CSI + '?2004l'

    save_title = ('t', '22;%s')
    restore_title =  ('t', '23;%s')

    def __enter__(self):
        ''' Go full-screen and save title. '''
        self._stream.write(self.enable_alt_screen)
        self._stream.write(self.save_title(0))          # 0 = both icon, title
        self._stream.flush()
        return self

    def __exit__(self, type_, value, traceback):
        ''' Return to normal screen, restore title. '''
        self._stream.write(self.disable_alt_screen)
        self._stream.write(self.restore_title(0))       # 0 = both icon, title
        self._stream.flush()

    @contextmanager
    def bracketed_paste(self):
        ''' Context Manager that brackets-the-paste:

            - https://cirw.in/blog/bracketed-paste
            - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode

            .. code-block:: python

                with screen.bracketed_paste():
                    print('Hit me with your best shot…')
        '''
        stream = self._stream
        stream.write(self.enable_bracketed_paste)
        stream.flush()
        try:
            yield self
        finally:
            stream.write(self.disable_bracketed_paste)
            stream.flush()

    @contextmanager
    def fullscreen(self):
        ''' Context Manager that enters full-screen mode and restores normal
            mode on exit.

            .. code-block:: python

                with screen.fullscreen():
                    print('Hello, world!')
        '''
        stream = self._stream
        stream.write(self.enable_alt_screen)
        stream.write(self.save_title(0))            # 0 = both icon, title
        stream.flush()
        try:
            yield self
        finally:
            stream.write(self.disable_alt_screen)
            stream.write(self.restore_title(0))     # 0 = icon & title
            stream.flush()

    @contextmanager
    def hidden_cursor(self):
        ''' Context Manager that hides the cursor and restores it on exit.

            .. code-block:: python

                with screen.hidden_cursor():
                    print('Clandestine activity…')
        '''
        stream = self._stream
        stream.write(self.hide_cursor)
        stream.flush()
        try:
            yield self
        finally:
            stream.write(self.show_cursor)
            stream.flush()

    @contextmanager
    def location(self, x=None, y=None):
        ''' Temporarily move the cursor, perform work, and return to the
            previous location.

            ::

                with screen.location(40, 20):
                    print('Hello, world!')
        '''
        stream = self._stream
        stream.write(self.save_position)

        if x is not None and y is not None:
            stream.write(self.move_to(y, x))
        elif x is not None:
            stream.write(self.move_x(x))
        elif y is not None:
            stream.write(self.move_y(y))

        stream.flush()
        try:
            yield self
        finally:
            stream.write(self.restore_position)
            stream.flush()

    @contextmanager
    def rare_mode(self):
        ''' Context Manager that temporarily turns off echo and line-editing
            functionality; still recognizes Ctrl-C break, Ctrl-Z suspend, etc.
            Also known as "cbreak" mode.  POSIX only.

            See getpass for a usage example.

            .. code-block:: python

                with screen.rare_mode():
                    read_keys()
        '''
        import termios, tty  # defer

        with TermStack() as fd:
            termios.tcflush(fd, termios.TCIFLUSH)   # clear Input
            tty.setcbreak(fd, termios.TCSANOW)
            yield self                              # wait

    @contextmanager
    def raw_mode(self):
        ''' Context Manager that temporarily that temporarily sets terminal
            to raw mode. POSIX only.

            See utils.wait_key() if looking for a simple read-key function.

            .. code-block:: python

                with screen.raw_mode():
                    read_raw_keypresses()
        '''
        import termios, tty  # defer

        with TermStack() as fd:
            termios.tcflush(fd, termios.TCIFLUSH)   # clear Input
            tty.setraw(fd, termios.TCSANOW)
            yield self                              # wait


[docs]class Screen(_ContextMixin): ''' Convenience class for cursor and screen manipulation. Short names (terminfo capnames) are defined below, while the NAME_TO_TERMINFO_MAP mapping defines easier to remember verbose names. ScreenTermInfo defaults to standard (x, y) coordinate order and uses 0-based locations as does Python curses. Example:: from console.screen import sc >>> sc.move_to '\x1b[%s;%sH' >>> sc.move_to(3, 6) '\x1b[7;4H' # swapped and incremented to ANSI format. https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences ''' # The class attributes below will be wrapped with a _TemplateString on init cuu = 'A' cud = 'B' cuf = 'C' cub = 'D' cnl = 'E' # col 1 cpl = 'F' # col 1 hpa = 'G' # cha? cup = ('H', '%s;%s') # double trouble - move to pos vpa = 'd' # cva? hvp = ('f', '%s;%s') # "", appears to move the view rather than cursor cht = 'I' ed = 'J' # clear, 1 from start, 2 whole, 3 scrollback el = 'K' # line ich = '@' il = 'L' dl = 'M' dch = 'P' su = 'S' sd = 'T' ech = 'X' cbt = 'Z' # The following don't need parameter wrapping, all start with ESC rs1 = RIS sc = ESC + '7' # save cursor position rc = ESC + '8' # restore cnorm = CSI + '?25h' civis = CSI + '?25l' # alt screen smcup = CSI + '?1049h' rmcup = CSI + '?1049l' def __new__(cls, force=None, **kwargs): ''' Override new() to replace the class entirely on deactivation. Complies with terminal level detection, unless force is on: Arguments: force - Force sequences on. ''' if _ansi_capable or force: self = super().__new__(cls) else: from .disabled import empty_scr_bin # Makes new empties to deactivate completely: self = empty_scr_bin return self def __init__(self, stream=sys.stdout, swap=True, **kwargs): ''' Arguments: stream - For context managers swap - Coordinate order, i.e. Given in: True # Standard order, needs swapping. False # ANSI/Curses format ''' self._stream = stream # look for attributes to wrap in a _TemplateString: for name in dir(self): if not name.startswith('_'): value = getattr(self, name) if type(value) is str and not value.startswith(ESC): attr = _TemplateString(value) setattr(self, name, attr) # only cup and hvp need to worry about coordinates: elif type(value) is tuple: attr = _TemplateString(*value, swap=swap) setattr(self, name, attr) def __getattr__(self, attr): # when attr is *missing*, look in convenience map: cap_name = NAME_TO_TERMINFO_MAP.get(attr) if cap_name: value = getattr(self, cap_name) setattr(self, attr, value) # cache return value else: class_name = self.__class__.__name__ msg = f'{class_name!r} object has no attribute {attr!r}' raise AttributeError(msg)
class _TemplateString(str): ''' A template string that renders itself with given or default args. ''' _default = 1 def __new__(cls, endcode, arg='%s', swap=None): self = str.__new__(cls, CSI + arg + endcode) self.endcode = endcode # used in test self._swap = swap return self def __call__(self, *args): if len(args) == 2: if self._swap: # swap standard coordinate order backwards args = args[::-1] args = (args[0] + 1, args[1] + 1) # use 1-based coordinate origin return self % args def __str__(self): try: return self % self._default # default move 1 cell except TypeError: return self
[docs]class ScreenTermInfo(_ContextMixin): ''' Convenience class for cursor and screen manipulation. Short names (terminfo capnames) are available, while the NAME_TO_TERMINFO_MAP mapping defines several easier to remember verbose names. This implementation uses Terminfo instead of hard-coded ANSI sequences. ScreenTermInfo defaults to standard (x, y) coordinate order and uses 0-based locations as does Python curses. Use swap=False to reverse that. Example:: >>> sc.move_to '\x1b[%i%p1%d;%p2%dH' >>> sc.move_to(3, 6) '\x1b[7;4H' # swap and incremented to ANSI format. https://en.wikipedia.org/wiki/Terminfo ''' def __new__(cls, force=False, **kwargs): ''' Override new() to replace the class entirely on deactivation. Complies with terminfo details, unless force is on: Arguments: force - Force on. ''' if _ansi_capable or force: self = super().__new__(cls) else: from .disabled import empty_scr_bin # Makes new empties to deactivate completely: self = empty_scr_bin return self def __init__(self, stream=sys.stdout, swap=True, **kwargs): ''' Arguments: stream - For context managers swap - Coordinate order, i.e. Given in: True # Standard order, needs swapping. False # ANSI/Curses format ''' self._stream = stream self._swap = swap def __getattr__(self, attr): # when attribute is *missing* if using_terminfo: cap_name = NAME_TO_TERMINFO_MAP.get(attr, attr) # search terminfo value = None for get_cap in _ti_query_functions: value = get_cap(cap_name) if value is None: continue elif value in (-1, -2): value = None else: # found break if value is None: # didn't find it, return None or raise? #~ class_name = self.__class__.__name__ #~ raise AttributeError(f'{class_name!r} object has no attribute {cap_name!r}') pass else: # convert, cache, and return if b'%' in value: # tparm! value = _TemplateStringTermInfo(value, swap=self._swap) else: value = value.decode('ascii') setattr(self, cap_name, value) # short name if cap_name != attr: # long name also setattr(self, attr, value) return value else: raise RuntimeError('Terminfo is not available, use the standard ' 'Screen class instead.')
class _TemplateStringTermInfo(str): ''' A callable template string that renders itself with given args. ''' def __new__(cls, value, swap=None): self = str.__new__(cls, value.decode('ascii')) # as str self._byte_str = value # orig as bytes self._swap = swap return self def __call__(self, *args): ''' Run the tparm! ''' if len(args) == 2 and self._swap: args = args[::-1] # swap standard coordinate order backwards return _tparm(self._byte_str, *args).decode('ascii') # to string # Rather than define get_position() under Screen*, # we let detection pick the implementation, # as it is different under Windows. Then we attach it here. if _ansi_capable: Screen.get_position = ScreenTermInfo.get_position = ( staticmethod(_get_position) ) else: from .meta import defaults Screen.get_position = ScreenTermInfo.get_position = ( lambda *args, **kwargs: defaults.CURSOR_POS_FALLBACK ) # It's Automatic: https://youtu.be/y5ybok6ZGXk if using_terminfo: from . import _curses _ti_query_functions = ( _curses.tigetstr, _curses.tigetnum, _curses.tigetflag ) _tparm = _curses.tparm sc = ScreenTermInfo() else: sc = Screen()