Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ESCDELAY environment value #260

Merged
merged 9 commits into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions blessed/keyboard.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading