"""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',)