Merge pull request #1926 from Textualize/fix-1815

Minor `keys.py` tidy up.
This commit is contained in:
Rodrigo Girão Serrão
2023-03-06 11:14:20 +00:00
committed by GitHub
5 changed files with 84 additions and 21 deletions

View File

@@ -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

View File

@@ -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
@@ -241,12 +241,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)

View File

@@ -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
@@ -865,16 +870,11 @@ 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)
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})")

View File

@@ -211,6 +211,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.
@@ -235,6 +266,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, [])
@@ -249,22 +288,24 @@ 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:
"""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:

23
tests/test_pilot.py Normal file
View File

@@ -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