Spaces:
No application file
No application file
# -*- 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 | |
# Alias py2 exception to py3 | |
if sys.version_info[:2] < (3, 3): | |
InterruptedError = select.error # pylint: disable=redefined-builtin | |
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 = { | |
'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 | |
def kind(self): | |
""" | |
Read-only property: Terminal kind determined on class initialization. | |
:rtype: str | |
""" | |
return self._kind | |
def does_styling(self): | |
""" | |
Read-only property: Whether this class instance may emit sequences. | |
:rtype: bool | |
""" | |
return self._does_styling | |
def is_a_tty(self): | |
""" | |
Read-only property: Whether :attr:`~.stream` is a terminal. | |
:rtype: bool | |
""" | |
return self._is_a_tty | |
def height(self): | |
""" | |
Read-only property: Height of the terminal (in number of lines). | |
:rtype: int | |
""" | |
return self._height_and_width().ws_row | |
def width(self): | |
""" | |
Read-only property: Width of the terminal (in number of columns). | |
:rtype: int | |
""" | |
return self._height_and_width().ws_col | |
def pixel_height(self): | |
""" | |
Read-only property: Height ofthe terminal (in pixels). | |
:rtype: int | |
""" | |
return self._height_and_width().ws_ypixel | |
def pixel_width(self): | |
""" | |
Read-only property: Width of terminal (in pixels). | |
:rtype: int | |
""" | |
return self._height_and_width().ws_xpixel | |
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) | |
def _query_response(self, query_str, response_re, timeout): | |
""" | |
Sends a query string to the terminal and waits for a response. | |
:arg str query_str: Query string written to output | |
:arg str response_re: Regular expression matching query response | |
:arg float timeout: Return after time elapsed in seconds | |
:return: re.match object for response_re or None if not found | |
:rtype: re.Match | |
""" | |
# 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 sequence, | |
self.stream.write(query_str) | |
self.stream.flush() | |
# Wait for response | |
match, data = _read_until(term=self, | |
pattern=response_re, | |
timeout=timeout) | |
# Exclude response from subsequent input | |
if match: | |
data = data[:match.start()] + data[match.end():] | |
# re-buffer keyboard data, if any | |
self.ungetch(data) | |
finally: | |
if ctx is not None: | |
ctx.__exit__(None, None, None) # pylint: disable=no-member | |
return match | |
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) | |
response_str = getattr(self, self.caps['cursor_report'].attribute) or u'\x1b[%i%d;%dR' | |
match = self._query_response( | |
self.u7 or u'\x1b[6n', self.caps['cursor_report'].re_compiled, timeout | |
) | |
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 | |
# 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 | |
def get_fgcolor(self, timeout=None): | |
""" | |
Return tuple (r, g, b) of foreground color. | |
:arg float timeout: Return after time elapsed in seconds with value ``(-1, -1, -1)`` | |
indicating that the remote end did not respond. | |
:rtype: tuple | |
:returns: foreground color as tuple in form of ``(r, g, b)``. When a timeout is specified, | |
always ensure the return value is checked for ``(-1, -1, -1)``. | |
The foreground color is determined by emitting an `OSC 10 color query | |
<https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands>`_. | |
""" | |
match = self._query_response( | |
u'\x1b]10;?\x07', | |
re.compile(u'\x1b]10;rgb:([0-9a-fA-F]+)/([0-9a-fA-F]+)/([0-9a-fA-F]+)\x07'), | |
timeout | |
) | |
return tuple(int(val, 16) for val in match.groups()) if match else (-1, -1, -1) | |
def get_bgcolor(self, timeout=None): | |
""" | |
Return tuple (r, g, b) of background color. | |
:arg float timeout: Return after time elapsed in seconds with value ``(-1, -1, -1)`` | |
indicating that the remote end did not respond. | |
:rtype: tuple | |
:returns: background color as tuple in form of ``(r, g, b)``. When a timeout is specified, | |
always ensure the return value is checked for ``(-1, -1, -1)``. | |
The background color is determined by emitting an `OSC 11 color query | |
<https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands>`_. | |
""" | |
match = self._query_response( | |
u'\x1b]11;?\x07', | |
re.compile(u'\x1b]11;rgb:([0-9a-fA-F]+)/([0-9a-fA-F]+)/([0-9a-fA-F]+)\x07'), | |
timeout | |
) | |
return tuple(int(val, 16) for val in match.groups()) if match else (-1, -1, -1) | |
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() | |
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) | |
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)) | |
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)) | |
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)) | |
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)) | |
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 self.does_styling: | |
return ParameterizingString(self._foreground_color, self.normal, 'color') | |
return NullCallableString() | |
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) | |
def on_color(self): | |
""" | |
A callable capability that sets the background color. | |
:rtype: ParameterizingString | |
""" | |
if self.does_styling: | |
return ParameterizingString(self._background_color, self.normal, 'on_color') | |
return NullCallableString() | |
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 | |
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)) | |
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 | |
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 | |
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() | |
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 | |
def color_distance_algorithm(self, value): | |
assert value in COLOR_DISTANCE_ALGORITHMS | |
self._color_distance_algorithm = value | |
self.__clear_color_capabilities() | |
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 | |
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 | |
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 | |
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 | |
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. | |