From 6941a63db29857962ee943ce4a10946078d6be91 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Sat, 16 Dec 2023 22:33:50 -0500 Subject: [PATCH] Implement ESCDELAY environment value (#260) Closes #158 Also pins docformatter due to related issue https://github.com/PyCQA/docformatter/issues/264 --- blessed/keyboard.py | 25 +++++++++++++++++++++++++ blessed/terminal.py | 26 ++++++++++++++++---------- tests/accessories.py | 2 +- tests/test_core.py | 10 ++++++++++ tests/test_keyboard.py | 30 ++++++++++++++++++++++++++++++ tox.ini | 6 ++++-- 6 files changed, 86 insertions(+), 13 deletions(-) diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 31cc98c6..401ec459 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -1,6 +1,7 @@ """Sub-module providing 'keyboard awareness'.""" # std imports +import os import re import time import platform @@ -448,4 +449,28 @@ def _read_until(term, pattern, timeout): ('KEY_BEGIN', curses.KEY_BEG), ) +#: Default delay, in seconds, of Escape key detection in +#: :meth:`Terminal.inkey`.` curses has a default delay of 1000ms (1 second) for +#: escape sequences. This is too long for modern applications, so we set it to +#: 350ms, or 0.35 seconds. It is still a bit conservative, for remote telnet or +#: ssh servers, for example. +DEFAULT_ESCDELAY = 0.35 + + +def _reinit_escdelay(): + # pylint: disable=W0603 + # Using the global statement: this is necessary to + # allow test coverage without complex module reload + global DEFAULT_ESCDELAY + if os.environ.get('ESCDELAY'): + try: + DEFAULT_ESCDELAY = int(os.environ['ESCDELAY']) / 1000.0 + except ValueError: + # invalid values of 'ESCDELAY' are ignored + pass + + +_reinit_escdelay() + + __all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',) diff --git a/blessed/terminal.py b/blessed/terminal.py index 76214e9d..fc0b0c4f 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -18,7 +18,8 @@ # local from .color import COLOR_DISTANCE_ALGORITHMS -from .keyboard import (_time_left, +from .keyboard import (DEFAULT_ESCDELAY, + _time_left, _read_until, resolve_sequence, get_keyboard_codes, @@ -1425,8 +1426,8 @@ def keypad(self): self.stream.write(self.rmkx) self.stream.flush() - def inkey(self, timeout=None, esc_delay=0.35): - """ + def inkey(self, timeout=None, esc_delay=DEFAULT_ESCDELAY): + r""" Read and return the next keyboard event within given timeout. Generally, this should be used inside the :meth:`raw` context manager. @@ -1434,12 +1435,16 @@ def inkey(self, timeout=None, esc_delay=0.35): :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``. + :arg float esc_delay: Time in seconds to block after Escape key + is received to await another key sequence beginning with + escape such as *KEY_LEFT*, sequence ``'\x1b[D'``], before returning a + :class:`~.Keystroke` instance for ``KEY_ESCAPE``. + + Users may override the default value of ``esc_delay`` in seconds, + using environment value of ``ESCDELAY`` as milliseconds, see + `ncurses(3)`_ section labeled *ESCDELAY* for details. Setting + the value as an argument to this function will override any + such preference. :rtype: :class:`~.Keystroke`. :returns: :class:`~.Keystroke`, which may be empty (``u''``) if ``timeout`` is specified and keystroke is not received. @@ -1454,11 +1459,12 @@ def inkey(self, timeout=None, esc_delay=0.35): `_. 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. + + _`ncurses(3)`: https://www.man7.org/linux/man-pages/man3/ncurses.3x.html """ resolve = functools.partial(resolve_sequence, mapper=self._keymap, codes=self._keycodes) - stime = time.time() # re-buffer previously received keystrokes, diff --git a/tests/accessories.py b/tests/accessories.py index 9d04d5aa..54eec455 100644 --- a/tests/accessories.py +++ b/tests/accessories.py @@ -61,7 +61,7 @@ class as_subprocess(object): # pylint: disable=too-few-public-methods def __init__(self, func): self.func = func - def __call__(self, *args, **kwargs): # pylint: disable=too-many-locals, too-complex + def __call__(self, *args, **kwargs): # pylint: disable=too-many-locals,too-complex if IS_WINDOWS: self.func(*args, **kwargs) return diff --git a/tests/test_core.py b/tests/test_core.py index d5039dc1..cb6cf6f8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -94,6 +94,9 @@ def child(): @pytest.mark.skipif(IS_WINDOWS, reason="requires more than 1 tty") def test_number_of_colors_without_tty(): """``number_of_colors`` should return 0 when there's no tty.""" + if 'COLORTERM' in os.environ: + del os.environ['COLORTERM'] + @as_subprocess def child_256_nostyle(): t = TestTerminal(stream=six.StringIO()) @@ -118,6 +121,13 @@ def child_0_forcestyle(): force_styling=True) assert (t.number_of_colors == 0) + @as_subprocess + def child_24bit_forcestyle_with_colorterm(): + os.environ['COLORTERM'] = 'truecolor' + t = TestTerminal(kind='vt220', stream=six.StringIO(), + force_styling=True) + assert (t.number_of_colors == 1 << 24) + child_0_forcestyle() child_8_forcestyle() child_256_forcestyle() diff --git a/tests/test_keyboard.py b/tests/test_keyboard.py index 6622f4c7..8cd50e03 100644 --- a/tests/test_keyboard.py +++ b/tests/test_keyboard.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Tests for keyboard support.""" # std imports +import os import sys import platform import tempfile @@ -363,3 +364,32 @@ def child(kind): # pylint: disable=too-many-statements assert resolve(u"\x1bOS").name == "KEY_F4" child('xterm') + + +def test_ESCDELAY_unset_unchanged(): + """Unset ESCDELAY leaves DEFAULT_ESCDELAY unchanged in _reinit_escdelay().""" + if 'ESCDELAY' in os.environ: + del os.environ['ESCDELAY'] + import blessed.keyboard + prev_value = blessed.keyboard.DEFAULT_ESCDELAY + blessed.keyboard._reinit_escdelay() + assert blessed.keyboard.DEFAULT_ESCDELAY == prev_value + + +def test_ESCDELAY_bad_value_unchanged(): + """Invalid ESCDELAY leaves DEFAULT_ESCDELAY unchanged in _reinit_escdelay().""" + os.environ['ESCDELAY'] = 'XYZ123!' + import blessed.keyboard + prev_value = blessed.keyboard.DEFAULT_ESCDELAY + blessed.keyboard._reinit_escdelay() + assert blessed.keyboard.DEFAULT_ESCDELAY == prev_value + del os.environ['ESCDELAY'] + + +def test_ESCDELAY_10ms(): + """Verify ESCDELAY modifies DEFAULT_ESCDELAY in _reinit_escdelay().""" + os.environ['ESCDELAY'] = '1234' + import blessed.keyboard + blessed.keyboard._reinit_escdelay() + assert blessed.keyboard.DEFAULT_ESCDELAY == 1.234 + del os.environ['ESCDELAY'] diff --git a/tox.ini b/tox.ini index 017f354d..d3a44051 100644 --- a/tox.ini +++ b/tox.ini @@ -67,8 +67,9 @@ commands = autopep8 --in-place --recursive --aggressive --aggressive blessed/ bin/ setup.py [testenv:docformatter] +# docformatter pinned due to https://github.com/PyCQA/docformatter/issues/264 deps = - docformatter + docformatter<1.7.4 untokenize commands = docformatter \ @@ -83,8 +84,9 @@ commands = {toxinidir}/docs/conf.py [testenv:docformatter_check] +# docformatter pinned due to https://github.com/PyCQA/docformatter/issues/264 deps = - docformatter + docformatter<1.7.4 untokenize commands = docformatter \