Skip to content
Open
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
4 changes: 3 additions & 1 deletion pyte/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
:license: LGPL, see LICENSE for more details.
"""

__all__ = ("Screen", "DiffScreen", "HistoryScreen", "DebugScreen",
__all__ = ("KeyboardFlags",
"Screen", "DiffScreen", "HistoryScreen", "DebugScreen",
"Stream", "ByteStream")

from .keyboard import KeyboardFlags
from .screens import Screen, DiffScreen, HistoryScreen, DebugScreen
from .streams import Stream, ByteStream

Expand Down
5 changes: 5 additions & 0 deletions pyte/escape.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,8 @@

#: *Horizontal position adjust*: Same as :data:`CHA`.
HPA = "'"

#: *Progressive enhancement event*: Shell queries or sends flags to configure
#: alternative keyboard escape sequences and key codes.
#: see: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
PE = "u"
94 changes: 94 additions & 0 deletions pyte/keyboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from __future__ import annotations
from enum import IntFlag


class KeyboardFlags(IntFlag):
DEFAULT = 0
"""
All progressive enhancements disabled
"""

DISAMBIGUATE_ESCAPE_CODES = 1
"""
This type of progressive enhancement (0b1) fixes the problem of some legacy
key press encodings overlapping with other control codes. For instance,
pressing the Esc key generates the byte 0x1b which also is used to indicate
the start of an escape code. Similarly pressing the key alt+[ will generate
the bytes used for CSI control codes.

Turning on this flag will cause the terminal to report the Esc, alt+key,
ctrl+key, ctrl+alt+key, shift+alt+key keys using CSI u sequences instead of
legacy ones. Here key is any ASCII key as described in Legacy text keys.
Additionally, all non text keypad keys will be reported as separate keys
with CSI u encoding, using dedicated numbers from the table below.

With this flag turned on, all key events that do not generate text are
represented in one of the following two forms:

.. code:

CSI number; modifier u
CSI 1; modifier [~ABCDEFHPQS]

This makes it very easy to parse key events in an application. In
particular, ctrl+c will no longer generate the SIGINT signal, but instead
be delivered as a CSI u escape code. This has the nice side effect of
making it much easier to integrate into the application event loop. The
only exceptions are the Enter, Tab and Backspace keys which still generate
the same bytes as in legacy mode this is to allow the user to type and
execute commands in the shell such as reset after a program that sets this
mode crashes without clearing it. Note that the Lock modifiers are not
reported for text producing keys, to keep them useable in legacy programs.
To get lock modifiers for all keys use the Report all keys as escape codes
enhancement.
"""

REPORT_EVENT_TYPES = 2
"""
This progressive enhancement (0b10) causes the terminal to report key
repeat and key release events. Normally only key press events are reported
and key repeat events are treated as key press events. See Event types for
details on how these are reported.
"""

REPORT_ALTERNATE_KEYS = 4
"""
This progressive enhancement (0b100) causes the terminal to report
alternate key values in addition to the main value, to aid in shortcut
matching. See Key codes for details on how these are reported. Note that
this flag is a pure enhancement to the form of the escape code used to
represent key events, only key events represented as escape codes due to
the other enhancements in effect will be affected by this enhancement. In
other words, only if a key event was already going to be represented as an
escape code due to one of the other enhancements will this enhancement
affect it.
"""

REPORT_ALL_KEYS_AS_ESCAPE_CODES = 8
"""
Key events that generate text, such as plain key presses without modifiers,
result in just the text being sent, in the legacy protocol. There is no way
to be notified of key repeat/release events. These types of events are
needed for some applications, such as games (think of movement using the
WASD keys).

This progressive enhancement (0b1000) turns on key reporting even for key
events that generate text. When it is enabled, text will not be sent,
instead only key events are sent. If the text is needed as well, combine
with the Report associated text enhancement below.

Additionally, with this mode, events for pressing modifier keys are
reported. Note that all keys are reported as escape codes, including Enter,
Tab, Backspace etc. Note that this enhancement implies all keys are
automatically disambiguated as well, since they are represented in their
canonical escape code form.
"""

REPORT_ASSOCIATED_TEXT = 16
"""
This progressive enhancement (0b10000) additionally causes key events that
generate text to be reported as CSI u escape codes with the text embedded
in the escape code. See Text as code points above for details on the
mechanism. Note that this flag is an enhancement to Report all keys as
escape codes and is undefined if used without it.
"""
56 changes: 56 additions & 0 deletions pyte/screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
graphics as g,
modes as mo
)
from .keyboard import KeyboardFlags
from .streams import Stream

if TYPE_CHECKING:
Expand All @@ -56,11 +57,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
Expand Down Expand Up @@ -226,6 +229,7 @@ def __init__(self, columns: int, lines: int) -> None:
self.reset()
self.mode = _DEFAULT_MODE.copy()
self.margins: Margins | None = None
self._keyboard_flags: list[KeyboardFlags] = [KeyboardFlags.DEFAULT]

def __repr__(self) -> str:
return ("{}({}, {})".format(self.__class__.__name__,
Expand Down Expand Up @@ -443,6 +447,58 @@ def reset_mode(self, *modes: int, **kwargs: Any) -> None:
if mo.DECTCEM in mode_list:
self.cursor.hidden = True

@property
def keyboard_flags(self) -> KeyboardFlags:
"""Keyboard flags of current stack level.

Keyboard flags are to be used by terminal implementations to decide
how to encode keyboard events sent to shell applications.
"""
return self._keyboard_flags[-1]

def set_keyboard_flags(self, *args: int, private: bool = False, operator: str = "") -> None:
"""Handle progressive enhancement events.

Assign keyboard flags for shells supporting "progressive enhancements".
see: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
"""
if private:
# CSI ? u
# progressive enhancment state query
# report flags of current stack level
self.write_process_input(str(self.keyboard_flags))
elif operator == "=":
# assign/set/reset flags
# CSI = u
# CSI = mode u
# CSI = flags ; mode u
flags = KeyboardFlags.DEFAULT if len(args) == 0 else KeyboardFlags(args[0])
mode = 1 if len(args) < 2 else args[1]
if mode == 1:
# set all set and reset all unset bits
self._keyboard_flags[-1] = KeyboardFlags(flags)
elif mode == 2:
# set all set and retain all unset bits
self._keyboard_flags[-1] = self._keyboard_flags[-1] | flags
elif mode == 3:
# reset all set and retain all unset bits
self._keyboard_flags[-1] = self._keyboard_flags[-1] & ~flags
elif operator == ">":
# push flags onto stack
# CSI > u
# CSI > flags u
flags = KeyboardFlags.DEFAULT if len(args) == 0 else KeyboardFlags(args[0])
if len(self._keyboard_flags) < 99:
self._keyboard_flags.append(flags)
elif operator == "<":
# pop flags from stack
# CSI < u
# CSI < count u
count = 1 if len(args) == 0 else args[0]
self._keyboard_flags = self._keyboard_flags[:-count]
if len(self._keyboard_flags) == 0:
self._keyboard_flags = [KeyboardFlags.DEFAULT]

def define_charset(self, code: str, mode: str) -> None:
"""Define ``G0`` or ``G1`` charset.

Expand Down
12 changes: 11 additions & 1 deletion pyte/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ class Stream:
esc.SGR: "select_graphic_rendition",
esc.DSR: "report_device_status",
esc.DECSTBM: "set_margins",
esc.HPA: "cursor_to_column"
esc.HPA: "cursor_to_column",
esc.PE: "set_keyboard_flags",
}

#: A set of all events dispatched by the stream.
Expand Down Expand Up @@ -322,10 +323,17 @@ def create_dispatcher(mapping: Mapping[str, str]) -> dict[str, Callable[..., Non
params = []
current = ""
private = False
operator = ""
while True:
char = yield None
if char == "?":
private = True
elif char in "<>=":
# may indicate secondary device attribute query
# see: https://vt100.net/docs/vt510-rm/DA2.html
# may be part of progressive enhencement
# see: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
operator = char
elif char in ALLOWED_IN_CSI:
basic_dispatch[char]()
elif char in SP_OR_GT:
Expand All @@ -352,6 +360,8 @@ def create_dispatcher(mapping: Mapping[str, str]) -> dict[str, Callable[..., Non
else:
if private:
csi_dispatch[char](*params, private=True)
elif operator:
csi_dispatch[char](*params, operator=operator)
else:
csi_dispatch[char](*params)
break # CSI is finished.
Expand Down
35 changes: 34 additions & 1 deletion tests/test_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pyte
from pyte import modes as mo, control as ctrl, graphics as g
from pyte.screens import Char
from pyte.screens import Char, KeyboardFlags


# Test helpers.
Expand Down Expand Up @@ -1583,3 +1583,36 @@ def test_screen_set_icon_name_title():

screen.set_title(text)
assert screen.title == text


def test_progressive_enhancements():
screen = pyte.Screen(10, 1)
assert screen.keyboard_flags == KeyboardFlags.DEFAULT

# assign flags
screen.set_keyboard_flags(5, operator="=")
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
| KeyboardFlags.REPORT_ALTERNATE_KEYS

# set flags
screen.set_keyboard_flags(16, 2, operator="=")
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
| KeyboardFlags.REPORT_ALTERNATE_KEYS | KeyboardFlags.REPORT_ASSOCIATED_TEXT

# reset flags
screen.set_keyboard_flags(16, 3, operator="=")
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
| KeyboardFlags.REPORT_ALTERNATE_KEYS

# push flags to stack
screen.set_keyboard_flags(16, operator=">")
assert screen.keyboard_flags == KeyboardFlags.REPORT_ASSOCIATED_TEXT

# pop flags and expect bits from stack level 0 to be reported
screen.set_keyboard_flags(operator="<")
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
| KeyboardFlags.REPORT_ALTERNATE_KEYS

# pop stack level 0, resets flags to default
screen.set_keyboard_flags(operator="<")
assert screen.keyboard_flags == KeyboardFlags.DEFAULT
39 changes: 38 additions & 1 deletion tests/test_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pyte
from pyte import charsets as cs, control as ctrl, escape as esc

from pyte import KeyboardFlags

class counter:
def __init__(self):
Expand Down Expand Up @@ -332,3 +332,40 @@ def test_byte_stream_select_other_charset():
# c) enable utf-8
stream.select_other_charset("G")
assert stream.use_utf8


def test_progressive_enhancements():
screen = pyte.Screen(10, 1)
stream = pyte.Stream(screen)
assert screen.keyboard_flags == KeyboardFlags.DEFAULT

# assign flags
stream.feed(ctrl.CSI + "=5u")
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
| KeyboardFlags.REPORT_ALTERNATE_KEYS

# set flags
stream.feed(ctrl.CSI + "=16;2u")
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
| KeyboardFlags.REPORT_ALTERNATE_KEYS | KeyboardFlags.REPORT_ASSOCIATED_TEXT

# reset flags
stream.feed(ctrl.CSI + "=16;3u")
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
| KeyboardFlags.REPORT_ALTERNATE_KEYS

# push flags to stack
stream.feed(ctrl.CSI + ">16u")
assert screen.keyboard_flags == KeyboardFlags.REPORT_ASSOCIATED_TEXT

# pop flags and expect bits from stack level 0 to be reported
stream.feed(ctrl.CSI + "<1u")
assert screen.keyboard_flags == KeyboardFlags.DISAMBIGUATE_ESCAPE_CODES \
| KeyboardFlags.REPORT_ALTERNATE_KEYS

# pop stack level 0, resets flags to default
stream.feed(ctrl.CSI + "<u")
assert screen.keyboard_flags == KeyboardFlags.DEFAULT

# verify empty buffer to ensure nothing passed through
assert screen.display[0] == " " * 10