From b6b76025d06c32dc847ffbac918127ab5a6819e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:21:19 +0000 Subject: [PATCH 1/5] Add regression test for #1815. --- tests/test_pilot.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/test_pilot.py diff --git a/tests/test_pilot.py b/tests/test_pilot.py new file mode 100644 index 000000000..2ca527895 --- /dev/null +++ b/tests/test_pilot.py @@ -0,0 +1,23 @@ +from string import punctuation + +from textual import events +from textual.app import App + +KEY_CHARACTERS_TO_TEST = "akTW03" + punctuation.replace("_", "") +"""Test some "simple" characters (letters + digits) and all punctuation. +Ignore the underscore because that is an alias to add a pause in the pilot. +""" + + +async def test_pilot_press_ascii_chars(): + """Test that the pilot can press most ASCII characters as keys.""" + keys_pressed = [] + + class TestApp(App[None]): + def on_key(self, event: events.Key) -> None: + keys_pressed.append(event.character) + + async with TestApp().run_test() as pilot: + for char in KEY_CHARACTERS_TO_TEST: + await pilot.press(char) + assert keys_pressed[-1] == char From 7acacf0746c7235b5f2daade6a1b9381a3cb52be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:31:55 +0000 Subject: [PATCH 2/5] Allow conversion to and from unicode names. Throughout the code base, there are a couple of places (noted in #1830) where we take a Unicode name and make it more friendly, or take a 'friendly' name and try to recover the original Unicode name. We add a function to go from 'friendly' to the Unicode name, which tackles the issue highlighted in #1815. Related issues: #1815, #1830. --- src/textual/keys.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/textual/keys.py b/src/textual/keys.py index 81e0aca38..64c26206a 100644 --- a/src/textual/keys.py +++ b/src/textual/keys.py @@ -209,6 +209,37 @@ KEY_NAME_REPLACEMENTS = { } REPLACED_KEYS = {value: key for key, value in KEY_NAME_REPLACEMENTS.items()} +# Convert the friendly versions of character key Unicode names +# back to their original names. +# This is because we go from Unicode to friendly by replacing spaces and dashes +# with underscores, which cannot be undone by replacing underscores with spaces/dashes. +KEY_TO_UNICODE_NAME = { + "exclamation_mark": "EXCLAMATION MARK", + "quotation_mark": "QUOTATION MARK", + "number_sign": "NUMBER SIGN", + "dollar_sign": "DOLLAR SIGN", + "percent_sign": "PERCENT SIGN", + "left_parenthesis": "LEFT PARENTHESIS", + "right_parenthesis": "RIGHT PARENTHESIS", + "plus_sign": "PLUS SIGN", + "hyphen_minus": "HYPHEN-MINUS", + "full_stop": "FULL STOP", + "less_than_sign": "LESS-THAN SIGN", + "equals_sign": "EQUALS SIGN", + "greater_than_sign": "GREATER-THAN SIGN", + "question_mark": "QUESTION MARK", + "commercial_at": "COMMERCIAL AT", + "left_square_bracket": "LEFT SQUARE BRACKET", + "reverse_solidus": "REVERSE SOLIDUS", + "right_square_bracket": "RIGHT SQUARE BRACKET", + "circumflex_accent": "CIRCUMFLEX ACCENT", + "low_line": "LOW LINE", + "grave_accent": "GRAVE ACCENT", + "left_curly_bracket": "LEFT CURLY BRACKET", + "vertical_line": "VERTICAL LINE", + "right_curly_bracket": "RIGHT CURLY BRACKET", +} + # Some keys have aliases. For example, if you press `ctrl+m` on your keyboard, # it's treated the same way as if you press `enter`. Key handlers `key_ctrl_m` and # `key_enter` are both valid in this case. @@ -233,6 +264,14 @@ KEY_DISPLAY_ALIASES = { } +def _get_unicode_name_from_key(key: str) -> str: + """Get the best guess for the Unicode name of the char corresponding to the key. + + This function can be seen as a pseudo-inverse of the function `_character_to_key`. + """ + return KEY_TO_UNICODE_NAME.get(key, key.upper()) + + def _get_key_aliases(key: str) -> list[str]: """Return all aliases for the given key, including the key itself""" return [key] + KEY_ALIASES.get(key, []) @@ -261,8 +300,10 @@ def _get_key_display(key: str) -> str: def _character_to_key(character: str) -> str: - """Convert a single character to a key value.""" - assert len(character) == 1 + """Convert a single character to a key value. + + This transformation can be undone by the function `_get_unicode_name_from_key`. + """ if not character.isalnum(): key = unicodedata.name(character).lower().replace("-", "_").replace(" ", "_") else: From b88e4b08b46f5b5401cd2fb2c7f1b3811f1cf0d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:32:23 +0000 Subject: [PATCH 3/5] Use keys.py utility functions consistently. --- src/textual/_xterm_parser.py | 9 ++------- src/textual/app.py | 19 ++++++++++--------- src/textual/keys.py | 8 ++++---- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 336da9af0..7e6da16fd 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -8,7 +8,7 @@ from . import events, messages from ._ansi_sequences import ANSI_SEQUENCES_KEYS from ._parser import Awaitable, Parser, TokenCallback from ._types import MessageTarget -from .keys import KEY_NAME_REPLACEMENTS +from .keys import KEY_NAME_REPLACEMENTS, _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 @@ -253,12 +253,7 @@ class XTermParser(Parser[events.Event]): elif len(sequence) == 1: try: if not sequence.isalnum(): - name = ( - _unicode_name(sequence) - .lower() - .replace("-", "_") - .replace(" ", "_") - ) + name = _character_to_key(sequence) else: name = sequence name = KEY_NAME_REPLACEMENTS.get(name, name) diff --git a/src/textual/app.py b/src/textual/app.py index e49812f01..215e35119 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -70,7 +70,12 @@ from .features import FeatureFlag, parse_features from .file_monitor import FileMonitor from .filter import LineFilter, Monochrome from .geometry import Offset, Region, Size -from .keys import REPLACED_KEYS, _get_key_display +from .keys import ( + REPLACED_KEYS, + _character_to_key, + _get_key_display, + _get_unicode_name_from_key, +) from .messages import CallbackType from .reactive import Reactive from .renderables.blank import Blank @@ -864,19 +869,15 @@ class App(Generic[ReturnType], DOMNode): await asyncio.sleep(float(wait_ms) / 1000) else: if len(key) == 1 and not key.isalnum(): - key = ( - unicodedata.name(key) - .lower() - .replace("-", "_") - .replace(" ", "_") - ) + key = _character_to_key(key) original_key = REPLACED_KEYS.get(key, key) + print(f"original key is {original_key}") char: str | None try: - char = unicodedata.lookup(original_key.upper().replace("_", " ")) + char = unicodedata.lookup(_get_unicode_name_from_key(original_key)) except KeyError: char = key if len(key) == 1 else None - print(f"press {key!r} (char={char!r})") + print(f"char is {char!r}") key_event = events.Key(app, key, char) driver.send_event(key_event) await wait_for_idle(0) diff --git a/src/textual/keys.py b/src/textual/keys.py index 64c26206a..38bc60a8e 100644 --- a/src/textual/keys.py +++ b/src/textual/keys.py @@ -286,17 +286,17 @@ def _get_key_display(key: str) -> str: return display_alias original_key = REPLACED_KEYS.get(key, key) - upper_original = original_key.upper().replace("_", " ") + tentative_unicode_name = _get_unicode_name_from_key(original_key) try: - unicode_character = unicodedata.lookup(upper_original) + unicode_character = unicodedata.lookup(tentative_unicode_name) except KeyError: - return upper_original + return tentative_unicode_name # Check if printable. `delete` for example maps to a control sequence # which we don't want to write to the terminal. if unicode_character.isprintable(): return unicode_character - return upper_original + return tentative_unicode_name def _character_to_key(character: str) -> str: From 571c2c3be5e948b2fdc114d96cc69770490c0b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:04:18 +0000 Subject: [PATCH 4/5] Restore print. --- src/textual/app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 215e35119..d47a73eef 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -871,13 +871,12 @@ class App(Generic[ReturnType], DOMNode): if len(key) == 1 and not key.isalnum(): key = _character_to_key(key) original_key = REPLACED_KEYS.get(key, key) - print(f"original key is {original_key}") char: str | None try: char = unicodedata.lookup(_get_unicode_name_from_key(original_key)) except KeyError: char = key if len(key) == 1 else None - print(f"char is {char!r}") + print(f"press {key!r} (char={char!r})") key_event = events.Key(app, key, char) driver.send_event(key_event) await wait_for_idle(0) From 6c27bcfc337216349e8a686b9ed382d4ec549bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:07:10 +0000 Subject: [PATCH 5/5] Update changelog. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76fb3117a..a6305e842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `switch` attribute to `Switch` events https://github.com/Textualize/textual/pull/1940 - Breaking change: Added `toggle_button` attribute to RadioButton and Checkbox events, replaces `input` https://github.com/Textualize/textual/pull/1940 +### Fixed + +- Fixed bug that prevented app pilot to press some keys https://github.com/Textualize/textual/issues/1815 + ## [0.13.0] - 2023-03-02 ### Added