Skip to content

Commit

Permalink
Implement ESCDELAY environment value (#260)
Browse files Browse the repository at this point in the history
Closes #158

Also pins docformatter due to related issue PyCQA/docformatter#264
  • Loading branch information
jquast committed Dec 17, 2023
1 parent daaa307 commit 6941a63
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 13 deletions.
25 changes: 25 additions & 0 deletions blessed/keyboard.py
@@ -1,6 +1,7 @@
"""Sub-module providing 'keyboard awareness'."""

# std imports
import os
import re
import time
import platform
Expand Down Expand Up @@ -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',)
26 changes: 16 additions & 10 deletions blessed/terminal.py
Expand Up @@ -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,
Expand Down Expand Up @@ -1425,21 +1426,25 @@ 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.
: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.
Expand All @@ -1454,11 +1459,12 @@ def inkey(self, timeout=None, esc_delay=0.35):
<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.
_`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,
Expand Down
2 changes: 1 addition & 1 deletion tests/accessories.py
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tests/test_core.py
Expand Up @@ -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())
Expand All @@ -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()
Expand Down
30 changes: 30 additions & 0 deletions 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
Expand Down Expand Up @@ -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']
6 changes: 4 additions & 2 deletions tox.ini
Expand Up @@ -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 \
Expand All @@ -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 \
Expand Down

0 comments on commit 6941a63

Please sign in to comment.