diff --git a/pyte/__init__.py b/pyte/__init__.py index 35d0a98..778414a 100644 --- a/pyte/__init__.py +++ b/pyte/__init__.py @@ -22,9 +22,11 @@ :license: LGPL, see LICENSE for more details. """ -__all__ = ("Screen", "DiffScreen", "HistoryScreen", "DebugScreen", +__all__ = ("KeypadMode", + "Screen", "DiffScreen", "HistoryScreen", "DebugScreen", "Stream", "ByteStream") +from .keyboard import KeypadMode from .screens import Screen, DiffScreen, HistoryScreen, DebugScreen from .streams import Stream, ByteStream diff --git a/pyte/escape.py b/pyte/escape.py index 7dfd6b8..1aa4ca8 100644 --- a/pyte/escape.py +++ b/pyte/escape.py @@ -11,6 +11,9 @@ :license: LGPL, see LICENSE for more details. """ +# non-CSI escape sequences. +# ------------------------- + #: *Reset*. RIS = "c" @@ -38,6 +41,14 @@ #: selection. If none were saved, move cursor to home position. DECRC = "8" +#: *Set keypad application mode*: Keypad sends control sequences (NUMLOCK off) +#: see: https://vt100.net/docs/vt510-rm/DECKPAM.html +DECKPAM = "=" + +#: *Set keypad numeric mode*: Keypad sends numbers (NUMLOCK ON). +#: see: https://vt100.net/docs/vt510-rm/DECKPNM.html +DECKPNM = ">" + # "Sharp" escape sequences. # ------------------------- diff --git a/pyte/keyboard.py b/pyte/keyboard.py new file mode 100644 index 0000000..6b73905 --- /dev/null +++ b/pyte/keyboard.py @@ -0,0 +1,10 @@ +from __future__ import annotations +from enum import IntEnum + + +class KeypadMode(IntEnum): + """Supported keypad modes""" + NUMERIC = 0 + """Keypad sends numbers (default).""" + APPLICATION = 1 + """Keypad sends control sequences.""" diff --git a/pyte/screens.py b/pyte/screens.py index 384542f..55dcaa6 100644 --- a/pyte/screens.py +++ b/pyte/screens.py @@ -42,9 +42,11 @@ from . import ( charsets as cs, control as ctrl, + escape as esc, graphics as g, modes as mo ) +from .keyboard import KeypadMode from .streams import Stream if TYPE_CHECKING: @@ -56,11 +58,13 @@ KT = TypeVar("KT") VT = TypeVar("VT") + class Margins(NamedTuple): """A container for screen's scroll margins.""" top: int bottom: int + class Savepoint(NamedTuple): """A container for savepoint, created on :data:`~pyte.escape.DECSC`.""" cursor: Cursor @@ -277,6 +281,8 @@ def reset(self) -> None: self.g0_charset = cs.LAT1_MAP self.g1_charset = cs.VT100_MAP + self.keypad_mode: KeypadMode = KeypadMode.NUMERIC + # From ``man terminfo`` -- "... hardware tabs are initially # set every `n` spaces when the terminal is powered up. Since # we aim to support VT102 / VT220 and linux -- we use n = 8. @@ -443,6 +449,23 @@ def reset_mode(self, *modes: int, **kwargs: Any) -> None: if mo.DECTCEM in mode_list: self.cursor.hidden = True + def set_keypad_mode(self, mode: str) -> None: + """Set keypad mode + + DECKPAM enables the keypad to send application sequences. + DECKPNM enables the keypad to send numeric characters to the host. + + Default: Send numeric keypad characters. + + :param: mode + ``esc.DECKPAM`` - application mode + ``esc.DECKPNM`` - numeric mode + """ + if mode == esc.DECKPAM: + self.keypad_mode = KeypadMode.APPLICATION + else: + self.keypad_mode = KeypadMode.NUMERIC + def define_charset(self, code: str, mode: str) -> None: """Define ``G0`` or ``G1`` charset. diff --git a/pyte/streams.py b/pyte/streams.py index f728780..ee5a8d9 100644 --- a/pyte/streams.py +++ b/pyte/streams.py @@ -129,6 +129,7 @@ class Stream: events = frozenset(itertools.chain( basic.values(), escape.values(), sharp.values(), csi.values(), ["define_charset"], + ["set_keypad_mode"], ["set_icon_name", "set_title"], # OSC. ["draw", "debug"])) @@ -239,6 +240,7 @@ def _parser_fsm(self) -> ParserGenerator: debug = listener.debug ESC, CSI_C1 = ctrl.ESC, ctrl.CSI_C1 + KEYPAD_MODE = esc.DECKPAM + esc.DECKPNM OSC_C1 = ctrl.OSC_C1 SP_OR_GT = ctrl.SP + ">" NUL_OR_DEL = ctrl.NUL + ctrl.DEL @@ -293,6 +295,11 @@ def create_dispatcher(mapping: Mapping[str, str]) -> dict[str, Callable[..., Non # See http://www.cl.cam.ac.uk/~mgk25/unicode.html#term # for the why on the UTF-8 restriction. listener.define_charset(code, mode=char) + elif char in KEYPAD_MODE: + # See https://vt100.net/docs/vt510-rm/DECKPAM.html + # DECKPAM enables the keypad to send application sequences. + # DECKPNM enables the keypad to send numeric characters to the host. + listener.set_keypad_mode(mode=char) else: escape_dispatch[char]() continue # Don't go to CSI. diff --git a/tests/test_screen.py b/tests/test_screen.py index 7626f61..af0924f 100644 --- a/tests/test_screen.py +++ b/tests/test_screen.py @@ -3,7 +3,7 @@ import pytest import pyte -from pyte import modes as mo, control as ctrl, graphics as g +from pyte import escape as esc, modes as mo, control as ctrl, graphics as g from pyte.screens import Char @@ -301,6 +301,28 @@ def test_set_mode(): assert screen.cursor.hidden +def test_set_keypad_mode(): + screen = pyte.Screen(2, 1) + # test numeric mode being the default + assert screen.keypad_mode == pyte.KeypadMode.NUMERIC + # test setting application mode + screen.set_keypad_mode(esc.DECKPAM) + assert screen.keypad_mode == pyte.KeypadMode.APPLICATION + # test setting numeric mode + screen.set_keypad_mode(esc.DECKPNM) + assert screen.keypad_mode == pyte.KeypadMode.NUMERIC + + +def test_numeric_keypad_mode_on_reset(): + screen = pyte.Screen(2, 1) + # test setting application mode + screen.set_keypad_mode(esc.DECKPAM) + assert screen.keypad_mode == pyte.KeypadMode.APPLICATION + # test numeric mode after reset + screen.reset() + assert screen.keypad_mode == pyte.KeypadMode.NUMERIC + + def test_draw(): # ``DECAWM`` on (default). screen = pyte.Screen(3, 3) diff --git a/tests/test_stream.py b/tests/test_stream.py index 23eb357..8bd652c 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -332,3 +332,29 @@ def test_byte_stream_select_other_charset(): # c) enable utf-8 stream.select_other_charset("G") assert stream.use_utf8 + + +@pytest.mark.parametrize("mode", [esc.DECKPAM, esc.DECKPNM]) +def test_set_keypad_mode_args(mode): + """Verify escape sequences being passed to event handler.""" + handler = argcheck() + screen = pyte.Screen(80, 1) + setattr(screen, 'set_keypad_mode', handler) + + stream = pyte.Stream(screen) + stream.feed(ctrl.ESC + mode) + assert handler.count == 1 + assert handler.kwargs == {"mode": mode} + + +def test_set_keypad_mode(): + """Verify escape sequences correctly applying keypad mode.""" + screen = pyte.Screen(80, 1) + stream = pyte.Stream(screen) + assert screen.keypad_mode == pyte.KeypadMode.NUMERIC + stream.feed(ctrl.ESC + esc.DECKPAM) + assert screen.keypad_mode == pyte.KeypadMode.APPLICATION + stream.feed(ctrl.ESC + esc.DECKPNM) + assert screen.keypad_mode == pyte.KeypadMode.NUMERIC + # verify nothing added to buffer + assert screen.display[0] == " " * 80