ai-station/.venv/lib/python3.12/site-packages/textual/keys.py

369 lines
10 KiB
Python
Raw Normal View History

2025-12-25 14:54:33 +00:00
from __future__ import annotations
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
class Keys(str, Enum): # type: ignore[no-redef]
"""
List of keys for use in key bindings.
Note that this is an "StrEnum", all values can be compared against
strings.
"""
@property
def value(self) -> str:
return super().value
Escape = "escape" # Also Control-[
ShiftEscape = "shift+escape"
Return = "return"
ControlAt = "ctrl+@" # Also Control-Space.
ControlA = "ctrl+a"
ControlB = "ctrl+b"
ControlC = "ctrl+c"
ControlD = "ctrl+d"
ControlE = "ctrl+e"
ControlF = "ctrl+f"
ControlG = "ctrl+g"
ControlH = "ctrl+h"
ControlI = "ctrl+i" # Tab
ControlJ = "ctrl+j" # Newline
ControlK = "ctrl+k"
ControlL = "ctrl+l"
ControlM = "ctrl+m" # Carriage return
ControlN = "ctrl+n"
ControlO = "ctrl+o"
ControlP = "ctrl+p"
ControlQ = "ctrl+q"
ControlR = "ctrl+r"
ControlS = "ctrl+s"
ControlT = "ctrl+t"
ControlU = "ctrl+u"
ControlV = "ctrl+v"
ControlW = "ctrl+w"
ControlX = "ctrl+x"
ControlY = "ctrl+y"
ControlZ = "ctrl+z"
Control1 = "ctrl+1"
Control2 = "ctrl+2"
Control3 = "ctrl+3"
Control4 = "ctrl+4"
Control5 = "ctrl+5"
Control6 = "ctrl+6"
Control7 = "ctrl+7"
Control8 = "ctrl+8"
Control9 = "ctrl+9"
Control0 = "ctrl+0"
ControlShift1 = "ctrl+shift+1"
ControlShift2 = "ctrl+shift+2"
ControlShift3 = "ctrl+shift+3"
ControlShift4 = "ctrl+shift+4"
ControlShift5 = "ctrl+shift+5"
ControlShift6 = "ctrl+shift+6"
ControlShift7 = "ctrl+shift+7"
ControlShift8 = "ctrl+shift+8"
ControlShift9 = "ctrl+shift+9"
ControlShift0 = "ctrl+shift+0"
ControlBackslash = "ctrl+backslash"
ControlSquareClose = "ctrl+right_square_bracket"
ControlCircumflex = "ctrl+circumflex_accent"
ControlUnderscore = "ctrl+underscore"
Left = "left"
Right = "right"
Up = "up"
Down = "down"
Home = "home"
End = "end"
Insert = "insert"
Delete = "delete"
PageUp = "pageup"
PageDown = "pagedown"
ControlLeft = "ctrl+left"
ControlRight = "ctrl+right"
ControlUp = "ctrl+up"
ControlDown = "ctrl+down"
ControlHome = "ctrl+home"
ControlEnd = "ctrl+end"
ControlInsert = "ctrl+insert"
ControlDelete = "ctrl+delete"
ControlPageUp = "ctrl+pageup"
ControlPageDown = "ctrl+pagedown"
ShiftLeft = "shift+left"
ShiftRight = "shift+right"
ShiftUp = "shift+up"
ShiftDown = "shift+down"
ShiftHome = "shift+home"
ShiftEnd = "shift+end"
ShiftInsert = "shift+insert"
ShiftDelete = "shift+delete"
ShiftPageUp = "shift+pageup"
ShiftPageDown = "shift+pagedown"
ControlShiftLeft = "ctrl+shift+left"
ControlShiftRight = "ctrl+shift+right"
ControlShiftUp = "ctrl+shift+up"
ControlShiftDown = "ctrl+shift+down"
ControlShiftHome = "ctrl+shift+home"
ControlShiftEnd = "ctrl+shift+end"
ControlShiftInsert = "ctrl+shift+insert"
ControlShiftDelete = "ctrl+shift+delete"
ControlShiftPageUp = "ctrl+shift+pageup"
ControlShiftPageDown = "ctrl+shift+pagedown"
BackTab = "shift+tab" # shift + tab
F1 = "f1"
F2 = "f2"
F3 = "f3"
F4 = "f4"
F5 = "f5"
F6 = "f6"
F7 = "f7"
F8 = "f8"
F9 = "f9"
F10 = "f10"
F11 = "f11"
F12 = "f12"
F13 = "f13"
F14 = "f14"
F15 = "f15"
F16 = "f16"
F17 = "f17"
F18 = "f18"
F19 = "f19"
F20 = "f20"
F21 = "f21"
F22 = "f22"
F23 = "f23"
F24 = "f24"
ControlF1 = "ctrl+f1"
ControlF2 = "ctrl+f2"
ControlF3 = "ctrl+f3"
ControlF4 = "ctrl+f4"
ControlF5 = "ctrl+f5"
ControlF6 = "ctrl+f6"
ControlF7 = "ctrl+f7"
ControlF8 = "ctrl+f8"
ControlF9 = "ctrl+f9"
ControlF10 = "ctrl+f10"
ControlF11 = "ctrl+f11"
ControlF12 = "ctrl+f12"
ControlF13 = "ctrl+f13"
ControlF14 = "ctrl+f14"
ControlF15 = "ctrl+f15"
ControlF16 = "ctrl+f16"
ControlF17 = "ctrl+f17"
ControlF18 = "ctrl+f18"
ControlF19 = "ctrl+f19"
ControlF20 = "ctrl+f20"
ControlF21 = "ctrl+f21"
ControlF22 = "ctrl+f22"
ControlF23 = "ctrl+f23"
ControlF24 = "ctrl+f24"
# Matches any key.
Any = "<any>"
# Special.
ScrollUp = "<scroll-up>"
ScrollDown = "<scroll-down>"
# For internal use: key which is ignored.
# (The key binding for this key should not do anything.)
Ignore = "<ignore>"
# Some 'Key' aliases (for backwardshift+compatibility).
ControlSpace = "ctrl-at"
Tab = "tab"
Space = "space"
Enter = "enter"
Backspace = "backspace"
# ShiftControl was renamed to ControlShift in
# 888fcb6fa4efea0de8333177e1bbc792f3ff3c24 (20 Feb 2020).
ShiftControlLeft = ControlShiftLeft
ShiftControlRight = ControlShiftRight
ShiftControlHome = ControlShiftHome
ShiftControlEnd = ControlShiftEnd
# Unicode db contains some obscure names
# This mapping replaces them with more common terms
KEY_NAME_REPLACEMENTS = {
"solidus": "slash",
"reverse_solidus": "backslash",
"commercial_at": "at",
"hyphen_minus": "minus",
"plus_sign": "plus",
"low_line": "underscore",
}
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.
KEY_ALIASES = {
"tab": ["ctrl+i"],
"enter": ["ctrl+m"],
"escape": ["ctrl+left_square_brace"],
"ctrl+at": ["ctrl+space"],
"ctrl+j": ["newline"],
}
KEY_DISPLAY_ALIASES = {
"up": "",
"down": "",
"left": "",
"right": "",
"backspace": "",
"escape": "esc",
"enter": "",
"minus": "-",
"space": "space",
"pagedown": "pgdn",
"pageup": "pgup",
"delete": "del",
}
ASCII_KEY_NAMES = {"\t": "tab"}
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)
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, [])
@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
to this key (e.g. in the Footer widget)."""
display_alias = KEY_DISPLAY_ALIASES.get(key)
if display_alias:
return display_alias
original_key = REPLACED_KEYS.get(key, key)
tentative_unicode_name = _get_unicode_name_from_key(original_key)
try:
unicode_name = unicodedata.lookup(tentative_unicode_name)
except KeyError:
pass
else:
if unicode_name.isprintable():
return unicode_name
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.
This transformation can be undone by the function `_get_unicode_name_from_key`.
"""
if not character.isalnum():
try:
key = (
unicodedata.name(character).lower().replace("-", "_").replace(" ", "_")
)
except ValueError:
key = ASCII_KEY_NAMES.get(character, character)
else:
key = character
key = KEY_NAME_REPLACEMENTS.get(key, key)
return key
def _normalize_key_list(keys: str) -> str:
"""Normalizes a comma separated list of keys.
Replaces single letter keys with full name.
"""
keys_list = [key.strip() for key in keys.split(",")]
return ",".join(
_character_to_key(key) if len(key) == 1 else key for key in keys_list
)