diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 6203163ccb..1b7f3e57d9 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -163,20 +163,29 @@ def on_key_token(event: events.Key) -> None: else: on_token(event) - def reissue_sequence_as_keys(reissue_sequence: str) -> None: + def reissue_sequence_as_keys( + reissue_sequence: str, process_alt: bool = False + ) -> None: """Called when an escape sequence hasn't been understood. Args: reissue_sequence: Key sequence to report to the app. """ + + alt = False + if reissue_sequence: self.debug_log("REISSUE", repr(reissue_sequence)) for character in reissue_sequence: - key_events = sequence_to_key_events(character) + if process_alt and character == ESC: + alt = True + continue + key_events = sequence_to_key_events(character, alt=alt) for event in key_events: - if event.key == "escape": + if event.key == "escape" and not process_alt: event = events.Key("circumflex_accent", "^") on_token(event) + alt = False while not self.is_eof: if not bracketed_paste and paste_buffer: @@ -211,23 +220,25 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None: # # Could be the escape key was pressed OR the start of an escape sequence sequence: str = ESC - def send_escape() -> None: + def send_sequence(process_alt: bool = True) -> None: """Send escape key and reissue sequence.""" - on_token(events.Key("escape", "\x1b")) - reissue_sequence_as_keys(sequence[1:]) + if sequence == ESC: + on_token(events.Key("escape", "\x1b")) + else: + reissue_sequence_as_keys(sequence, process_alt=process_alt) while True: try: new_character = yield read1(constants.ESCAPE_DELAY) except ParseTimeout: - send_escape() + send_sequence() break except ParseEOF: - send_escape() + send_sequence() return if new_character == ESC: - send_escape() + send_sequence(process_alt=False) sequence = character continue else: @@ -313,7 +324,9 @@ def send_escape() -> None: self._debug_log_file.close() self._debug_log_file = None - def _sequence_to_key_events(self, sequence: str) -> Iterable[events.Key]: + def _sequence_to_key_events( + self, sequence: str, alt: bool = False + ) -> Iterable[events.Key]: """Map a sequence of code points on to a sequence of keys. Args: @@ -342,7 +355,7 @@ def _sequence_to_key_events(self, sequence: str) -> Iterable[events.Key]: key_tokens.append(modifier) key_tokens.sort() - key_tokens.append(key) + key_tokens.append(key.lower()) yield events.Key( "+".join(key_tokens), sequence if len(sequence) == 1 else None ) @@ -376,7 +389,12 @@ def _sequence_to_key_events(self, sequence: str) -> Iterable[events.Key]: name = _character_to_key(sequence) else: name = sequence + name = KEY_NAME_REPLACEMENTS.get(name, name) + if len(name) == 1 and alt: + if name.isupper(): + name = f"shift+{name.lower()}" + name = f"alt+{name}" yield events.Key(name, sequence) except Exception: yield events.Key(sequence, sequence) diff --git a/tests/test_xterm_parser.py b/tests/test_xterm_parser.py index 609313232c..28a002f2e6 100644 --- a/tests/test_xterm_parser.py +++ b/tests/test_xterm_parser.py @@ -132,13 +132,13 @@ def test_unknown_sequence_followed_by_known_sequence(parser, chunk_size): events = [] for chunk in chunks(sequence, chunk_size): - events.append(parser.feed(chunk)) + events.extend(list(parser.feed(chunk))) - events = list(itertools.chain.from_iterable(list(event) for event in events)) + # events = list(itertools.chain.from_iterable(list(event) for event in events)) print(repr([event.key for event in events])) assert [event.key for event in events] == [ - "escape", + "circumflex_accent", "left_square_bracket", "question_mark", "end", @@ -181,6 +181,29 @@ def test_double_escape(parser): assert [event.key for event in events] == ["escape", "escape"] +@pytest.mark.parametrize( + "sequence,key", + [ + ("a", "a"), + ("B", "B"), + ("\x1ba", "alt+a"), + ("\x1b[97;3u", "alt+a"), + ("\x1b[65;4u", "alt+shift+a"), + ("\x1bA", "alt+shift+a"), + ("\x1b[120;7u", "alt+ctrl+x"), + ], +) +def test_keys(parser, sequence: str, key: str) -> None: + """Test rarer keys.""" + events = [] + for event in parser.feed(sequence): + events.append(event) + for event in parser.feed(""): + events.append(event) + event = events[0] + assert event.key == key + + @pytest.mark.parametrize( "sequence, event_type, shift, meta", [