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