generated from mwc/lab_retro
452 lines
17 KiB
Python
452 lines
17 KiB
Python
"""Sub-module providing 'keyboard awareness'."""
|
|
|
|
# std imports
|
|
import re
|
|
import time
|
|
import platform
|
|
from collections import OrderedDict
|
|
|
|
# 3rd party
|
|
import six
|
|
|
|
# isort: off
|
|
# curses
|
|
if platform.system() == 'Windows':
|
|
# pylint: disable=import-error
|
|
import jinxed as curses
|
|
from jinxed.has_key import _capability_names as capability_names
|
|
else:
|
|
import curses
|
|
from curses.has_key import _capability_names as capability_names
|
|
|
|
|
|
class Keystroke(six.text_type):
|
|
"""
|
|
A unicode-derived class for describing a single keystroke.
|
|
|
|
A class instance describes a single keystroke received on input,
|
|
which may contain multiple characters as a multibyte sequence,
|
|
which is indicated by properties :attr:`is_sequence` returning
|
|
``True``.
|
|
|
|
When the string is a known sequence, :attr:`code` matches terminal
|
|
class attributes for comparison, such as ``term.KEY_LEFT``.
|
|
|
|
The string-name of the sequence, such as ``u'KEY_LEFT'`` is accessed
|
|
by property :attr:`name`, and is used by the :meth:`__repr__` method
|
|
to display a human-readable form of the Keystroke this class
|
|
instance represents. It may otherwise by joined, split, or evaluated
|
|
just as as any other unicode string.
|
|
"""
|
|
|
|
def __new__(cls, ucs='', code=None, name=None):
|
|
"""Class constructor."""
|
|
new = six.text_type.__new__(cls, ucs)
|
|
new._name = name
|
|
new._code = code
|
|
return new
|
|
|
|
@property
|
|
def is_sequence(self):
|
|
"""Whether the value represents a multibyte sequence (bool)."""
|
|
return self._code is not None
|
|
|
|
def __repr__(self):
|
|
"""Docstring overwritten."""
|
|
return (six.text_type.__repr__(self) if self._name is None else
|
|
self._name)
|
|
__repr__.__doc__ = six.text_type.__doc__
|
|
|
|
@property
|
|
def name(self):
|
|
"""String-name of key sequence, such as ``u'KEY_LEFT'`` (str)."""
|
|
return self._name
|
|
|
|
@property
|
|
def code(self):
|
|
"""Integer keycode value of multibyte sequence (int)."""
|
|
return self._code
|
|
|
|
|
|
def get_curses_keycodes():
|
|
"""
|
|
Return mapping of curses key-names paired by their keycode integer value.
|
|
|
|
:rtype: dict
|
|
:returns: Dictionary of (name, code) pairs for curses keyboard constant
|
|
values and their mnemonic name. Such as code ``260``, with the value of
|
|
its key-name identity, ``u'KEY_LEFT'``.
|
|
"""
|
|
_keynames = [attr for attr in dir(curses)
|
|
if attr.startswith('KEY_')]
|
|
return {keyname: getattr(curses, keyname) for keyname in _keynames}
|
|
|
|
|
|
def get_keyboard_codes():
|
|
"""
|
|
Return mapping of keycode integer values paired by their curses key-name.
|
|
|
|
:rtype: dict
|
|
:returns: Dictionary of (code, name) pairs for curses keyboard constant
|
|
values and their mnemonic name. Such as key ``260``, with the value of
|
|
its identity, ``u'KEY_LEFT'``.
|
|
|
|
These keys are derived from the attributes by the same of the curses module,
|
|
with the following exceptions:
|
|
|
|
* ``KEY_DELETE`` in place of ``KEY_DC``
|
|
* ``KEY_INSERT`` in place of ``KEY_IC``
|
|
* ``KEY_PGUP`` in place of ``KEY_PPAGE``
|
|
* ``KEY_PGDOWN`` in place of ``KEY_NPAGE``
|
|
* ``KEY_ESCAPE`` in place of ``KEY_EXIT``
|
|
* ``KEY_SUP`` in place of ``KEY_SR``
|
|
* ``KEY_SDOWN`` in place of ``KEY_SF``
|
|
|
|
This function is the inverse of :func:`get_curses_keycodes`. With the
|
|
given override "mixins" listed above, the keycode for the delete key will
|
|
map to our imaginary ``KEY_DELETE`` mnemonic, effectively erasing the
|
|
phrase ``KEY_DC`` from our code vocabulary for anyone that wishes to use
|
|
the return value to determine the key-name by keycode.
|
|
"""
|
|
keycodes = OrderedDict(get_curses_keycodes())
|
|
keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN)
|
|
# merge _CURSES_KEYCODE_ADDINS added to our module space
|
|
keycodes.update(
|
|
(name, value) for name, value in globals().copy().items() if name.startswith('KEY_')
|
|
)
|
|
|
|
# invert dictionary (key, values) => (values, key), preferring the
|
|
# last-most inserted value ('KEY_DELETE' over 'KEY_DC').
|
|
return dict(zip(keycodes.values(), keycodes.keys()))
|
|
|
|
|
|
def _alternative_left_right(term):
|
|
r"""
|
|
Determine and return mapping of left and right arrow keys sequences.
|
|
|
|
:arg blessed.Terminal term: :class:`~.Terminal` instance.
|
|
:rtype: dict
|
|
:returns: Dictionary of sequences ``term._cuf1``, and ``term._cub1``,
|
|
valued as ``KEY_RIGHT``, ``KEY_LEFT`` (when appropriate).
|
|
|
|
This function supports :func:`get_terminal_sequences` to discover
|
|
the preferred input sequence for the left and right application keys.
|
|
|
|
It is necessary to check the value of these sequences to ensure we do not
|
|
use ``u' '`` and ``u'\b'`` for ``KEY_RIGHT`` and ``KEY_LEFT``,
|
|
preferring their true application key sequence, instead.
|
|
"""
|
|
# pylint: disable=protected-access
|
|
keymap = {}
|
|
if term._cuf1 and term._cuf1 != u' ':
|
|
keymap[term._cuf1] = curses.KEY_RIGHT
|
|
if term._cub1 and term._cub1 != u'\b':
|
|
keymap[term._cub1] = curses.KEY_LEFT
|
|
return keymap
|
|
|
|
|
|
def get_keyboard_sequences(term):
|
|
r"""
|
|
Return mapping of keyboard sequences paired by keycodes.
|
|
|
|
:arg blessed.Terminal term: :class:`~.Terminal` instance.
|
|
:returns: mapping of keyboard unicode sequences paired by keycodes
|
|
as integer. This is used as the argument ``mapper`` to
|
|
the supporting function :func:`resolve_sequence`.
|
|
:rtype: OrderedDict
|
|
|
|
Initialize and return a keyboard map and sequence lookup table,
|
|
(sequence, keycode) from :class:`~.Terminal` instance ``term``,
|
|
where ``sequence`` is a multibyte input sequence of unicode
|
|
characters, such as ``u'\x1b[D'``, and ``keycode`` is an integer
|
|
value, matching curses constant such as term.KEY_LEFT.
|
|
|
|
The return value is an OrderedDict instance, with their keys
|
|
sorted longest-first.
|
|
"""
|
|
# A small gem from curses.has_key that makes this all possible,
|
|
# _capability_names: a lookup table of terminal capability names for
|
|
# keyboard sequences (fe. kcub1, key_left), keyed by the values of
|
|
# constants found beginning with KEY_ in the main curses module
|
|
# (such as KEY_LEFT).
|
|
#
|
|
# latin1 encoding is used so that bytes in 8-bit range of 127-255
|
|
# have equivalent chr() and unichr() values, so that the sequence
|
|
# of a kermit or avatar terminal, for example, remains unchanged
|
|
# in its byte sequence values even when represented by unicode.
|
|
#
|
|
sequence_map = dict((
|
|
(seq.decode('latin1'), val)
|
|
for (seq, val) in (
|
|
(curses.tigetstr(cap), val)
|
|
for (val, cap) in capability_names.items()
|
|
) if seq
|
|
) if term.does_styling else ())
|
|
|
|
sequence_map.update(_alternative_left_right(term))
|
|
sequence_map.update(DEFAULT_SEQUENCE_MIXIN)
|
|
|
|
# This is for fast lookup matching of sequences, preferring
|
|
# full-length sequence such as ('\x1b[D', KEY_LEFT)
|
|
# over simple sequences such as ('\x1b', KEY_EXIT).
|
|
return OrderedDict((
|
|
(seq, sequence_map[seq]) for seq in sorted(
|
|
sequence_map.keys(), key=len, reverse=True)))
|
|
|
|
|
|
def get_leading_prefixes(sequences):
|
|
"""
|
|
Return a set of proper prefixes for given sequence of strings.
|
|
|
|
:arg iterable sequences
|
|
:rtype: set
|
|
:return: Set of all string prefixes
|
|
|
|
Given an iterable of strings, all textparts leading up to the final
|
|
string is returned as a unique set. This function supports the
|
|
:meth:`~.Terminal.inkey` method by determining whether the given
|
|
input is a sequence that **may** lead to a final matching pattern.
|
|
|
|
>>> prefixes(['abc', 'abdf', 'e', 'jkl'])
|
|
set([u'a', u'ab', u'abd', u'j', u'jk'])
|
|
"""
|
|
return {seq[:i] for seq in sequences for i in range(1, len(seq))}
|
|
|
|
|
|
def resolve_sequence(text, mapper, codes):
|
|
r"""
|
|
Return a single :class:`Keystroke` instance for given sequence ``text``.
|
|
|
|
:arg str text: string of characters received from terminal input stream.
|
|
:arg OrderedDict mapper: unicode multibyte sequences, such as ``u'\x1b[D'``
|
|
paired by their integer value (260)
|
|
:arg dict codes: a :type:`dict` of integer values (such as 260) paired
|
|
by their mnemonic name, such as ``'KEY_LEFT'``.
|
|
:rtype: Keystroke
|
|
:returns: Keystroke instance for the given sequence
|
|
|
|
The given ``text`` may extend beyond a matching sequence, such as
|
|
``u\x1b[Dxxx`` returns a :class:`Keystroke` instance of attribute
|
|
:attr:`Keystroke.sequence` valued only ``u\x1b[D``. It is up to
|
|
calls to determine that ``xxx`` remains unresolved.
|
|
"""
|
|
for sequence, code in mapper.items():
|
|
if text.startswith(sequence):
|
|
return Keystroke(ucs=sequence, code=code, name=codes[code])
|
|
return Keystroke(ucs=text and text[0] or u'')
|
|
|
|
|
|
def _time_left(stime, timeout):
|
|
"""
|
|
Return time remaining since ``stime`` before given ``timeout``.
|
|
|
|
This function assists determining the value of ``timeout`` for
|
|
class method :meth:`~.Terminal.kbhit` and similar functions.
|
|
|
|
:arg float stime: starting time for measurement
|
|
:arg float timeout: timeout period, may be set to None to
|
|
indicate no timeout (where None is always returned).
|
|
:rtype: float or int
|
|
:returns: time remaining as float. If no time is remaining,
|
|
then the integer ``0`` is returned.
|
|
"""
|
|
return max(0, timeout - (time.time() - stime)) if timeout else timeout
|
|
|
|
|
|
def _read_until(term, pattern, timeout):
|
|
"""
|
|
Convenience read-until-pattern function, supporting :meth:`~.get_location`.
|
|
|
|
:arg blessed.Terminal term: :class:`~.Terminal` instance.
|
|
:arg float timeout: timeout period, may be set to None to indicate no
|
|
timeout (where 0 is always returned).
|
|
:arg str pattern: target regular expression pattern to seek.
|
|
:rtype: tuple
|
|
:returns: tuple in form of ``(match, str)``, *match*
|
|
may be :class:`re.MatchObject` if pattern is discovered
|
|
in input stream before timeout has elapsed, otherwise
|
|
None. ``str`` is any remaining text received exclusive
|
|
of the matching pattern).
|
|
|
|
The reason a tuple containing non-matching data is returned, is that the
|
|
consumer should push such data back into the input buffer by
|
|
:meth:`~.Terminal.ungetch` if any was received.
|
|
|
|
For example, when a user is performing rapid input keystrokes while its
|
|
terminal emulator surreptitiously responds to this in-band sequence, we
|
|
must ensure any such keyboard data is well-received by the next call to
|
|
term.inkey() without delay.
|
|
"""
|
|
stime = time.time()
|
|
match, buf = None, u''
|
|
|
|
# first, buffer all pending data. pexpect library provides a
|
|
# 'searchwindowsize' attribute that limits this memory region. We're not
|
|
# concerned about OOM conditions: only (human) keyboard input and terminal
|
|
# response sequences are expected.
|
|
|
|
while True: # pragma: no branch
|
|
# block as long as necessary to ensure at least one character is
|
|
# received on input or remaining timeout has elapsed.
|
|
ucs = term.inkey(timeout=_time_left(stime, timeout))
|
|
# while the keyboard buffer is "hot" (has input), we continue to
|
|
# aggregate all awaiting data. We do this to ensure slow I/O
|
|
# calls do not unnecessarily give up within the first 'while' loop
|
|
# for short timeout periods.
|
|
while ucs:
|
|
buf += ucs
|
|
ucs = term.inkey(timeout=0)
|
|
|
|
match = re.search(pattern=pattern, string=buf)
|
|
if match is not None:
|
|
# match
|
|
break
|
|
|
|
if timeout is not None and not _time_left(stime, timeout):
|
|
# timeout
|
|
break
|
|
|
|
return match, buf
|
|
|
|
|
|
#: Though we may determine *keynames* and codes for keyboard input that
|
|
#: generate multibyte sequences, it is also especially useful to aliases
|
|
#: a few basic ASCII characters such as ``KEY_TAB`` instead of ``u'\t'`` for
|
|
#: uniformity.
|
|
#:
|
|
#: Furthermore, many key-names for application keys enabled only by context
|
|
#: manager :meth:`~.Terminal.keypad` are surprisingly absent. We inject them
|
|
#: here directly into the curses module.
|
|
_CURSES_KEYCODE_ADDINS = (
|
|
'TAB',
|
|
'KP_MULTIPLY',
|
|
'KP_ADD',
|
|
'KP_SEPARATOR',
|
|
'KP_SUBTRACT',
|
|
'KP_DECIMAL',
|
|
'KP_DIVIDE',
|
|
'KP_EQUAL',
|
|
'KP_0',
|
|
'KP_1',
|
|
'KP_2',
|
|
'KP_3',
|
|
'KP_4',
|
|
'KP_5',
|
|
'KP_6',
|
|
'KP_7',
|
|
'KP_8',
|
|
'KP_9')
|
|
|
|
_LASTVAL = max(get_curses_keycodes().values())
|
|
for keycode_name in _CURSES_KEYCODE_ADDINS:
|
|
_LASTVAL += 1
|
|
globals()['KEY_' + keycode_name] = _LASTVAL
|
|
|
|
#: In a perfect world, terminal emulators would always send exactly what
|
|
#: the terminfo(5) capability database plans for them, accordingly by the
|
|
#: value of the ``TERM`` name they declare.
|
|
#:
|
|
#: But this isn't a perfect world. Many vt220-derived terminals, such as
|
|
#: those declaring 'xterm', will continue to send vt220 codes instead of
|
|
#: their native-declared codes, for backwards-compatibility.
|
|
#:
|
|
#: This goes for many: rxvt, putty, iTerm.
|
|
#:
|
|
#: These "mixins" are used for *all* terminals, regardless of their type.
|
|
#:
|
|
#: Furthermore, curses does not provide sequences sent by the keypad,
|
|
#: at least, it does not provide a way to distinguish between keypad 0
|
|
#: and numeric 0.
|
|
DEFAULT_SEQUENCE_MIXIN = (
|
|
# these common control characters (and 127, ctrl+'?') mapped to
|
|
# an application key definition.
|
|
(six.unichr(10), curses.KEY_ENTER),
|
|
(six.unichr(13), curses.KEY_ENTER),
|
|
(six.unichr(8), curses.KEY_BACKSPACE),
|
|
(six.unichr(9), KEY_TAB), # noqa # pylint: disable=undefined-variable
|
|
(six.unichr(27), curses.KEY_EXIT),
|
|
(six.unichr(127), curses.KEY_BACKSPACE),
|
|
|
|
(u"\x1b[A", curses.KEY_UP),
|
|
(u"\x1b[B", curses.KEY_DOWN),
|
|
(u"\x1b[C", curses.KEY_RIGHT),
|
|
(u"\x1b[D", curses.KEY_LEFT),
|
|
(u"\x1b[1;2A", curses.KEY_SR),
|
|
(u"\x1b[1;2B", curses.KEY_SF),
|
|
(u"\x1b[1;2C", curses.KEY_SRIGHT),
|
|
(u"\x1b[1;2D", curses.KEY_SLEFT),
|
|
(u"\x1b[F", curses.KEY_END),
|
|
(u"\x1b[H", curses.KEY_HOME),
|
|
# not sure where these are from .. please report
|
|
(u"\x1b[K", curses.KEY_END),
|
|
(u"\x1b[U", curses.KEY_NPAGE),
|
|
(u"\x1b[V", curses.KEY_PPAGE),
|
|
|
|
# keys sent after term.smkx (keypad_xmit) is emitted, source:
|
|
# http://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys
|
|
# http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes
|
|
#
|
|
# keypad, numlock on
|
|
(u"\x1bOM", curses.KEY_ENTER), # noqa return
|
|
(u"\x1bOj", KEY_KP_MULTIPLY), # noqa * # pylint: disable=undefined-variable
|
|
(u"\x1bOk", KEY_KP_ADD), # noqa + # pylint: disable=undefined-variable
|
|
(u"\x1bOl", KEY_KP_SEPARATOR), # noqa , # pylint: disable=undefined-variable
|
|
(u"\x1bOm", KEY_KP_SUBTRACT), # noqa - # pylint: disable=undefined-variable
|
|
(u"\x1bOn", KEY_KP_DECIMAL), # noqa . # pylint: disable=undefined-variable
|
|
(u"\x1bOo", KEY_KP_DIVIDE), # noqa / # pylint: disable=undefined-variable
|
|
(u"\x1bOX", KEY_KP_EQUAL), # noqa = # pylint: disable=undefined-variable
|
|
(u"\x1bOp", KEY_KP_0), # noqa 0 # pylint: disable=undefined-variable
|
|
(u"\x1bOq", KEY_KP_1), # noqa 1 # pylint: disable=undefined-variable
|
|
(u"\x1bOr", KEY_KP_2), # noqa 2 # pylint: disable=undefined-variable
|
|
(u"\x1bOs", KEY_KP_3), # noqa 3 # pylint: disable=undefined-variable
|
|
(u"\x1bOt", KEY_KP_4), # noqa 4 # pylint: disable=undefined-variable
|
|
(u"\x1bOu", KEY_KP_5), # noqa 5 # pylint: disable=undefined-variable
|
|
(u"\x1bOv", KEY_KP_6), # noqa 6 # pylint: disable=undefined-variable
|
|
(u"\x1bOw", KEY_KP_7), # noqa 7 # pylint: disable=undefined-variable
|
|
(u"\x1bOx", KEY_KP_8), # noqa 8 # pylint: disable=undefined-variable
|
|
(u"\x1bOy", KEY_KP_9), # noqa 9 # pylint: disable=undefined-variable
|
|
|
|
# keypad, numlock off
|
|
(u"\x1b[1~", curses.KEY_FIND), # find
|
|
(u"\x1b[2~", curses.KEY_IC), # insert (0)
|
|
(u"\x1b[3~", curses.KEY_DC), # delete (.), "Execute"
|
|
(u"\x1b[4~", curses.KEY_SELECT), # select
|
|
(u"\x1b[5~", curses.KEY_PPAGE), # pgup (9)
|
|
(u"\x1b[6~", curses.KEY_NPAGE), # pgdown (3)
|
|
(u"\x1b[7~", curses.KEY_HOME), # home
|
|
(u"\x1b[8~", curses.KEY_END), # end
|
|
(u"\x1b[OA", curses.KEY_UP), # up (8)
|
|
(u"\x1b[OB", curses.KEY_DOWN), # down (2)
|
|
(u"\x1b[OC", curses.KEY_RIGHT), # right (6)
|
|
(u"\x1b[OD", curses.KEY_LEFT), # left (4)
|
|
(u"\x1b[OF", curses.KEY_END), # end (1)
|
|
(u"\x1b[OH", curses.KEY_HOME), # home (7)
|
|
|
|
# The vt220 placed F1-F4 above the keypad, in place of actual
|
|
# F1-F4 were local functions (hold screen, print screen,
|
|
# set up, data/talk, break).
|
|
(u"\x1bOP", curses.KEY_F1),
|
|
(u"\x1bOQ", curses.KEY_F2),
|
|
(u"\x1bOR", curses.KEY_F3),
|
|
(u"\x1bOS", curses.KEY_F4),
|
|
)
|
|
|
|
#: Override mixins for a few curses constants with easier
|
|
#: mnemonics: there may only be a 1:1 mapping when only a
|
|
#: keycode (int) is given, where these phrases are preferred.
|
|
CURSES_KEYCODE_OVERRIDE_MIXIN = (
|
|
('KEY_DELETE', curses.KEY_DC),
|
|
('KEY_INSERT', curses.KEY_IC),
|
|
('KEY_PGUP', curses.KEY_PPAGE),
|
|
('KEY_PGDOWN', curses.KEY_NPAGE),
|
|
('KEY_ESCAPE', curses.KEY_EXIT),
|
|
('KEY_SUP', curses.KEY_SR),
|
|
('KEY_SDOWN', curses.KEY_SF),
|
|
('KEY_UP_LEFT', curses.KEY_A1),
|
|
('KEY_UP_RIGHT', curses.KEY_A3),
|
|
('KEY_CENTER', curses.KEY_B2),
|
|
('KEY_BEGIN', curses.KEY_BEG),
|
|
)
|
|
|
|
__all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',)
|