summaryrefslogtreecommitdiffstats
path: root/third_party/python/blessed/blessed/keyboard.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/blessed/blessed/keyboard.py')
-rw-r--r--third_party/python/blessed/blessed/keyboard.py449
1 files changed, 449 insertions, 0 deletions
diff --git a/third_party/python/blessed/blessed/keyboard.py b/third_party/python/blessed/blessed/keyboard.py
new file mode 100644
index 0000000000..2736da160f
--- /dev/null
+++ b/third_party/python/blessed/blessed/keyboard.py
@@ -0,0 +1,449 @@
+"""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().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',)