Skip to content

Commit

Permalink
Merge pull request #4945 from Textualize/key-to-character
Browse files Browse the repository at this point in the history
Add key_to_character function
  • Loading branch information
willmcgugan authored Aug 28, 2024
2 parents 46a300e + f709f99 commit 1136f8d
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 72 deletions.
3 changes: 2 additions & 1 deletion src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions src/textual/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 3 additions & 2 deletions src/textual/widgets/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
8 changes: 6 additions & 2 deletions src/textual/widgets/_text_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Loading

0 comments on commit 1136f8d

Please sign in to comment.