Skip to content

Commit c1c8fdd

Browse files
committed
Handle progressive enhencements
1 parent 636b679 commit c1c8fdd

File tree

6 files changed

+232
-5
lines changed

6 files changed

+232
-5
lines changed

pyte/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
:license: LGPL, see LICENSE for more details.
2323
"""
2424

25-
__all__ = ("Screen", "DiffScreen", "HistoryScreen", "DebugScreen",
25+
__all__ = ("Screen", "DiffScreen", "HistoryScreen", "DebugScreen", "KeyboardFlags",
2626
"Stream", "ByteStream")
2727

2828
import io
2929
from typing import Union
3030

31-
from .screens import Screen, DiffScreen, HistoryScreen, DebugScreen
31+
from .screens import Screen, DiffScreen, HistoryScreen, DebugScreen, KeyboardFlags
3232
from .streams import Stream, ByteStream
3333

3434

pyte/escape.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,8 @@
150150

151151
#: *Horizontal position adjust*: Same as :data:`CHA`.
152152
HPA = "'"
153+
154+
#: *Progressive enhancement event*: Shell queries or sends flags to configure
155+
#: alternative keyboard escape sequences and key codes.
156+
#: see: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
157+
PE = "u"

pyte/screens.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import unicodedata
3535
import warnings
3636
from collections import deque, defaultdict
37+
from enum import IntFlag
3738
from functools import lru_cache
3839
from typing import Any, Callable, Dict, Generator, List, NamedTuple, Optional, Set, Sequence, TextIO, TypeVar
3940

@@ -52,11 +53,105 @@
5253
KT = TypeVar("KT")
5354
VT = TypeVar("VT")
5455

56+
57+
class KeyboardFlags(IntFlag):
58+
DEFAULT = 0
59+
"""
60+
All progressive enhancements disabled
61+
"""
62+
63+
DISAMBIGUATE_ESCAPE_CODES = 1
64+
"""
65+
This type of progressive enhancement (0b1) fixes the problem of some legacy
66+
key press encodings overlapping with other control codes. For instance,
67+
pressing the Esc key generates the byte 0x1b which also is used to indicate
68+
the start of an escape code. Similarly pressing the key alt+[ will generate
69+
the bytes used for CSI control codes.
70+
71+
Turning on this flag will cause the terminal to report the Esc, alt+key,
72+
ctrl+key, ctrl+alt+key, shift+alt+key keys using CSI u sequences instead of
73+
legacy ones. Here key is any ASCII key as described in Legacy text keys.
74+
Additionally, all non text keypad keys will be reported as separate keys
75+
with CSI u encoding, using dedicated numbers from the table below.
76+
77+
With this flag turned on, all key events that do not generate text are
78+
represented in one of the following two forms:
79+
80+
.. code:
81+
82+
CSI number; modifier u
83+
CSI 1; modifier [~ABCDEFHPQS]
84+
85+
This makes it very easy to parse key events in an application. In
86+
particular, ctrl+c will no longer generate the SIGINT signal, but instead
87+
be delivered as a CSI u escape code. This has the nice side effect of
88+
making it much easier to integrate into the application event loop. The
89+
only exceptions are the Enter, Tab and Backspace keys which still generate
90+
the same bytes as in legacy mode this is to allow the user to type and
91+
execute commands in the shell such as reset after a program that sets this
92+
mode crashes without clearing it. Note that the Lock modifiers are not
93+
reported for text producing keys, to keep them useable in legacy programs.
94+
To get lock modifiers for all keys use the Report all keys as escape codes
95+
enhancement.
96+
"""
97+
98+
REPORT_EVENT_TYPES = 2
99+
"""
100+
This progressive enhancement (0b10) causes the terminal to report key
101+
repeat and key release events. Normally only key press events are reported
102+
and key repeat events are treated as key press events. See Event types for
103+
details on how these are reported.
104+
"""
105+
106+
REPORT_ALTERNATE_KEYS = 4
107+
"""
108+
This progressive enhancement (0b100) causes the terminal to report
109+
alternate key values in addition to the main value, to aid in shortcut
110+
matching. See Key codes for details on how these are reported. Note that
111+
this flag is a pure enhancement to the form of the escape code used to
112+
represent key events, only key events represented as escape codes due to
113+
the other enhancements in effect will be affected by this enhancement. In
114+
other words, only if a key event was already going to be represented as an
115+
escape code due to one of the other enhancements will this enhancement
116+
affect it.
117+
"""
118+
119+
RPORT_ALL_KEYS_AS_ESCAPE_CODES = 8
120+
"""
121+
Key events that generate text, such as plain key presses without modifiers,
122+
result in just the text being sent, in the legacy protocol. There is no way
123+
to be notified of key repeat/release events. These types of events are
124+
needed for some applications, such as games (think of movement using the
125+
WASD keys).
126+
127+
This progressive enhancement (0b1000) turns on key reporting even for key
128+
events that generate text. When it is enabled, text will not be sent,
129+
instead only key events are sent. If the text is needed as well, combine
130+
with the Report associated text enhancement below.
131+
132+
Additionally, with this mode, events for pressing modifier keys are
133+
reported. Note that all keys are reported as escape codes, including Enter,
134+
Tab, Backspace etc. Note that this enhancement implies all keys are
135+
automatically disambiguated as well, since they are represented in their
136+
canonical escape code form.
137+
"""
138+
139+
REPORT_ASSOCIATED_TEXT = 16
140+
"""
141+
This progressive enhancement (0b10000) additionally causes key events that
142+
generate text to be reported as CSI u escape codes with the text embedded
143+
in the escape code. See Text as code points above for details on the
144+
mechanism. Note that this flag is an enhancement to Report all keys as
145+
escape codes and is undefined if used without it.
146+
"""
147+
148+
55149
class Margins(NamedTuple):
56150
"""A container for screen's scroll margins."""
57151
top: int
58152
bottom: int
59153

154+
60155
class Savepoint(NamedTuple):
61156
"""A container for savepoint, created on :data:`~pyte.escape.DECSC`."""
62157
cursor: Cursor
@@ -222,6 +317,7 @@ def __init__(self, columns: int, lines: int) -> None:
222317
self.reset()
223318
self.mode = _DEFAULT_MODE.copy()
224319
self.margins: Optional[Margins] = None
320+
self._keyboard_flags: list[KeyboardFlags] = [KeyboardFlags.DEFAULT]
225321

226322
def __repr__(self) -> str:
227323
return ("{0}({1}, {2})".format(self.__class__.__name__,
@@ -439,6 +535,58 @@ def reset_mode(self, *modes: int, **kwargs: Any) -> None:
439535
if mo.DECTCEM in mode_list:
440536
self.cursor.hidden = True
441537

538+
@property
539+
def keyboard_flags(self) -> KeyboardFlags:
540+
"""Keyboard flags of current stack level.
541+
542+
Keyboard flags are to be used by terminal implementations to decide
543+
how to encode keyboard events sent to shell applications.
544+
"""
545+
return self._keyboard_flags[-1]
546+
547+
def set_keyboard_flags(self, *args, private: bool = False, operator: str = "") -> None:
548+
"""Handle progressive enhancement events.
549+
550+
Assign keyboard flags for shells supporting "progressive enhancements".
551+
see: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
552+
"""
553+
if private:
554+
# CSI ? u
555+
# progressive enhancment state query
556+
# report flags of current stack level
557+
self.write_process_input(str(self.keyboard_flags))
558+
elif operator == "=":
559+
# assign/set/reset flags
560+
# CSI = u
561+
# CSI = mode u
562+
# CSI = flags ; mode u
563+
flags = KeyboardFlags.DEFAULT if len(args) == 0 else args[0]
564+
mode: int = 1 if len(args) < 2 else args[1]
565+
if mode == 1:
566+
# set all set and reset all unset bits
567+
self._keyboard_flags[-1] = flags
568+
elif mode == 2:
569+
# set all set and retain all unset bits
570+
self._keyboard_flags[-1] = self._keyboard_flags[-1] | flags
571+
elif mode == 3:
572+
# reset all set and retain all unset bits
573+
self._keyboard_flags[-1] = self._keyboard_flags[-1] & ~flags
574+
elif operator == ">":
575+
# push flags onto stack
576+
# CSI > u
577+
# CSI > flags u
578+
flags = KeyboardFlags.DEFAULT if len(args) == 0 else args[0]
579+
if len(self._keyboard_flags) < 99:
580+
self._keyboard_flags.append(flags)
581+
elif operator == "<":
582+
# pop flags from stack
583+
# CSI < u
584+
# CSI < count u
585+
count = 1 if len(args) == 0 else args[0]
586+
self._keyboard_flags = self._keyboard_flags[:-count]
587+
if len(self._keyboard_flags) == 0:
588+
self._keyboard_flags = [KeyboardFlags.DEFAULT]
589+
442590
def define_charset(self, code: str, mode: str) -> None:
443591
"""Define ``G0`` or ``G1`` charset.
444592

pyte/streams.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ class Stream:
121121
esc.SGR: "select_graphic_rendition",
122122
esc.DSR: "report_device_status",
123123
esc.DECSTBM: "set_margins",
124-
esc.HPA: "cursor_to_column"
124+
esc.HPA: "cursor_to_column",
125+
esc.PE: "set_keyboard_flags",
125126
}
126127

127128
#: A set of all events dispatched by the stream.
@@ -321,10 +322,17 @@ def create_dispatcher(mapping: Mapping[str, str]) -> Dict[str, Callable[..., Non
321322
params = []
322323
current = ""
323324
private = False
325+
operator = ""
324326
while True:
325327
char = yield None
326328
if char == "?":
327329
private = True
330+
elif char in "<>=":
331+
# may indicate secondary device attribute query
332+
# see: https://vt100.net/docs/vt510-rm/DA2.html
333+
# may be part of progressive enhencement
334+
# see: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
335+
operator = char
328336
elif char in ALLOWED_IN_CSI:
329337
basic_dispatch[char]()
330338
elif char in SP_OR_GT:
@@ -351,6 +359,8 @@ def create_dispatcher(mapping: Mapping[str, str]) -> Dict[str, Callable[..., Non
351359
else:
352360
if private:
353361
csi_dispatch[char](*params, private=True)
362+
elif operator:
363+
csi_dispatch[char](*params, operator=operator)
354364
else:
355365
csi_dispatch[char](*params)
356366
break # CSI is finished.

tests/test_screen.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pyte
66
from pyte import modes as mo, control as ctrl, graphics as g
7-
from pyte.screens import Char
7+
from pyte.screens import Char, KeyboardFlags
88

99

1010
# Test helpers.
@@ -1583,3 +1583,33 @@ def test_screen_set_icon_name_title():
15831583

15841584
screen.set_title(text)
15851585
assert screen.title == text
1586+
1587+
1588+
def test_progressive_enhancements():
1589+
screen = pyte.Screen(10, 1)
1590+
assert screen.keyboard_flags == KeyboardFlags.DEFAULT
1591+
# assign flags
1592+
screen.set_keyboard_flags(5, operator="=")
1593+
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
1594+
| KeyboardFlags.REPORT_ALTERNATE_KEYS
1595+
# set flags
1596+
screen.set_keyboard_flags(16, 2, operator="=")
1597+
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
1598+
| KeyboardFlags.REPORT_ALTERNATE_KEYS | KeyboardFlags.REPORT_ASSOCIATED_TEXT
1599+
# reset flags
1600+
screen.set_keyboard_flags(16, 3, operator="=")
1601+
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
1602+
| KeyboardFlags.REPORT_ALTERNATE_KEYS
1603+
1604+
# push flags to stack
1605+
screen.set_keyboard_flags(16, operator=">")
1606+
assert screen.keyboard_flags == KeyboardFlags.REPORT_ASSOCIATED_TEXT
1607+
1608+
# pop flags and expect bits from stack level 0 to be reported
1609+
screen.set_keyboard_flags(operator="<")
1610+
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
1611+
| KeyboardFlags.REPORT_ALTERNATE_KEYS
1612+
1613+
# pop stack level 0, resets flags to default
1614+
screen.set_keyboard_flags(operator="<")
1615+
assert screen.keyboard_flags == KeyboardFlags.DEFAULT

tests/test_stream.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pyte
66
from pyte import charsets as cs, control as ctrl, escape as esc
7-
7+
from pyte import KeyboardFlags
88

99
class counter:
1010
def __init__(self):
@@ -332,3 +332,37 @@ def test_byte_stream_select_other_charset():
332332
# c) enable utf-8
333333
stream.select_other_charset("G")
334334
assert stream.use_utf8
335+
336+
337+
def test_progressive_enhancements():
338+
screen = pyte.Screen(10, 1)
339+
stream = pyte.Stream(screen)
340+
assert screen.keyboard_flags == KeyboardFlags.DEFAULT
341+
# assign flags
342+
stream.feed(ctrl.CSI + "=5u")
343+
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
344+
| KeyboardFlags.REPORT_ALTERNATE_KEYS
345+
# set flags
346+
stream.feed(ctrl.CSI + "=16;2u")
347+
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
348+
| KeyboardFlags.REPORT_ALTERNATE_KEYS | KeyboardFlags.REPORT_ASSOCIATED_TEXT
349+
# reset flags
350+
stream.feed(ctrl.CSI + "=16;3u")
351+
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
352+
| KeyboardFlags.REPORT_ALTERNATE_KEYS
353+
354+
# push flags to stack
355+
stream.feed(ctrl.CSI + ">16u")
356+
assert screen.keyboard_flags == KeyboardFlags.REPORT_ASSOCIATED_TEXT
357+
358+
# pop flags and expect bits from stack level 0 to be reported
359+
stream.feed(ctrl.CSI + "<1u")
360+
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
361+
| KeyboardFlags.REPORT_ALTERNATE_KEYS
362+
363+
# pop stack level 0, resets flags to default
364+
stream.feed(ctrl.CSI + "<u")
365+
assert screen.keyboard_flags == KeyboardFlags.DEFAULT
366+
367+
# verify empty buffer to ensure nothing passed through
368+
assert screen.display[0] == " " * 10

0 commit comments

Comments
 (0)