From 35ab6136b815011b1293015f11f0ecab7c52ea1b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 8 Jan 2024 10:27:46 +0000 Subject: [PATCH] Improve the method via which unsupported sequences are ignored (#3800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make ignoring keys a silent operation But when we do so, ensure that we log what sequence was ignored if the key driver logging is enabled. The core requirement for #3742. * Start a proper ignore section in the dictionary of sequences And by "proper ignore section" I simply mean: have the sequences we want to ignore all gathered together in a really obvious location, and with some sort of explanation. * Simplify opt+§ in WezTerm on macOS * Improve the wezterm opt mappings This brings them more in line with other terminals I've tested. * Tidy up the explanation for the WezTerm mappings * Ignore various ctrl-cmd- sequences from kitty * Fix a couple of incorrectly-copied sequences * Transform Ctrl+§ into 0 under kitty Most (all?) other terminals on macOS do this anyway. * Tweak some wording * Add Shift-F11 and Shift-F12 for rxvt * Switch to using a special value for ignored sequences Rather than use the pre-existing convention of a tuple that contains Keys.Ignore, which *could* imply that a sequence maps to a set of keys that happens to include Key.Ignore, but isn't exclusive to just that, here we swap over to using a single special value for an ignored sequence. * Swap to using a value as a flag, not a type https://github.com/Textualize/textual/pull/3800#pullrequestreview-1770853775 --- src/textual/_ansi_sequences.py | 74 ++++++++++++++++++++++++++++++---- src/textual/_xterm_parser.py | 29 +++++++++++-- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/src/textual/_ansi_sequences.py b/src/textual/_ansi_sequences.py index a504ea718d..f993b4ddab 100644 --- a/src/textual/_ansi_sequences.py +++ b/src/textual/_ansi_sequences.py @@ -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,), @@ -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,), @@ -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 @@ -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 diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 1b2b47a64f..0a2ba5b045 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -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 @@ -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) @@ -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? @@ -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 @@ -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.