summaryrefslogtreecommitdiffstats
path: root/third_party/python/blessed/blessed/terminal.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--third_party/python/blessed/blessed/terminal.py1502
-rw-r--r--third_party/python/blessed/blessed/terminal.pyi106
2 files changed, 1608 insertions, 0 deletions
diff --git a/third_party/python/blessed/blessed/terminal.py b/third_party/python/blessed/blessed/terminal.py
new file mode 100644
index 0000000000..38bd2bb66b
--- /dev/null
+++ b/third_party/python/blessed/blessed/terminal.py
@@ -0,0 +1,1502 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=too-many-lines
+"""Module containing :class:`Terminal`, the primary API entry point."""
+# std imports
+import os
+import re
+import sys
+import time
+import codecs
+import locale
+import select
+import struct
+import platform
+import warnings
+import functools
+import contextlib
+import collections
+
+# local
+from .color import COLOR_DISTANCE_ALGORITHMS
+from .keyboard import (_time_left,
+ _read_until,
+ resolve_sequence,
+ get_keyboard_codes,
+ get_leading_prefixes,
+ get_keyboard_sequences)
+from .sequences import Termcap, Sequence, SequenceTextWrapper
+from .colorspace import RGB_256TABLE
+from .formatters import (COLORS,
+ COMPOUNDABLES,
+ FormattingString,
+ NullCallableString,
+ ParameterizingString,
+ FormattingOtherString,
+ split_compound,
+ resolve_attribute,
+ resolve_capability)
+from ._capabilities import CAPABILITY_DATABASE, CAPABILITIES_ADDITIVES, CAPABILITIES_RAW_MIXIN
+
+# isort: off
+
+try:
+ InterruptedError
+except NameError:
+ # alias py2 exception to py3
+ # pylint: disable=redefined-builtin
+ InterruptedError = select.error
+
+
+HAS_TTY = True
+if platform.system() == 'Windows':
+ IS_WINDOWS = True
+ import jinxed as curses # pylint: disable=import-error
+ from jinxed.win32 import get_console_input_encoding # pylint: disable=import-error
+else:
+ IS_WINDOWS = False
+ import curses
+
+ try:
+ import fcntl
+ import termios
+ import tty
+ except ImportError:
+ _TTY_METHODS = ('setraw', 'cbreak', 'kbhit', 'height', 'width')
+ _MSG_NOSUPPORT = (
+ "One or more of the modules: 'termios', 'fcntl', and 'tty' "
+ "are not found on your platform '{platform}'. "
+ "The following methods of Terminal are dummy/no-op "
+ "unless a deriving class overrides them: {tty_methods}."
+ .format(platform=platform.system(),
+ tty_methods=', '.join(_TTY_METHODS)))
+ warnings.warn(_MSG_NOSUPPORT)
+ HAS_TTY = False
+
+_CUR_TERM = None # See comments at end of file
+
+
+class Terminal(object):
+ """
+ An abstraction for color, style, positioning, and input in the terminal.
+
+ This keeps the endless calls to ``tigetstr()`` and ``tparm()`` out of your code, acts
+ intelligently when somebody pipes your output to a non-terminal, and abstracts over the
+ complexity of unbuffered keyboard input. It uses the terminfo database to remain portable across
+ terminal types.
+ """
+ # pylint: disable=too-many-instance-attributes,too-many-public-methods
+ # Too many public methods (28/20)
+ # Too many instance attributes (12/7)
+
+ #: Sugary names for commonly-used capabilities
+ _sugar = dict(
+ save='sc',
+ restore='rc',
+ clear_eol='el',
+ clear_bol='el1',
+ clear_eos='ed',
+ enter_fullscreen='smcup',
+ exit_fullscreen='rmcup',
+ move='cup',
+ move_yx='cup',
+ move_x='hpa',
+ move_y='vpa',
+ hide_cursor='civis',
+ normal_cursor='cnorm',
+ reset_colors='op',
+ normal='sgr0',
+ reverse='rev',
+ italic='sitm',
+ no_italic='ritm',
+ shadow='sshm',
+ no_shadow='rshm',
+ standout='smso',
+ no_standout='rmso',
+ subscript='ssubm',
+ no_subscript='rsubm',
+ superscript='ssupm',
+ no_superscript='rsupm',
+ underline='smul',
+ no_underline='rmul',
+ cursor_report='u6',
+ cursor_request='u7',
+ terminal_answerback='u8',
+ terminal_enquire='u9',
+ )
+
+ def __init__(self, kind=None, stream=None, force_styling=False):
+ """
+ Initialize the terminal.
+
+ :arg str kind: A terminal string as taken by :func:`curses.setupterm`.
+ Defaults to the value of the ``TERM`` environment variable.
+
+ .. note:: Terminals withing a single process must share a common
+ ``kind``. See :obj:`_CUR_TERM`.
+
+ :arg file stream: A file-like object representing the Terminal output.
+ Defaults to the original value of :obj:`sys.__stdout__`, like
+ :func:`curses.initscr` does.
+
+ If ``stream`` is not a tty, empty Unicode strings are returned for
+ all capability values, so things like piping your program output to
+ a pipe or file does not emit terminal sequences.
+
+ :arg bool force_styling: Whether to force the emission of capabilities
+ even if :obj:`sys.__stdout__` does not seem to be connected to a
+ terminal. If you want to force styling to not happen, use
+ ``force_styling=None``.
+
+ This comes in handy if users are trying to pipe your output through
+ something like ``less -r`` or build systems which support decoding
+ of terminal sequences.
+ """
+ # pylint: disable=global-statement,too-many-branches
+ global _CUR_TERM
+ self.errors = ['parameters: kind=%r, stream=%r, force_styling=%r' %
+ (kind, stream, force_styling)]
+ self._normal = None # cache normal attr, preventing recursive lookups
+ # we assume our input stream to be line-buffered until either the
+ # cbreak of raw context manager methods are entered with an attached tty.
+ self._line_buffered = True
+
+ self._stream = stream
+ self._keyboard_fd = None
+ self._init_descriptor = None
+ self._is_a_tty = False
+ self.__init__streams()
+
+ if IS_WINDOWS and self._init_descriptor is not None:
+ self._kind = kind or curses.get_term(self._init_descriptor)
+ else:
+ self._kind = kind or os.environ.get('TERM', 'dumb') or 'dumb'
+
+ self._does_styling = False
+ if force_styling is None and self.is_a_tty:
+ self.errors.append('force_styling is None')
+ elif force_styling or self.is_a_tty:
+ self._does_styling = True
+
+ if self.does_styling:
+ # Initialize curses (call setupterm), so things like tigetstr() work.
+ try:
+ curses.setupterm(self._kind, self._init_descriptor)
+ except curses.error as err:
+ msg = 'Failed to setupterm(kind={0!r}): {1}'.format(self._kind, err)
+ warnings.warn(msg)
+ self.errors.append(msg)
+ self._kind = None
+ self._does_styling = False
+ else:
+ if _CUR_TERM is None or self._kind == _CUR_TERM:
+ _CUR_TERM = self._kind
+ else:
+ # termcap 'kind' is immutable in a python process! Once
+ # initialized by setupterm, it is unsupported by the
+ # 'curses' module to change the terminal type again. If you
+ # are a downstream developer and you need this
+ # functionality, consider sub-processing, instead.
+ warnings.warn(
+ 'A terminal of kind "%s" has been requested; due to an'
+ ' internal python curses bug, terminal capabilities'
+ ' for a terminal of kind "%s" will continue to be'
+ ' returned for the remainder of this process.' % (
+ self._kind, _CUR_TERM,))
+
+ self.__init__color_capabilities()
+ self.__init__capabilities()
+ self.__init__keycodes()
+
+ def __init__streams(self):
+ # pylint: disable=too-complex,too-many-branches
+ # Agree to disagree !
+ stream_fd = None
+
+ # Default stream is stdout
+ if self._stream is None:
+ self._stream = sys.__stdout__
+
+ if not hasattr(self._stream, 'fileno'):
+ self.errors.append('stream has no fileno method')
+ elif not callable(self._stream.fileno):
+ self.errors.append('stream.fileno is not callable')
+ else:
+ try:
+ stream_fd = self._stream.fileno()
+ except ValueError as err:
+ # The stream is not a file, such as the case of StringIO, or, when it has been
+ # "detached", such as might be the case of stdout in some test scenarios.
+ self.errors.append('Unable to determine output stream file descriptor: %s' % err)
+ else:
+ self._is_a_tty = os.isatty(stream_fd)
+ if not self._is_a_tty:
+ self.errors.append('stream not a TTY')
+
+ # Keyboard valid as stdin only when output stream is stdout or stderr and is a tty.
+ if self._stream in (sys.__stdout__, sys.__stderr__):
+ try:
+ self._keyboard_fd = sys.__stdin__.fileno()
+ except (AttributeError, ValueError) as err:
+ self.errors.append('Unable to determine input stream file descriptor: %s' % err)
+ else:
+ # _keyboard_fd only non-None if both stdin and stdout is a tty.
+ if not self.is_a_tty:
+ self.errors.append('Output stream is not a TTY')
+ self._keyboard_fd = None
+ elif not os.isatty(self._keyboard_fd):
+ self.errors.append('Input stream is not a TTY')
+ self._keyboard_fd = None
+ else:
+ self.errors.append('Output stream is not a default stream')
+
+ # The descriptor to direct terminal initialization sequences to.
+ self._init_descriptor = stream_fd
+ if stream_fd is None:
+ try:
+ self._init_descriptor = sys.__stdout__.fileno()
+ except ValueError as err:
+ self.errors.append('Unable to determine __stdout__ file descriptor: %s' % err)
+
+ def __init__color_capabilities(self):
+ self._color_distance_algorithm = 'cie2000'
+ if not self.does_styling:
+ self.number_of_colors = 0
+ elif IS_WINDOWS or os.environ.get('COLORTERM') in ('truecolor', '24bit'):
+ self.number_of_colors = 1 << 24
+ else:
+ self.number_of_colors = max(0, curses.tigetnum('colors') or -1)
+
+ def __clear_color_capabilities(self):
+ for cached_color_cap in set(dir(self)) & COLORS:
+ delattr(self, cached_color_cap)
+
+ def __init__capabilities(self):
+ # important that we lay these in their ordered direction, so that our
+ # preferred, 'color' over 'set_a_attributes1', for example.
+ self.caps = collections.OrderedDict()
+
+ # some static injected patterns, esp. without named attribute access.
+ for name, (attribute, pattern) in CAPABILITIES_ADDITIVES.items():
+ self.caps[name] = Termcap(name, pattern, attribute)
+
+ for name, (attribute, kwds) in CAPABILITY_DATABASE.items():
+ if self.does_styling:
+ # attempt dynamic lookup
+ cap = getattr(self, attribute)
+ if cap:
+ self.caps[name] = Termcap.build(
+ name, cap, attribute, **kwds)
+ continue
+
+ # fall-back
+ pattern = CAPABILITIES_RAW_MIXIN.get(name)
+ if pattern:
+ self.caps[name] = Termcap(name, pattern, attribute)
+
+ # make a compiled named regular expression table
+ self.caps_compiled = re.compile(
+ '|'.join(cap.pattern for name, cap in self.caps.items()))
+
+ # for tokenizer, the '.lastgroup' is the primary lookup key for
+ # 'self.caps', unless 'MISMATCH'; then it is an unmatched character.
+ self._caps_compiled_any = re.compile('|'.join(
+ cap.named_pattern for name, cap in self.caps.items()
+ ) + '|(?P<MISMATCH>.)')
+ self._caps_unnamed_any = re.compile('|'.join(
+ '({0})'.format(cap.pattern) for name, cap in self.caps.items()
+ ) + '|(.)')
+
+ def __init__keycodes(self):
+ # Initialize keyboard data determined by capability.
+ # Build database of int code <=> KEY_NAME.
+ self._keycodes = get_keyboard_codes()
+
+ # Store attributes as: self.KEY_NAME = code.
+ for key_code, key_name in self._keycodes.items():
+ setattr(self, key_name, key_code)
+
+ # Build database of sequence <=> KEY_NAME.
+ self._keymap = get_keyboard_sequences(self)
+
+ # build set of prefixes of sequences
+ self._keymap_prefixes = get_leading_prefixes(self._keymap)
+
+ # keyboard stream buffer
+ self._keyboard_buf = collections.deque()
+
+ if self._keyboard_fd is not None:
+ # set input encoding and initialize incremental decoder
+
+ if IS_WINDOWS:
+ self._encoding = get_console_input_encoding() \
+ or locale.getpreferredencoding() or 'UTF-8'
+ else:
+ self._encoding = locale.getpreferredencoding() or 'UTF-8'
+
+ try:
+ self._keyboard_decoder = codecs.getincrementaldecoder(self._encoding)()
+ except LookupError as err:
+ # encoding is illegal or unsupported, use 'UTF-8'
+ warnings.warn('LookupError: {0}, defaulting to UTF-8 for keyboard.'.format(err))
+ self._encoding = 'UTF-8'
+ self._keyboard_decoder = codecs.getincrementaldecoder(self._encoding)()
+
+ def __getattr__(self, attr):
+ r"""
+ Return a terminal capability as Unicode string.
+
+ For example, ``term.bold`` is a unicode string that may be prepended
+ to text to set the video attribute for bold, which should also be
+ terminated with the pairing :attr:`normal`. This capability
+ returns a callable, so you can use ``term.bold("hi")`` which
+ results in the joining of ``(term.bold, "hi", term.normal)``.
+
+ Compound formatters may also be used. For example::
+
+ >>> term.bold_blink_red_on_green("merry x-mas!")
+
+ For a parameterized capability such as ``move`` (or ``cup``), pass the
+ parameters as positional arguments::
+
+ >>> term.move(line, column)
+
+ See the manual page `terminfo(5)
+ <https://invisible-island.net/ncurses/man/terminfo.5.html>`_ for a
+ complete list of capabilities and their arguments.
+ """
+ if not self._does_styling:
+ return NullCallableString()
+ # Fetch the missing 'attribute' into some kind of curses-resolved
+ # capability, and cache by attaching to this Terminal class instance.
+ #
+ # Note that this will prevent future calls to __getattr__(), but
+ # that's precisely the idea of the cache!
+ val = resolve_attribute(self, attr)
+ setattr(self, attr, val)
+ return val
+
+ @property
+ def kind(self):
+ """
+ Read-only property: Terminal kind determined on class initialization.
+
+ :rtype: str
+ """
+ return self._kind
+
+ @property
+ def does_styling(self):
+ """
+ Read-only property: Whether this class instance may emit sequences.
+
+ :rtype: bool
+ """
+ return self._does_styling
+
+ @property
+ def is_a_tty(self):
+ """
+ Read-only property: Whether :attr:`~.stream` is a terminal.
+
+ :rtype: bool
+ """
+ return self._is_a_tty
+
+ @property
+ def height(self):
+ """
+ Read-only property: Height of the terminal (in number of lines).
+
+ :rtype: int
+ """
+ return self._height_and_width().ws_row
+
+ @property
+ def width(self):
+ """
+ Read-only property: Width of the terminal (in number of columns).
+
+ :rtype: int
+ """
+ return self._height_and_width().ws_col
+
+ @property
+ def pixel_height(self):
+ """
+ Read-only property: Height ofthe terminal (in pixels).
+
+ :rtype: int
+ """
+ return self._height_and_width().ws_ypixel
+
+ @property
+ def pixel_width(self):
+ """
+ Read-only property: Width of terminal (in pixels).
+
+ :rtype: int
+ """
+ return self._height_and_width().ws_xpixel
+
+ @staticmethod
+ def _winsize(fd):
+ """
+ Return named tuple describing size of the terminal by ``fd``.
+
+ If the given platform does not have modules :mod:`termios`,
+ :mod:`fcntl`, or :mod:`tty`, window size of 80 columns by 25
+ rows is always returned.
+
+ :arg int fd: file descriptor queries for its window size.
+ :raises IOError: the file descriptor ``fd`` is not a terminal.
+ :rtype: WINSZ
+ :returns: named tuple describing size of the terminal
+
+ WINSZ is a :class:`collections.namedtuple` instance, whose structure
+ directly maps to the return value of the :const:`termios.TIOCGWINSZ`
+ ioctl return value. The return parameters are:
+
+ - ``ws_row``: width of terminal by its number of character cells.
+ - ``ws_col``: height of terminal by its number of character cells.
+ - ``ws_xpixel``: width of terminal by pixels (not accurate).
+ - ``ws_ypixel``: height of terminal by pixels (not accurate).
+ """
+ if HAS_TTY:
+ # pylint: disable=protected-access
+ data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF)
+ return WINSZ(*struct.unpack(WINSZ._FMT, data))
+ return WINSZ(ws_row=25, ws_col=80, ws_xpixel=0, ws_ypixel=0)
+
+ def _height_and_width(self):
+ """
+ Return a tuple of (terminal height, terminal width).
+
+ If :attr:`stream` or :obj:`sys.__stdout__` is not a tty or does not
+ support :func:`fcntl.ioctl` of :const:`termios.TIOCGWINSZ`, a window
+ size of 80 columns by 25 rows is returned for any values not
+ represented by environment variables ``LINES`` and ``COLUMNS``, which
+ is the default text mode of IBM PC compatibles.
+
+ :rtype: WINSZ
+ :returns: Named tuple specifying the terminal size
+
+ WINSZ is a :class:`collections.namedtuple` instance, whose structure
+ directly maps to the return value of the :const:`termios.TIOCGWINSZ`
+ ioctl return value. The return parameters are:
+
+ - ``ws_row``: height of terminal by its number of cell rows.
+ - ``ws_col``: width of terminal by its number of cell columns.
+ - ``ws_xpixel``: width of terminal by pixels (not accurate).
+ - ``ws_ypixel``: height of terminal by pixels (not accurate).
+
+ .. note:: the peculiar (height, width, width, height) order, which
+ matches the return order of TIOCGWINSZ!
+ """
+ for fd in (self._init_descriptor, sys.__stdout__):
+ try:
+ if fd is not None:
+ return self._winsize(fd)
+ except (IOError, OSError, ValueError, TypeError): # pylint: disable=overlapping-except
+ pass
+
+ return WINSZ(ws_row=int(os.getenv('LINES', '25')),
+ ws_col=int(os.getenv('COLUMNS', '80')),
+ ws_xpixel=None,
+ ws_ypixel=None)
+
+ @contextlib.contextmanager
+ def location(self, x=None, y=None):
+ """
+ Context manager for temporarily moving the cursor.
+
+ :arg int x: horizontal position, from left, *0*, to right edge of screen, *self.width - 1*.
+ :arg int y: vertical position, from top, *0*, to bottom of screen, *self.height - 1*.
+ :return: a context manager.
+ :rtype: Iterator
+
+ Move the cursor to a certain position on entry, do any kind of I/O, and upon exit
+ let you print stuff there, then return the cursor to its original position:
+
+
+ .. code-block:: python
+
+ term = Terminal()
+ with term.location(y=0, x=0):
+ for row_num in range(term.height-1):
+ print('Row #{row_num}')
+ print(term.clear_eol + 'Back to original location.')
+
+ Specify ``x`` to move to a certain column, ``y`` to move to a certain
+ row, both, or neither. If you specify neither, only the saving and
+ restoration of cursor position will happen. This can be useful if you
+ simply want to restore your place after doing some manual cursor
+ movement.
+
+ Calls cannot be nested: only one should be entered at a time.
+
+ .. note:: The argument order *(x, y)* differs from the return value order *(y, x)*
+ of :meth:`get_location`, or argument order *(y, x)* of :meth:`move`. This is
+ for API Compaibility with the blessings library, sorry for the trouble!
+ """
+ # pylint: disable=invalid-name
+ # Invalid argument name "x"
+
+ # Save position and move to the requested column, row, or both:
+ self.stream.write(self.save)
+ if x is not None and y is not None:
+ self.stream.write(self.move(y, x))
+ elif x is not None:
+ self.stream.write(self.move_x(x))
+ elif y is not None:
+ self.stream.write(self.move_y(y))
+ try:
+ self.stream.flush()
+ yield
+ finally:
+ # Restore original cursor position:
+ self.stream.write(self.restore)
+ self.stream.flush()
+
+ def get_location(self, timeout=None):
+ r"""
+ Return tuple (row, column) of cursor position.
+
+ :arg float timeout: Return after time elapsed in seconds with value ``(-1, -1)`` indicating
+ that the remote end did not respond.
+ :rtype: tuple
+ :returns: cursor position as tuple in form of ``(y, x)``. When a timeout is specified,
+ always ensure the return value is checked for ``(-1, -1)``.
+
+ The location of the cursor is determined by emitting the ``u7`` terminal capability, or
+ VT100 `Query Cursor Position
+ <https://www2.ccs.neu.edu/research/gpc/VonaUtils/vona/terminal/vtansi.htm#status>`_
+ when such capability is undefined, which elicits a response from a reply string described by
+ capability ``u6``, or again VT100's definition of ``\x1b[%i%d;%dR`` when undefined.
+
+ The ``(y, x)`` return value matches the parameter order of the :meth:`move_xy` capability.
+ The following sequence should cause the cursor to not move at all::
+
+ >>> term = Terminal()
+ >>> term.move_yx(*term.get_location()))
+
+ And the following should assert True with a terminal:
+
+ >>> term = Terminal()
+ >>> given_y, given_x = 10, 20
+ >>> with term.location(y=given_y, x=given_x):
+ ... result_y, result_x = term.get_location()
+ ...
+ >>> assert given_x == result_x, (given_x, result_x)
+ >>> assert given_y == result_y, (given_y, result_y)
+
+ """
+ # Local lines attached by termios and remote login protocols such as
+ # ssh and telnet both provide a means to determine the window
+ # dimensions of a connected client, but **no means to determine the
+ # location of the cursor**.
+ #
+ # from https://invisible-island.net/ncurses/terminfo.src.html,
+ #
+ # > The System V Release 4 and XPG4 terminfo format defines ten string
+ # > capabilities for use by applications, <u0>...<u9>. In this file,
+ # > we use certain of these capabilities to describe functions which
+ # > are not covered by terminfo. The mapping is as follows:
+ # >
+ # > u9 terminal enquire string (equiv. to ANSI/ECMA-48 DA)
+ # > u8 terminal answerback description
+ # > u7 cursor position request (equiv. to VT100/ANSI/ECMA-48 DSR 6)
+ # > u6 cursor position report (equiv. to ANSI/ECMA-48 CPR)
+ query_str = self.u7 or u'\x1b[6n'
+ response_str = getattr(self, self.caps['cursor_report'].attribute) or u'\x1b[%i%d;%dR'
+
+ # determine response format as a regular expression
+ response_re = self.caps['cursor_report'].re_compiled
+
+ # Avoid changing user's desired raw or cbreak mode if already entered,
+ # by entering cbreak mode ourselves. This is necessary to receive user
+ # input without awaiting a human to press the return key. This mode
+ # also disables echo, which we should also hide, as our input is an
+ # sequence that is not meaningful for display as an output sequence.
+
+ ctx = None
+ try:
+ if self._line_buffered:
+ ctx = self.cbreak()
+ ctx.__enter__() # pylint: disable=no-member
+
+ # emit the 'query cursor position' sequence,
+ self.stream.write(query_str)
+ self.stream.flush()
+
+ # expect a response,
+ match, data = _read_until(term=self,
+ pattern=response_re,
+ timeout=timeout)
+
+ # ensure response sequence is excluded from subsequent input,
+ if match:
+ data = (data[:match.start()] + data[match.end():])
+
+ # re-buffer keyboard data, if any
+ self.ungetch(data)
+
+ if match:
+ # return matching sequence response, the cursor location.
+ row, col = (int(val) for val in match.groups())
+
+ # Per https://invisible-island.net/ncurses/terminfo.src.html
+ # The cursor position report (<u6>) string must contain two
+ # scanf(3)-style %d format elements. The first of these must
+ # correspond to the Y coordinate and the second to the %d.
+ # If the string contains the sequence %i, it is taken as an
+ # instruction to decrement each value after reading it (this is
+ # the inverse sense from the cup string).
+ if u'%i' in response_str:
+ row -= 1
+ col -= 1
+ return row, col
+
+ finally:
+ if ctx is not None:
+ ctx.__exit__(None, None, None) # pylint: disable=no-member
+
+ # We chose to return an illegal value rather than an exception,
+ # favoring that users author function filters, such as max(0, y),
+ # rather than crowbarring such logic into an exception handler.
+ return -1, -1
+
+ @contextlib.contextmanager
+ def fullscreen(self):
+ """
+ Context manager that switches to secondary screen, restoring on exit.
+
+ Under the hood, this switches between the primary screen buffer and
+ the secondary one. The primary one is saved on entry and restored on
+ exit. Likewise, the secondary contents are also stable and are
+ faithfully restored on the next entry::
+
+ with term.fullscreen():
+ main()
+
+ .. note:: There is only one primary and one secondary screen buffer.
+ :meth:`fullscreen` calls cannot be nested, only one should be
+ entered at a time.
+ """
+ self.stream.write(self.enter_fullscreen)
+ self.stream.flush()
+ try:
+ yield
+ finally:
+ self.stream.write(self.exit_fullscreen)
+ self.stream.flush()
+
+ @contextlib.contextmanager
+ def hidden_cursor(self):
+ """
+ Context manager that hides the cursor, setting visibility on exit.
+
+ with term.hidden_cursor():
+ main()
+
+ .. note:: :meth:`hidden_cursor` calls cannot be nested: only one
+ should be entered at a time.
+ """
+ self.stream.write(self.hide_cursor)
+ self.stream.flush()
+ try:
+ yield
+ finally:
+ self.stream.write(self.normal_cursor)
+ self.stream.flush()
+
+ def move_xy(self, x, y):
+ """
+ A callable string that moves the cursor to the given ``(x, y)`` screen coordinates.
+
+ :arg int x: horizontal position, from left, *0*, to right edge of screen, *self.width - 1*.
+ :arg int y: vertical position, from top, *0*, to bottom of screen, *self.height - 1*.
+ :rtype: ParameterizingString
+ :returns: Callable string that moves the cursor to the given coordinates
+ """
+ # this is just a convenience alias to the built-in, but hidden 'move'
+ # attribute -- we encourage folks to use only (x, y) positional
+ # arguments, or, if they must use (y, x), then use the 'move_yx'
+ # alias.
+ return self.move(y, x)
+
+ def move_yx(self, y, x):
+ """
+ A callable string that moves the cursor to the given ``(y, x)`` screen coordinates.
+
+ :arg int y: vertical position, from top, *0*, to bottom of screen, *self.height - 1*.
+ :arg int x: horizontal position, from left, *0*, to right edge of screen, *self.width - 1*.
+ :rtype: ParameterizingString
+ :returns: Callable string that moves the cursor to the given coordinates
+ """
+ return self.move(y, x)
+
+ @property
+ def move_left(self):
+ """Move cursor 1 cells to the left, or callable string for n>1 cells."""
+ return FormattingOtherString(self.cub1, ParameterizingString(self.cub))
+
+ @property
+ def move_right(self):
+ """Move cursor 1 or more cells to the right, or callable string for n>1 cells."""
+ return FormattingOtherString(self.cuf1, ParameterizingString(self.cuf))
+
+ @property
+ def move_up(self):
+ """Move cursor 1 or more cells upwards, or callable string for n>1 cells."""
+ return FormattingOtherString(self.cuu1, ParameterizingString(self.cuu))
+
+ @property
+ def move_down(self):
+ """Move cursor 1 or more cells downwards, or callable string for n>1 cells."""
+ return FormattingOtherString(self.cud1, ParameterizingString(self.cud))
+
+ @property
+ def color(self):
+ """
+ A callable string that sets the foreground color.
+
+ :rtype: ParameterizingString
+
+ The capability is unparameterized until called and passed a number, at which point it
+ returns another string which represents a specific color change. This second string can
+ further be called to color a piece of text and set everything back to normal afterward.
+
+ This should not be used directly, but rather a specific color by name or
+ :meth:`~.Terminal.color_rgb` value.
+ """
+ if not self.does_styling:
+ return NullCallableString()
+ return ParameterizingString(self._foreground_color,
+ self.normal, 'color')
+
+ def color_rgb(self, red, green, blue):
+ """
+ Provides callable formatting string to set foreground color to the specified RGB color.
+
+ :arg int red: RGB value of Red.
+ :arg int green: RGB value of Green.
+ :arg int blue: RGB value of Blue.
+ :rtype: FormattingString
+ :returns: Callable string that sets the foreground color
+
+ If the terminal does not support RGB color, the nearest supported
+ color will be determined using :py:attr:`color_distance_algorithm`.
+ """
+ if self.number_of_colors == 1 << 24:
+ # "truecolor" 24-bit
+ fmt_attr = u'\x1b[38;2;{0};{1};{2}m'.format(red, green, blue)
+ return FormattingString(fmt_attr, self.normal)
+
+ # color by approximation to 256 or 16-color terminals
+ color_idx = self.rgb_downconvert(red, green, blue)
+ return FormattingString(self._foreground_color(color_idx), self.normal)
+
+ @property
+ def on_color(self):
+ """
+ A callable capability that sets the background color.
+
+ :rtype: ParameterizingString
+ """
+ if not self.does_styling:
+ return NullCallableString()
+ return ParameterizingString(self._background_color,
+ self.normal, 'on_color')
+
+ def on_color_rgb(self, red, green, blue):
+ """
+ Provides callable formatting string to set background color to the specified RGB color.
+
+ :arg int red: RGB value of Red.
+ :arg int green: RGB value of Green.
+ :arg int blue: RGB value of Blue.
+ :rtype: FormattingString
+ :returns: Callable string that sets the foreground color
+
+ If the terminal does not support RGB color, the nearest supported
+ color will be determined using :py:attr:`color_distance_algorithm`.
+ """
+ if self.number_of_colors == 1 << 24:
+ fmt_attr = u'\x1b[48;2;{0};{1};{2}m'.format(red, green, blue)
+ return FormattingString(fmt_attr, self.normal)
+
+ color_idx = self.rgb_downconvert(red, green, blue)
+ return FormattingString(self._background_color(color_idx), self.normal)
+
+ def formatter(self, value):
+ """
+ Provides callable formatting string to set color and other text formatting options.
+
+ :arg str value: Sugary, ordinary, or compound formatted terminal capability,
+ such as "red_on_white", "normal", "red", or "bold_on_black".
+ :rtype: :class:`FormattingString` or :class:`NullCallableString`
+ :returns: Callable string that sets color and other text formatting options
+
+ Calling ``term.formatter('bold_on_red')`` is equivalent to ``term.bold_on_red``, but a
+ string that is not a valid text formatter will return a :class:`NullCallableString`.
+ This is intended to allow validation of text formatters without the possibility of
+ inadvertently returning another terminal capability.
+ """
+ formatters = split_compound(value)
+ if all((fmt in COLORS or fmt in COMPOUNDABLES) for fmt in formatters):
+ return getattr(self, value)
+
+ return NullCallableString()
+
+ def rgb_downconvert(self, red, green, blue):
+ """
+ Translate an RGB color to a color code of the terminal's color depth.
+
+ :arg int red: RGB value of Red (0-255).
+ :arg int green: RGB value of Green (0-255).
+ :arg int blue: RGB value of Blue (0-255).
+ :rtype: int
+ :returns: Color code of downconverted RGB color
+ """
+ # Though pre-computing all 1 << 24 options is memory-intensive, a pre-computed
+ # "k-d tree" of 256 (x,y,z) vectors of a colorspace in 3 dimensions, such as a
+ # cone of HSV, or simply 255x255x255 RGB square, any given rgb value is just a
+ # nearest-neighbor search of 256 points, which k-d should be much faster by
+ # sub-dividing / culling search points, rather than our "search all 256 points
+ # always" approach.
+ fn_distance = COLOR_DISTANCE_ALGORITHMS[self.color_distance_algorithm]
+ color_idx = 7
+ shortest_distance = None
+ for cmp_depth, cmp_rgb in enumerate(RGB_256TABLE):
+ cmp_distance = fn_distance(cmp_rgb, (red, green, blue))
+ if shortest_distance is None or cmp_distance < shortest_distance:
+ shortest_distance = cmp_distance
+ color_idx = cmp_depth
+ if cmp_depth >= self.number_of_colors:
+ break
+ return color_idx
+
+ @property
+ def normal(self):
+ """
+ A capability that resets all video attributes.
+
+ :rtype: str
+
+ ``normal`` is an alias for ``sgr0`` or ``exit_attribute_mode``. Any
+ styling attributes previously applied, such as foreground or
+ background colors, reverse video, or bold are reset to defaults.
+ """
+ if self._normal:
+ return self._normal
+ self._normal = resolve_capability(self, 'normal')
+ return self._normal
+
+ def link(self, url, text, url_id=''):
+ """
+ Display ``text`` that when touched or clicked, navigates to ``url``.
+
+ Optional ``url_id`` may be specified, so that non-adjacent cells can reference a single
+ target, all cells painted with the same "id" will highlight on hover, rather than any
+ individual one, as described in "Hovering and underlining the id parameter" of gist
+ https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda.
+
+ :param str url: Hyperlink URL.
+ :param str text: Clickable text.
+ :param str url_id: Optional 'id'.
+ :rtype: str
+ :returns: String of ``text`` as a hyperlink to ``url``.
+ """
+ assert len(url) < 2000, (len(url), url)
+ if url_id:
+ assert len(str(url_id)) < 250, (len(str(url_id)), url_id)
+ params = 'id={0}'.format(url_id)
+ else:
+ params = ''
+ if not self.does_styling:
+ return text
+ return ('\x1b]8;{0};{1}\x1b\\{2}'
+ '\x1b]8;;\x1b\\'.format(params, url, text))
+
+ @property
+ def stream(self):
+ """
+ Read-only property: stream the terminal outputs to.
+
+ This is a convenience attribute. It is used internally for implied
+ writes performed by context managers :meth:`~.hidden_cursor`,
+ :meth:`~.fullscreen`, :meth:`~.location`, and :meth:`~.keypad`.
+ """
+ return self._stream
+
+ @property
+ def number_of_colors(self):
+ """
+ Number of colors supported by terminal.
+
+ Common return values are 0, 8, 16, 256, or 1 << 24.
+
+ This may be used to test whether the terminal supports colors,
+ and at what depth, if that's a concern.
+
+ If this property is assigned a value of 88, the value 16 will be saved. This is due to the
+ the rarity of 88 color support and the inconsistency of behavior between implementations.
+
+ Assigning this property to a value other than 0, 4, 8, 16, 88, 256, or 1 << 24 will
+ raise an :py:exc:`AssertionError`.
+ """
+ return self._number_of_colors
+
+ @number_of_colors.setter
+ def number_of_colors(self, value):
+ assert value in (0, 4, 8, 16, 88, 256, 1 << 24)
+ # Because 88 colors is rare and we can't guarantee consistent behavior,
+ # when 88 colors is detected, it is treated as 16 colors
+ self._number_of_colors = 16 if value == 88 else value
+ self.__clear_color_capabilities()
+
+ @property
+ def color_distance_algorithm(self):
+ """
+ Color distance algorithm used by :meth:`rgb_downconvert`.
+
+ The slowest, but most accurate, 'cie2000', is default. Other available options are 'rgb',
+ 'rgb-weighted', 'cie76', and 'cie94'.
+ """
+ return self._color_distance_algorithm
+
+ @color_distance_algorithm.setter
+ def color_distance_algorithm(self, value):
+ assert value in COLOR_DISTANCE_ALGORITHMS
+ self._color_distance_algorithm = value
+ self.__clear_color_capabilities()
+
+ @property
+ def _foreground_color(self):
+ """
+ Convenience capability to support :attr:`~.on_color`.
+
+ Prefers returning sequence for capability ``setaf``, "Set foreground color to #1, using ANSI
+ escape". If the given terminal does not support such sequence, fallback to returning
+ attribute ``setf``, "Set foreground color #1".
+ """
+ return self.setaf or self.setf
+
+ @property
+ def _background_color(self):
+ """
+ Convenience capability to support :attr:`~.on_color`.
+
+ Prefers returning sequence for capability ``setab``, "Set background color to #1, using ANSI
+ escape". If the given terminal does not support such sequence, fallback to returning
+ attribute ``setb``, "Set background color #1".
+ """
+ return self.setab or self.setb
+
+ def ljust(self, text, width=None, fillchar=u' '):
+ """
+ Left-align ``text``, which may contain terminal sequences.
+
+ :arg str text: String to be aligned
+ :arg int width: Total width to fill with aligned text. If
+ unspecified, the whole width of the terminal is filled.
+ :arg str fillchar: String for padding the right of ``text``
+ :rtype: str
+ :returns: String of ``text``, left-aligned by ``width``.
+ """
+ # Left justification is different from left alignment, but we continue
+ # the vocabulary error of the str method for polymorphism.
+ if width is None:
+ width = self.width
+ return Sequence(text, self).ljust(width, fillchar)
+
+ def rjust(self, text, width=None, fillchar=u' '):
+ """
+ Right-align ``text``, which may contain terminal sequences.
+
+ :arg str text: String to be aligned
+ :arg int width: Total width to fill with aligned text. If
+ unspecified, the whole width of the terminal is used.
+ :arg str fillchar: String for padding the left of ``text``
+ :rtype: str
+ :returns: String of ``text``, right-aligned by ``width``.
+ """
+ if width is None:
+ width = self.width
+ return Sequence(text, self).rjust(width, fillchar)
+
+ def center(self, text, width=None, fillchar=u' '):
+ """
+ Center ``text``, which may contain terminal sequences.
+
+ :arg str text: String to be centered
+ :arg int width: Total width in which to center text. If
+ unspecified, the whole width of the terminal is used.
+ :arg str fillchar: String for padding the left and right of ``text``
+ :rtype: str
+ :returns: String of ``text``, centered by ``width``
+ """
+ if width is None:
+ width = self.width
+ return Sequence(text, self).center(width, fillchar)
+
+ def truncate(self, text, width=None):
+ r"""
+ Truncate ``text`` to maximum ``width`` printable characters, retaining terminal sequences.
+
+ :arg str text: Text to truncate
+ :arg int width: The maximum width to truncate it to
+ :rtype: str
+ :returns: ``text`` truncated to at most ``width`` printable characters
+
+ >>> term.truncate(u'xyz\x1b[0;3m', 2)
+ u'xy\x1b[0;3m'
+ """
+ if width is None:
+ width = self.width
+ return Sequence(text, self).truncate(width)
+
+ def length(self, text):
+ u"""
+ Return printable length of a string containing sequences.
+
+ :arg str text: String to measure. May contain terminal sequences.
+ :rtype: int
+ :returns: The number of terminal character cells the string will occupy
+ when printed
+
+ Wide characters that consume 2 character cells are supported:
+
+ >>> term = Terminal()
+ >>> term.length(term.clear + term.red(u'コンニチハ'))
+ 10
+
+ .. note:: Sequences such as 'clear', which is considered as a
+ "movement sequence" because it would move the cursor to
+ (y, x)(0, 0), are evaluated as a printable length of
+ *0*.
+ """
+ return Sequence(text, self).length()
+
+ def strip(self, text, chars=None):
+ r"""
+ Return ``text`` without sequences and leading or trailing whitespace.
+
+ :rtype: str
+ :returns: Text with leading and trailing whitespace removed
+
+ >>> term.strip(u' \x1b[0;3m xyz ')
+ u'xyz'
+ """
+ return Sequence(text, self).strip(chars)
+
+ def rstrip(self, text, chars=None):
+ r"""
+ Return ``text`` without terminal sequences or trailing whitespace.
+
+ :rtype: str
+ :returns: Text with terminal sequences and trailing whitespace removed
+
+ >>> term.rstrip(u' \x1b[0;3m xyz ')
+ u' xyz'
+ """
+ return Sequence(text, self).rstrip(chars)
+
+ def lstrip(self, text, chars=None):
+ r"""
+ Return ``text`` without terminal sequences or leading whitespace.
+
+ :rtype: str
+ :returns: Text with terminal sequences and leading whitespace removed
+
+ >>> term.lstrip(u' \x1b[0;3m xyz ')
+ u'xyz '
+ """
+ return Sequence(text, self).lstrip(chars)
+
+ def strip_seqs(self, text):
+ r"""
+ Return ``text`` stripped of only its terminal sequences.
+
+ :rtype: str
+ :returns: Text with terminal sequences removed
+
+ >>> term.strip_seqs(u'\x1b[0;3mxyz')
+ u'xyz'
+ >>> term.strip_seqs(term.cuf(5) + term.red(u'test'))
+ u' test'
+
+ .. note:: Non-destructive sequences that adjust horizontal distance
+ (such as ``\b`` or ``term.cuf(5)``) are replaced by destructive
+ space or erasing.
+ """
+ return Sequence(text, self).strip_seqs()
+
+ def split_seqs(self, text, maxsplit=0):
+ r"""
+ Return ``text`` split by individual character elements and sequences.
+
+ :arg str text: String containing sequences
+ :arg int maxsplit: When maxsplit is nonzero, at most maxsplit splits
+ occur, and the remainder of the string is returned as the final element
+ of the list (same meaning is argument for :func:`re.split`).
+ :rtype: list[str]
+ :returns: List of sequences and individual characters
+
+ >>> term.split_seqs(term.underline(u'xyz'))
+ ['\x1b[4m', 'x', 'y', 'z', '\x1b(B', '\x1b[m']
+
+ >>> term.split_seqs(term.underline(u'xyz'), 1)
+ ['\x1b[4m', r'xyz\x1b(B\x1b[m']
+ """
+ pattern = self._caps_unnamed_any
+ result = []
+ for idx, match in enumerate(re.finditer(pattern, text)):
+ result.append(match.group())
+ if maxsplit and idx == maxsplit:
+ remaining = text[match.end():]
+ if remaining:
+ result[-1] += remaining
+ break
+ return result
+
+ def wrap(self, text, width=None, **kwargs):
+ r"""
+ Text-wrap a string, returning a list of wrapped lines.
+
+ :arg str text: Unlike :func:`textwrap.wrap`, ``text`` may contain
+ terminal sequences, such as colors, bold, or underline. By
+ default, tabs in ``text`` are expanded by
+ :func:`string.expandtabs`.
+ :arg int width: Unlike :func:`textwrap.wrap`, ``width`` will
+ default to the width of the attached terminal.
+ :arg \**kwargs: See :py:class:`textwrap.TextWrapper`
+ :rtype: list
+ :returns: List of wrapped lines
+
+ See :class:`textwrap.TextWrapper` for keyword arguments that can
+ customize wrapping behaviour.
+ """
+ width = self.width if width is None else width
+ wrapper = SequenceTextWrapper(width=width, term=self, **kwargs)
+ lines = []
+ for line in text.splitlines():
+ lines.extend(iter(wrapper.wrap(line)) if line.strip() else (u'',))
+
+ return lines
+
+ def getch(self):
+ """
+ Read, decode, and return the next byte from the keyboard stream.
+
+ :rtype: unicode
+ :returns: a single unicode character, or ``u''`` if a multi-byte
+ sequence has not yet been fully received.
+
+ This method name and behavior mimics curses ``getch(void)``, and
+ it supports :meth:`inkey`, reading only one byte from
+ the keyboard string at a time. This method should always return
+ without blocking if called after :meth:`kbhit` has returned True.
+
+ Implementors of alternate input stream methods should override
+ this method.
+ """
+ assert self._keyboard_fd is not None
+ byte = os.read(self._keyboard_fd, 1)
+ return self._keyboard_decoder.decode(byte, final=False)
+
+ def ungetch(self, text):
+ """
+ Buffer input data to be discovered by next call to :meth:`~.inkey`.
+
+ :arg str text: String to be buffered as keyboard input.
+ """
+ self._keyboard_buf.extendleft(text)
+
+ def kbhit(self, timeout=None):
+ """
+ Return whether a keypress has been detected on the keyboard.
+
+ This method is used by :meth:`inkey` to determine if a byte may
+ be read using :meth:`getch` without blocking. The standard
+ implementation simply uses the :func:`select.select` call on stdin.
+
+ :arg float timeout: When ``timeout`` is 0, this call is
+ non-blocking, otherwise blocking indefinitely until keypress
+ is detected when None (default). When ``timeout`` is a
+ positive number, returns after ``timeout`` seconds have
+ elapsed (float).
+ :rtype: bool
+ :returns: True if a keypress is awaiting to be read on the keyboard
+ attached to this terminal. When input is not a terminal, False is
+ always returned.
+ """
+ stime = time.time()
+ ready_r = [None, ]
+ check_r = [self._keyboard_fd] if self._keyboard_fd is not None else []
+
+ while HAS_TTY:
+ try:
+ ready_r, _, _ = select.select(check_r, [], [], timeout)
+ except InterruptedError:
+ # Beginning with python3.5, IntrruptError is no longer thrown
+ # https://www.python.org/dev/peps/pep-0475/
+ #
+ # For previous versions of python, we take special care to
+ # retry select on InterruptedError exception, namely to handle
+ # a custom SIGWINCH handler. When installed, it would cause
+ # select() to be interrupted with errno 4 (EAGAIN).
+ #
+ # Just as in python3.5, it is ignored, and a new timeout value
+ # is derived from the previous unless timeout becomes negative.
+ # because the signal handler has blocked beyond timeout, then
+ # False is returned. Otherwise, when timeout is None, we
+ # continue to block indefinitely (default).
+ if timeout is not None:
+ # subtract time already elapsed,
+ timeout -= time.time() - stime
+ if timeout > 0:
+ continue
+ # no time remains after handling exception (rare)
+ ready_r = [] # pragma: no cover
+ break # pragma: no cover
+ else:
+ break
+
+ return False if self._keyboard_fd is None else check_r == ready_r
+
+ @contextlib.contextmanager
+ def cbreak(self):
+ """
+ Allow each keystroke to be read immediately after it is pressed.
+
+ This is a context manager for :func:`tty.setcbreak`.
+
+ This context manager activates 'rare' mode, the opposite of 'cooked'
+ mode: On entry, :func:`tty.setcbreak` mode is activated disabling
+ line-buffering of keyboard input and turning off automatic echo of
+ input as output.
+
+ .. note:: You must explicitly print any user input you would like
+ displayed. If you provide any kind of editing, you must handle
+ backspace and other line-editing control functions in this mode
+ as well!
+
+ **Normally**, characters received from the keyboard cannot be read
+ by Python until the *Return* key is pressed. Also known as *cooked* or
+ *canonical input* mode, it allows the tty driver to provide
+ line-editing before shuttling the input to your program and is the
+ (implicit) default terminal mode set by most unix shells before
+ executing programs.
+
+ Technically, this context manager sets the :mod:`termios` attributes
+ of the terminal attached to :obj:`sys.__stdin__`.
+
+ .. note:: :func:`tty.setcbreak` sets ``VMIN = 1`` and ``VTIME = 0``,
+ see http://www.unixwiz.net/techtips/termios-vmin-vtime.html
+ """
+ if HAS_TTY and self._keyboard_fd is not None:
+ # Save current terminal mode:
+ save_mode = termios.tcgetattr(self._keyboard_fd)
+ save_line_buffered = self._line_buffered
+ tty.setcbreak(self._keyboard_fd, termios.TCSANOW)
+ try:
+ self._line_buffered = False
+ yield
+ finally:
+ # Restore prior mode:
+ termios.tcsetattr(self._keyboard_fd,
+ termios.TCSAFLUSH,
+ save_mode)
+ self._line_buffered = save_line_buffered
+ else:
+ yield
+
+ @contextlib.contextmanager
+ def raw(self):
+ r"""
+ A context manager for :func:`tty.setraw`.
+
+ Although both :meth:`break` and :meth:`raw` modes allow each keystroke
+ to be read immediately after it is pressed, Raw mode disables
+ processing of input and output.
+
+ In cbreak mode, special input characters such as ``^C`` or ``^S`` are
+ interpreted by the terminal driver and excluded from the stdin stream.
+ In raw mode these values are receive by the :meth:`inkey` method.
+
+ Because output processing is not done, the newline ``'\n'`` is not
+ enough, you must also print carriage return to ensure that the cursor
+ is returned to the first column::
+
+ with term.raw():
+ print("printing in raw mode", end="\r\n")
+ """
+ if HAS_TTY and self._keyboard_fd is not None:
+ # Save current terminal mode:
+ save_mode = termios.tcgetattr(self._keyboard_fd)
+ save_line_buffered = self._line_buffered
+ tty.setraw(self._keyboard_fd, termios.TCSANOW)
+ try:
+ self._line_buffered = False
+ yield
+ finally:
+ # Restore prior mode:
+ termios.tcsetattr(self._keyboard_fd,
+ termios.TCSAFLUSH,
+ save_mode)
+ self._line_buffered = save_line_buffered
+ else:
+ yield
+
+ @contextlib.contextmanager
+ def keypad(self):
+ r"""
+ Context manager that enables directional keypad input.
+
+ On entrying, this puts the terminal into "keyboard_transmit" mode by
+ emitting the keypad_xmit (smkx) capability. On exit, it emits
+ keypad_local (rmkx).
+
+ On an IBM-PC keyboard with numeric keypad of terminal-type *xterm*,
+ with numlock off, the lower-left diagonal key transmits sequence
+ ``\\x1b[F``, translated to :class:`~.Terminal` attribute
+ ``KEY_END``.
+
+ However, upon entering :meth:`keypad`, ``\\x1b[OF`` is transmitted,
+ translating to ``KEY_LL`` (lower-left key), allowing you to determine
+ diagonal direction keys.
+ """
+ try:
+ self.stream.write(self.smkx)
+ self.stream.flush()
+ yield
+ finally:
+ self.stream.write(self.rmkx)
+ self.stream.flush()
+
+ def inkey(self, timeout=None, esc_delay=0.35):
+ """
+ Read and return the next keyboard event within given timeout.
+
+ Generally, this should be used inside the :meth:`raw` context manager.
+
+ :arg float timeout: Number of seconds to wait for a keystroke before
+ returning. When ``None`` (default), this method may block
+ indefinitely.
+ :arg float esc_delay: To distinguish between the keystroke of
+ ``KEY_ESCAPE``, and sequences beginning with escape, the parameter
+ ``esc_delay`` specifies the amount of time after receiving escape
+ (``chr(27)``) to seek for the completion of an application key
+ before returning a :class:`~.Keystroke` instance for
+ ``KEY_ESCAPE``.
+ :rtype: :class:`~.Keystroke`.
+ :returns: :class:`~.Keystroke`, which may be empty (``u''``) if
+ ``timeout`` is specified and keystroke is not received.
+
+ .. note:: When used without the context manager :meth:`cbreak`, or
+ :meth:`raw`, :obj:`sys.__stdin__` remains line-buffered, and this
+ function will block until the return key is pressed!
+
+ .. note:: On Windows, a 10 ms sleep is added to the key press detection loop to reduce CPU
+ load. Due to the behavior of :py:func:`time.sleep` on Windows, this will actually
+ result in a 15.6 ms delay when using the default `time resolution
+ <https://docs.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod>`_.
+ Decreasing the time resolution will reduce this to 10 ms, while increasing it, which
+ is rarely done, will have a perceptable impact on the behavior.
+ """
+ resolve = functools.partial(resolve_sequence,
+ mapper=self._keymap,
+ codes=self._keycodes)
+
+ stime = time.time()
+
+ # re-buffer previously received keystrokes,
+ ucs = u''
+ while self._keyboard_buf:
+ ucs += self._keyboard_buf.pop()
+
+ # receive all immediately available bytes
+ while self.kbhit(timeout=0):
+ ucs += self.getch()
+
+ # decode keystroke, if any
+ ks = resolve(text=ucs)
+
+ # so long as the most immediately received or buffered keystroke is
+ # incomplete, (which may be a multibyte encoding), block until until
+ # one is received.
+ while not ks and self.kbhit(timeout=_time_left(stime, timeout)):
+ ucs += self.getch()
+ ks = resolve(text=ucs)
+
+ # handle escape key (KEY_ESCAPE) vs. escape sequence (like those
+ # that begin with \x1b[ or \x1bO) up to esc_delay when
+ # received. This is not optimal, but causes least delay when
+ # "meta sends escape" is used, or when an unsupported sequence is
+ # sent.
+ #
+ # The statement, "ucs in self._keymap_prefixes" has an effect on
+ # keystrokes such as Alt + Z ("\x1b[z" with metaSendsEscape): because
+ # no known input sequences begin with such phrasing to allow it to be
+ # returned more quickly than esc_delay otherwise blocks for.
+ if ks.code == self.KEY_ESCAPE:
+ esctime = time.time()
+ while (ks.code == self.KEY_ESCAPE and
+ ucs in self._keymap_prefixes and
+ self.kbhit(timeout=_time_left(esctime, esc_delay))):
+ ucs += self.getch()
+ ks = resolve(text=ucs)
+
+ # buffer any remaining text received
+ self.ungetch(ucs[len(ks):])
+ return ks
+
+
+class WINSZ(collections.namedtuple('WINSZ', (
+ 'ws_row', 'ws_col', 'ws_xpixel', 'ws_ypixel'))):
+ """
+ Structure represents return value of :const:`termios.TIOCGWINSZ`.
+
+ .. py:attribute:: ws_row
+
+ rows, in characters
+
+ .. py:attribute:: ws_col
+
+ columns, in characters
+
+ .. py:attribute:: ws_xpixel
+
+ horizontal size, pixels
+
+ .. py:attribute:: ws_ypixel
+
+ vertical size, pixels
+ """
+ #: format of termios structure
+ _FMT = 'hhhh'
+ #: buffer of termios structure appropriate for ioctl argument
+ _BUF = '\x00' * struct.calcsize(_FMT)
+
+
+#: _CUR_TERM = None
+#: From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al)::
+#:
+#: "After the call to setupterm(), the global variable cur_term is set to
+#: point to the current structure of terminal capabilities. By calling
+#: setupterm() for each terminal, and saving and restoring cur_term, it
+#: is possible for a program to use two or more terminals at once."
+#:
+#: However, if you study Python's ``./Modules/_cursesmodule.c``, you'll find::
+#:
+#: if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) {
+#:
+#: Python - perhaps wrongly - will not allow for re-initialisation of new
+#: terminals through :func:`curses.setupterm`, so the value of cur_term cannot
+#: be changed once set: subsequent calls to :func:`curses.setupterm` have no
+#: effect.
+#:
+#: Therefore, the :attr:`Terminal.kind` of each :class:`Terminal` is
+#: essentially a singleton. This global variable reflects that, and a warning
+#: is emitted if somebody expects otherwise.
diff --git a/third_party/python/blessed/blessed/terminal.pyi b/third_party/python/blessed/blessed/terminal.pyi
new file mode 100644
index 0000000000..3d8eea4db7
--- /dev/null
+++ b/third_party/python/blessed/blessed/terminal.pyi
@@ -0,0 +1,106 @@
+# std imports
+from typing import IO, Any, List, Tuple, Union, Optional, OrderedDict, ContextManager
+
+# local
+from .keyboard import Keystroke
+from .sequences import Termcap
+from .formatters import (FormattingString,
+ NullCallableString,
+ ParameterizingString,
+ FormattingOtherString)
+
+HAS_TTY: bool
+
+class Terminal:
+ caps: OrderedDict[str, Termcap]
+ errors: List[str] = ...
+ def __init__(
+ self,
+ kind: Optional[str] = ...,
+ stream: Optional[IO[str]] = ...,
+ force_styling: bool = ...,
+ ) -> None: ...
+ def __getattr__(
+ self, attr: str
+ ) -> Union[NullCallableString, ParameterizingString, FormattingString]: ...
+ @property
+ def kind(self) -> str: ...
+ @property
+ def does_styling(self) -> bool: ...
+ @property
+ def is_a_tty(self) -> bool: ...
+ @property
+ def height(self) -> int: ...
+ @property
+ def width(self) -> int: ...
+ @property
+ def pixel_height(self) -> int: ...
+ @property
+ def pixel_width(self) -> int: ...
+ def location(
+ self, x: Optional[int] = ..., y: Optional[int] = ...
+ ) -> ContextManager[None]: ...
+ def get_location(self, timeout: Optional[float] = ...) -> Tuple[int, int]: ...
+ def fullscreen(self) -> ContextManager[None]: ...
+ def hidden_cursor(self) -> ContextManager[None]: ...
+ def move_xy(self, x: int, y: int) -> ParameterizingString: ...
+ def move_yx(self, y: int, x: int) -> ParameterizingString: ...
+ @property
+ def move_left(self) -> FormattingOtherString: ...
+ @property
+ def move_right(self) -> FormattingOtherString: ...
+ @property
+ def move_up(self) -> FormattingOtherString: ...
+ @property
+ def move_down(self) -> FormattingOtherString: ...
+ @property
+ def color(self) -> Union[NullCallableString, ParameterizingString]: ...
+ def color_rgb(self, red: int, green: int, blue: int) -> FormattingString: ...
+ @property
+ def on_color(self) -> Union[NullCallableString, ParameterizingString]: ...
+ def on_color_rgb(self, red: int, green: int, blue: int) -> FormattingString: ...
+ def formatter(self, value: str) -> Union[NullCallableString, FormattingString]: ...
+ def rgb_downconvert(self, red: int, green: int, blue: int) -> int: ...
+ @property
+ def normal(self) -> str: ...
+ def link(self, url: str, text: str, url_id: str = ...) -> str: ...
+ @property
+ def stream(self) -> IO[str]: ...
+ @property
+ def number_of_colors(self) -> int: ...
+ @number_of_colors.setter
+ def number_of_colors(self, value: int) -> None: ...
+ @property
+ def color_distance_algorithm(self) -> str: ...
+ @color_distance_algorithm.setter
+ def color_distance_algorithm(self, value: str) -> None: ...
+ def ljust(
+ self, text: str, width: Optional[int] = ..., fillchar: str = ...
+ ) -> str: ...
+ def rjust(
+ self, text: str, width: Optional[int] = ..., fillchar: str = ...
+ ) -> str: ...
+ def center(
+ self, text: str, width: Optional[int] = ..., fillchar: str = ...
+ ) -> str: ...
+ def truncate(self, text: str, width: Optional[int] = ...) -> str: ...
+ def length(self, text: str) -> int: ...
+ def strip(self, text: str, chars: Optional[str] = ...) -> str: ...
+ def rstrip(self, text: str, chars: Optional[str] = ...) -> str: ...
+ def lstrip(self, text: str, chars: Optional[str] = ...) -> str: ...
+ def strip_seqs(self, text: str) -> str: ...
+ def split_seqs(self, text: str, maxsplit: int) -> List[str]: ...
+ def wrap(
+ self, text: str, width: Optional[int] = ..., **kwargs: Any
+ ) -> List[str]: ...
+ def getch(self) -> str: ...
+ def ungetch(self, text: str) -> None: ...
+ def kbhit(self, timeout: Optional[float] = ...) -> bool: ...
+ def cbreak(self) -> ContextManager[None]: ...
+ def raw(self) -> ContextManager[None]: ...
+ def keypad(self) -> ContextManager[None]: ...
+ def inkey(
+ self, timeout: Optional[float] = ..., esc_delay: float = ...
+ ) -> Keystroke: ...
+
+class WINSZ: ...