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

Improve the method via which unsupported sequences are ignored #3800

Merged
merged 13 commits into from
Jan 8, 2024
74 changes: 67 additions & 7 deletions src/textual/_ansi_sequences.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@

from typing import Mapping, Tuple

from typing_extensions import Final

from .keys import Keys


class IgnoredSequence:
"""Class used to mark that a sequence should be ignored."""


IGNORE_SEQUENCE: Final[IgnoredSequence] = IgnoredSequence()
"""Constant to indicate that a sequence should be ignored."""


# Mapping of vt100 escape codes to Keys.
ANSI_SEQUENCES_KEYS: Mapping[str, Tuple[Keys, ...] | str] = {
ANSI_SEQUENCES_KEYS: Mapping[str, Tuple[Keys, ...] | str | IgnoredSequence] = {
# Control keys.
" ": (Keys.Space,),
"\r": (Keys.Enter,),
Expand Down Expand Up @@ -112,6 +123,8 @@
"\x1b[21;2~": (Keys.F22,),
"\x1b[23;2~": (Keys.F23,),
"\x1b[24;2~": (Keys.F24,),
"\x1b[23$": (Keys.F23,), # rxvt
"\x1b[24$": (Keys.F24,), # rxvt
# --
# Control + function keys.
"\x1b[1;5P": (Keys.ControlF1,),
Expand Down Expand Up @@ -170,12 +183,6 @@
# Tmux (Win32 subsystem) sends the following scroll events.
"\x1b[62~": (Keys.ScrollUp,),
"\x1b[63~": (Keys.ScrollDown,),
# --
# Sequences generated by numpad 5. Not sure what it means. (It doesn't
# appear in 'infocmp'. Just ignore.
"\x1b[E": (Keys.Ignore,), # Xterm.
"\x1b[G": (Keys.Ignore,), # Linux console.
# --
# Meta/control/escape + pageup/pagedown/insert/delete.
"\x1b[3;2~": (Keys.ShiftDelete,), # xterm, gnome-terminal.
"\x1b[3$": (Keys.ShiftDelete,), # rxvt
Expand Down Expand Up @@ -370,6 +377,59 @@
"\x1bOx": "8",
"\x1bOy": "9",
"\x1bOM": (Keys.Enter,),
# WezTerm on macOS emits sequences for Opt and keys on the top numeric
# row; whereas other terminals provide various characters. The following
# swallow up those sequences and turns them into characters the same as
# the other terminals.
"\x1b§": "§",
"\x1b1": "¡",
"\x1b2": "™",
"\x1b3": "£",
"\x1b4": "¢",
"\x1b5": "∞",
"\x1b6": "§",
"\x1b7": "¶",
"\x1b8": "•",
"\x1b9": "ª",
"\x1b0": "º",
"\x1b-": "–",
"\x1b=": "≠",
# Ctrl+§ on kitty is different from most other terminals on macOS.
"\x1b[167;5u": "0",
############################################################################
# The ignore section. Only add sequences here if they are going to be
# ignored. Also, when adding a sequence here, please include a note as
# to why it is being ignored; ideally citing sources if possible.
############################################################################
# The following 2 are inherited from prompt toolkit. They relate to a
# press of 5 on the numeric keypad, when *not* in number mode.
"\x1b[E": IGNORE_SEQUENCE, # Xterm.
"\x1b[G": IGNORE_SEQUENCE, # Linux console.
# Various ctrl+cmd+ keys under Kitty on macOS.
"\x1b[3;13~": IGNORE_SEQUENCE, # ctrl-cmd-del
"\x1b[1;13H": IGNORE_SEQUENCE, # ctrl-cmd-home
"\x1b[1;13F": IGNORE_SEQUENCE, # ctrl-cmd-end
"\x1b[5;13~": IGNORE_SEQUENCE, # ctrl-cmd-pgup
"\x1b[6;13~": IGNORE_SEQUENCE, # ctrl-cmd-pgdn
"\x1b[49;13u": IGNORE_SEQUENCE, # ctrl-cmd-1
"\x1b[50;13u": IGNORE_SEQUENCE, # ctrl-cmd-2
"\x1b[51;13u": IGNORE_SEQUENCE, # ctrl-cmd-3
"\x1b[52;13u": IGNORE_SEQUENCE, # ctrl-cmd-4
"\x1b[53;13u": IGNORE_SEQUENCE, # ctrl-cmd-5
"\x1b[54;13u": IGNORE_SEQUENCE, # ctrl-cmd-6
"\x1b[55;13u": IGNORE_SEQUENCE, # ctrl-cmd-7
"\x1b[56;13u": IGNORE_SEQUENCE, # ctrl-cmd-8
"\x1b[57;13u": IGNORE_SEQUENCE, # ctrl-cmd-9
"\x1b[48;13u": IGNORE_SEQUENCE, # ctrl-cmd-0
"\x1b[45;13u": IGNORE_SEQUENCE, # ctrl-cmd--
"\x1b[61;13u": IGNORE_SEQUENCE, # ctrl-cmd-+
"\x1b[91;13u": IGNORE_SEQUENCE, # ctrl-cmd-[
"\x1b[93;13u": IGNORE_SEQUENCE, # ctrl-cmd-]
"\x1b[92;13u": IGNORE_SEQUENCE, # ctrl-cmd-\
"\x1b[39;13u": IGNORE_SEQUENCE, # ctrl-cmd-'
"\x1b[59;13u": IGNORE_SEQUENCE, # ctrl-cmd-;
"\x1b[47;13u": IGNORE_SEQUENCE, # ctrl-cmd-/
"\x1b[46;13u": IGNORE_SEQUENCE, # ctrl-cmd-.
}

# https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
Expand Down
29 changes: 25 additions & 4 deletions src/textual/_xterm_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from typing import Any, Callable, Generator, Iterable

from . import events, messages
from ._ansi_sequences import ANSI_SEQUENCES_KEYS
from ._ansi_sequences import ANSI_SEQUENCES_KEYS, IGNORE_SEQUENCE
from ._parser import Awaitable, Parser, TokenCallback
from .keys import KEY_NAME_REPLACEMENTS, _character_to_key
from .keys import KEY_NAME_REPLACEMENTS, Keys, _character_to_key

# When trying to determine whether the current sequence is a supported/valid
# escape sequence, at which length should we give up and consider our search
Expand Down Expand Up @@ -100,6 +100,20 @@ def parse(self, on_token: TokenCallback) -> Generator[Awaitable, str, None]:
bracketed_paste = False
use_prior_escape = False

def on_key_token(event: events.Key) -> None:
"""Token callback wrapper for handling keys.

Args:
event: The key event to send to the callback.

This wrapper looks for keys that should be ignored, and filters
them out, logging the ignored sequence when it does.
"""
if event.key == Keys.Ignore:
self.debug_log(f"ignored={event.character!r}")
else:
on_token(event)

def reissue_sequence_as_keys(reissue_sequence: str) -> None:
if self._reissued_sequence_debug_book is not None:
self._reissued_sequence_debug_book(reissue_sequence)
Expand Down Expand Up @@ -204,7 +218,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None:
# Was it a pressed key event that we received?
key_events = list(sequence_to_key_events(sequence))
for key_event in key_events:
on_token(key_event)
on_key_token(key_event)
if key_events:
break
# Or a mouse event?
Expand All @@ -229,7 +243,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None:
else:
if not bracketed_paste:
for event in sequence_to_key_events(character):
on_token(event)
on_key_token(event)

def _sequence_to_key_events(
self, sequence: str, _unicode_name=unicodedata.name
Expand All @@ -243,6 +257,13 @@ def _sequence_to_key_events(
Keys
"""
keys = ANSI_SEQUENCES_KEYS.get(sequence)
# If we're being asked to ignore the key...
if keys is IGNORE_SEQUENCE:
# ...build a special ignore key event, which has the ignore
# name as the key (that is, the key this sequence is bound
# to is the ignore key) and the sequence that was ignored as
# the character.
yield events.Key(Keys.Ignore, sequence)
if isinstance(keys, tuple):
# If the sequence mapped to a tuple, then it's values from the
# `Keys` enum. Raise key events from what we find in the tuple.
Expand Down
Loading