diff --git a/src/textual/dom.py b/src/textual/dom.py index 786209295d..b75bbda542 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1026,7 +1026,7 @@ def rich_style(self) -> Style: ) return style - def check_consume_key(self, key: str) -> bool: + def check_consume_key(self, key: str, character: str | None) -> bool: """Check if the widget may consume the given key. This should be implemented in widgets that handle [`Key`][textual.events.Key] events and @@ -1037,6 +1037,7 @@ def check_consume_key(self, key: str) -> bool: Args: key: A key identifier. + character: A character associated with the key, or `None` if there isn't one. Returns: `True` if the widget may capture the key in its `Key` event handler, or `False` if it won't. diff --git a/src/textual/keys.py b/src/textual/keys.py index 762db093bc..22572e117f 100644 --- a/src/textual/keys.py +++ b/src/textual/keys.py @@ -2,6 +2,7 @@ import unicodedata from enum import Enum +from functools import lru_cache # Adapted from prompt toolkit https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/keys.py @@ -282,6 +283,7 @@ def _get_key_aliases(key: str) -> list[str]: return [key] + KEY_ALIASES.get(key, []) +@lru_cache(1024) def format_key(key: str) -> str: """Given a key (i.e. the `key` string argument to Binding __init__), return the value that should be displayed in the app when referring @@ -303,6 +305,36 @@ def format_key(key: str) -> str: return tentative_unicode_name +@lru_cache(1024) +def key_to_character(key: str) -> str | None: + """Given a key identifier, return the character associated with it. + + Args: + key: The key identifier. + + Returns: + A key if one could be found, otherwise `None`. + """ + _, separator, key = key.rpartition("+") + if separator: + # If there is a separator, then it means a modifier (other than shift) is applied. + # Keys with modifiers, don't come from printable keys. + return None + if len(key) == 1: + # Key identifiers with a length of one, are also characters. + return key + try: + return unicodedata.lookup(KEY_TO_UNICODE_NAME[key]) + except KeyError: + pass + try: + return unicodedata.lookup(key.replace("_", " ").upper()) + except KeyError: + pass + # Return None if we couldn't identify the key. + return None + + def _character_to_key(character: str) -> str: """Convert a single character to a key value. diff --git a/src/textual/screen.py b/src/textual/screen.py index f5c32eb934..fb3c2aeda2 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -29,6 +29,8 @@ from rich.console import RenderableType from rich.style import Style +from textual.keys import key_to_character + from . import constants, errors, events, messages from ._arrange import arrange from ._callback import invoke @@ -330,7 +332,7 @@ def _binding_chain(self) -> list[tuple[DOMNode, BindingsMap]]: for filter_namespace in filter_namespaces: check_consume_key = filter_namespace.check_consume_key for key in list(bindings_map.key_to_bindings): - if check_consume_key(key): + if check_consume_key(key, key_to_character(key)): del bindings_map.key_to_bindings[key] filter_namespaces.append(namespace) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 6b84335949..d0baff1e3b 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -361,18 +361,19 @@ def _cursor_at_end(self) -> bool: """Flag to indicate if the cursor is at the end""" return self.cursor_position >= len(self.value) - def check_consume_key(self, key: str) -> bool: + def check_consume_key(self, key: str, character: str | None) -> bool: """Check if the widget may consume the given key. As an input we are expecting to capture printable keys. Args: key: A key identifier. + character: A character associated with the key, or `None` if there isn't one. Returns: `True` if the widget may capture the key in it's `Key` message, or `False` if it won't. """ - return len(key) == 1 and key.isprintable() + return character is not None and character.isprintable() def validate_cursor_position(self, cursor_position: int) -> int: return min(max(0, cursor_position), len(self.value)) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 1b11e21331..283623d4c1 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -546,22 +546,26 @@ def _get_builtin_highlight_query(language_name: str) -> str: return highlight_query - def check_consume_key(self, key: str) -> bool: + def check_consume_key(self, key: str, character: str | None = None) -> bool: """Check if the widget may consume the given key. As a textarea we are expecting to capture printable keys. Args: key: A key identifier. + character: A character associated with the key, or `None` if there isn't one. Returns: `True` if the widget may capture the key in it's `Key` message, or `False` if it won't. """ if self.read_only: + # In read only mode we don't consume any key events return False if self.tab_behavior == "indent" and key == "tab": + # If tab_behavior is indent, then we consume the tab return True - return len(key) == 1 and key.isprintable() + # Otherwise we capture all printable keys + return character is not None and character.isprintable() def _build_highlight_map(self) -> None: """Query the tree for ranges to highlights, and update the internal highlights mapping.""" diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_check_consume_keys.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_check_consume_keys.svg index 335d6b325f..2f4cc24a62 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_check_consume_keys.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_check_consume_keys.svg @@ -19,141 +19,141 @@ font-weight: 700; } - .terminal-1756313272-matrix { + .terminal-2059904955-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1756313272-title { + .terminal-2059904955-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1756313272-r1 { fill: #1e1e1e } -.terminal-1756313272-r2 { fill: #0178d4 } -.terminal-1756313272-r3 { fill: #c5c8c6 } -.terminal-1756313272-r4 { fill: #787878 } -.terminal-1756313272-r5 { fill: #e2e2e2 } -.terminal-1756313272-r6 { fill: #121212 } -.terminal-1756313272-r7 { fill: #e1e1e1 } -.terminal-1756313272-r8 { fill: #fea62b;font-weight: bold } -.terminal-1756313272-r9 { fill: #a7a9ab } -.terminal-1756313272-r10 { fill: #e2e3e3 } -.terminal-1756313272-r11 { fill: #4c5055 } + .terminal-2059904955-r1 { fill: #1e1e1e } +.terminal-2059904955-r2 { fill: #0178d4 } +.terminal-2059904955-r3 { fill: #c5c8c6 } +.terminal-2059904955-r4 { fill: #787878 } +.terminal-2059904955-r5 { fill: #e2e2e2 } +.terminal-2059904955-r6 { fill: #121212 } +.terminal-2059904955-r7 { fill: #e1e1e1 } +.terminal-2059904955-r8 { fill: #e2e3e3 } +.terminal-2059904955-r9 { fill: #4c5055 } +.terminal-2059904955-r10 { fill: #fea62b;font-weight: bold } +.terminal-2059904955-r11 { fill: #a7a9ab } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -First Name -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Last Name -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - ? Show help screen ^p palette + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +First Name +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Last Name +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + +^p palette diff --git a/tests/test_keys.py b/tests/test_keys.py index f64c70527a..e9de532759 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -2,7 +2,7 @@ from textual.app import App from textual.binding import Binding -from textual.keys import _character_to_key, format_key +from textual.keys import _character_to_key, format_key, key_to_character @pytest.mark.parametrize( @@ -67,3 +67,12 @@ def test_get_key_display(): == "shift+^]" ) assert app.get_key_display(Binding("delete", "", "")) == "del" + + +def test_key_to_character(): + assert key_to_character("f") == "f" + assert key_to_character("F") == "F" + assert key_to_character("space") == " " + assert key_to_character("ctrl+space") is None + assert key_to_character("question_mark") == "?" + assert key_to_character("foo") is None