We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Binding for `]` renders incorrectly in `Footer`
The binding `Binding("ctrl+right_square_bracket", "toggle_indent_width", "Cycle indent width")` renders like this:

<img width="431" alt="image" src="https://github.com/Textualize/textual/assets/5740731/2c2bd6fa-288b-4205-aba0-48eb1b6c41e0">

It should probably render as `Ctrl+]`.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/textual/keys.py`
Content:
```
1 from __future__ import annotations
2 
3 import unicodedata
4 from enum import Enum
5 
6 
7 # Adapted from prompt toolkit https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/keys.py
8 class Keys(str, Enum):  # type: ignore[no-redef]
9     """
10     List of keys for use in key bindings.
11 
12     Note that this is an "StrEnum", all values can be compared against
13     strings.
14     """
15 
16     @property
17     def value(self) -> str:
18         return super().value
19 
20     Escape = "escape"  # Also Control-[
21     ShiftEscape = "shift+escape"
22     Return = "return"
23 
24     ControlAt = "ctrl+@"  # Also Control-Space.
25 
26     ControlA = "ctrl+a"
27     ControlB = "ctrl+b"
28     ControlC = "ctrl+c"
29     ControlD = "ctrl+d"
30     ControlE = "ctrl+e"
31     ControlF = "ctrl+f"
32     ControlG = "ctrl+g"
33     ControlH = "ctrl+h"
34     ControlI = "ctrl+i"  # Tab
35     ControlJ = "ctrl+j"  # Newline
36     ControlK = "ctrl+k"
37     ControlL = "ctrl+l"
38     ControlM = "ctrl+m"  # Carriage return
39     ControlN = "ctrl+n"
40     ControlO = "ctrl+o"
41     ControlP = "ctrl+p"
42     ControlQ = "ctrl+q"
43     ControlR = "ctrl+r"
44     ControlS = "ctrl+s"
45     ControlT = "ctrl+t"
46     ControlU = "ctrl+u"
47     ControlV = "ctrl+v"
48     ControlW = "ctrl+w"
49     ControlX = "ctrl+x"
50     ControlY = "ctrl+y"
51     ControlZ = "ctrl+z"
52 
53     Control1 = "ctrl+1"
54     Control2 = "ctrl+2"
55     Control3 = "ctrl+3"
56     Control4 = "ctrl+4"
57     Control5 = "ctrl+5"
58     Control6 = "ctrl+6"
59     Control7 = "ctrl+7"
60     Control8 = "ctrl+8"
61     Control9 = "ctrl+9"
62     Control0 = "ctrl+0"
63 
64     ControlShift1 = "ctrl+shift+1"
65     ControlShift2 = "ctrl+shift+2"
66     ControlShift3 = "ctrl+shift+3"
67     ControlShift4 = "ctrl+shift+4"
68     ControlShift5 = "ctrl+shift+5"
69     ControlShift6 = "ctrl+shift+6"
70     ControlShift7 = "ctrl+shift+7"
71     ControlShift8 = "ctrl+shift+8"
72     ControlShift9 = "ctrl+shift+9"
73     ControlShift0 = "ctrl+shift+0"
74 
75     ControlBackslash = "ctrl+backslash"
76     ControlSquareClose = "ctrl+right_square_bracket"
77     ControlCircumflex = "ctrl+circumflex_accent"
78     ControlUnderscore = "ctrl+underscore"
79 
80     Left = "left"
81     Right = "right"
82     Up = "up"
83     Down = "down"
84     Home = "home"
85     End = "end"
86     Insert = "insert"
87     Delete = "delete"
88     PageUp = "pageup"
89     PageDown = "pagedown"
90 
91     ControlLeft = "ctrl+left"
92     ControlRight = "ctrl+right"
93     ControlUp = "ctrl+up"
94     ControlDown = "ctrl+down"
95     ControlHome = "ctrl+home"
96     ControlEnd = "ctrl+end"
97     ControlInsert = "ctrl+insert"
98     ControlDelete = "ctrl+delete"
99     ControlPageUp = "ctrl+pageup"
100     ControlPageDown = "ctrl+pagedown"
101 
102     ShiftLeft = "shift+left"
103     ShiftRight = "shift+right"
104     ShiftUp = "shift+up"
105     ShiftDown = "shift+down"
106     ShiftHome = "shift+home"
107     ShiftEnd = "shift+end"
108     ShiftInsert = "shift+insert"
109     ShiftDelete = "shift+delete"
110     ShiftPageUp = "shift+pageup"
111     ShiftPageDown = "shift+pagedown"
112 
113     ControlShiftLeft = "ctrl+shift+left"
114     ControlShiftRight = "ctrl+shift+right"
115     ControlShiftUp = "ctrl+shift+up"
116     ControlShiftDown = "ctrl+shift+down"
117     ControlShiftHome = "ctrl+shift+home"
118     ControlShiftEnd = "ctrl+shift+end"
119     ControlShiftInsert = "ctrl+shift+insert"
120     ControlShiftDelete = "ctrl+shift+delete"
121     ControlShiftPageUp = "ctrl+shift+pageup"
122     ControlShiftPageDown = "ctrl+shift+pagedown"
123 
124     BackTab = "shift+tab"  # shift + tab
125 
126     F1 = "f1"
127     F2 = "f2"
128     F3 = "f3"
129     F4 = "f4"
130     F5 = "f5"
131     F6 = "f6"
132     F7 = "f7"
133     F8 = "f8"
134     F9 = "f9"
135     F10 = "f10"
136     F11 = "f11"
137     F12 = "f12"
138     F13 = "f13"
139     F14 = "f14"
140     F15 = "f15"
141     F16 = "f16"
142     F17 = "f17"
143     F18 = "f18"
144     F19 = "f19"
145     F20 = "f20"
146     F21 = "f21"
147     F22 = "f22"
148     F23 = "f23"
149     F24 = "f24"
150 
151     ControlF1 = "ctrl+f1"
152     ControlF2 = "ctrl+f2"
153     ControlF3 = "ctrl+f3"
154     ControlF4 = "ctrl+f4"
155     ControlF5 = "ctrl+f5"
156     ControlF6 = "ctrl+f6"
157     ControlF7 = "ctrl+f7"
158     ControlF8 = "ctrl+f8"
159     ControlF9 = "ctrl+f9"
160     ControlF10 = "ctrl+f10"
161     ControlF11 = "ctrl+f11"
162     ControlF12 = "ctrl+f12"
163     ControlF13 = "ctrl+f13"
164     ControlF14 = "ctrl+f14"
165     ControlF15 = "ctrl+f15"
166     ControlF16 = "ctrl+f16"
167     ControlF17 = "ctrl+f17"
168     ControlF18 = "ctrl+f18"
169     ControlF19 = "ctrl+f19"
170     ControlF20 = "ctrl+f20"
171     ControlF21 = "ctrl+f21"
172     ControlF22 = "ctrl+f22"
173     ControlF23 = "ctrl+f23"
174     ControlF24 = "ctrl+f24"
175 
176     # Matches any key.
177     Any = "<any>"
178 
179     # Special.
180     ScrollUp = "<scroll-up>"
181     ScrollDown = "<scroll-down>"
182 
183     # For internal use: key which is ignored.
184     # (The key binding for this key should not do anything.)
185     Ignore = "<ignore>"
186 
187     # Some 'Key' aliases (for backwardshift+compatibility).
188     ControlSpace = "ctrl-at"
189     Tab = "tab"
190     Space = "space"
191     Enter = "enter"
192     Backspace = "backspace"
193 
194     # ShiftControl was renamed to ControlShift in
195     # 888fcb6fa4efea0de8333177e1bbc792f3ff3c24 (20 Feb 2020).
196     ShiftControlLeft = ControlShiftLeft
197     ShiftControlRight = ControlShiftRight
198     ShiftControlHome = ControlShiftHome
199     ShiftControlEnd = ControlShiftEnd
200 
201 
202 # Unicode db contains some obscure names
203 # This mapping replaces them with more common terms
204 KEY_NAME_REPLACEMENTS = {
205     "solidus": "slash",
206     "reverse_solidus": "backslash",
207     "commercial_at": "at",
208     "hyphen_minus": "minus",
209     "plus_sign": "plus",
210     "low_line": "underscore",
211 }
212 REPLACED_KEYS = {value: key for key, value in KEY_NAME_REPLACEMENTS.items()}
213 
214 # Convert the friendly versions of character key Unicode names
215 # back to their original names.
216 # This is because we go from Unicode to friendly by replacing spaces and dashes
217 # with underscores, which cannot be undone by replacing underscores with spaces/dashes.
218 KEY_TO_UNICODE_NAME = {
219     "exclamation_mark": "EXCLAMATION MARK",
220     "quotation_mark": "QUOTATION MARK",
221     "number_sign": "NUMBER SIGN",
222     "dollar_sign": "DOLLAR SIGN",
223     "percent_sign": "PERCENT SIGN",
224     "left_parenthesis": "LEFT PARENTHESIS",
225     "right_parenthesis": "RIGHT PARENTHESIS",
226     "plus_sign": "PLUS SIGN",
227     "hyphen_minus": "HYPHEN-MINUS",
228     "full_stop": "FULL STOP",
229     "less_than_sign": "LESS-THAN SIGN",
230     "equals_sign": "EQUALS SIGN",
231     "greater_than_sign": "GREATER-THAN SIGN",
232     "question_mark": "QUESTION MARK",
233     "commercial_at": "COMMERCIAL AT",
234     "left_square_bracket": "LEFT SQUARE BRACKET",
235     "reverse_solidus": "REVERSE SOLIDUS",
236     "right_square_bracket": "RIGHT SQUARE BRACKET",
237     "circumflex_accent": "CIRCUMFLEX ACCENT",
238     "low_line": "LOW LINE",
239     "grave_accent": "GRAVE ACCENT",
240     "left_curly_bracket": "LEFT CURLY BRACKET",
241     "vertical_line": "VERTICAL LINE",
242     "right_curly_bracket": "RIGHT CURLY BRACKET",
243 }
244 
245 # Some keys have aliases. For example, if you press `ctrl+m` on your keyboard,
246 # it's treated the same way as if you press `enter`. Key handlers `key_ctrl_m` and
247 # `key_enter` are both valid in this case.
248 KEY_ALIASES = {
249     "tab": ["ctrl+i"],
250     "enter": ["ctrl+m"],
251     "escape": ["ctrl+left_square_brace"],
252     "ctrl+at": ["ctrl+space"],
253     "ctrl+j": ["newline"],
254 }
255 
256 KEY_DISPLAY_ALIASES = {
257     "up": "↑",
258     "down": "↓",
259     "left": "←",
260     "right": "→",
261     "backspace": "⌫",
262     "escape": "ESC",
263     "enter": "⏎",
264     "minus": "-",
265     "space": "SPACE",
266 }
267 
268 
269 def _get_unicode_name_from_key(key: str) -> str:
270     """Get the best guess for the Unicode name of the char corresponding to the key.
271 
272     This function can be seen as a pseudo-inverse of the function `_character_to_key`.
273     """
274     return KEY_TO_UNICODE_NAME.get(key, key.upper())
275 
276 
277 def _get_key_aliases(key: str) -> list[str]:
278     """Return all aliases for the given key, including the key itself"""
279     return [key] + KEY_ALIASES.get(key, [])
280 
281 
282 def _get_key_display(key: str) -> str:
283     """Given a key (i.e. the `key` string argument to Binding __init__),
284     return the value that should be displayed in the app when referring
285     to this key (e.g. in the Footer widget)."""
286     display_alias = KEY_DISPLAY_ALIASES.get(key)
287     if display_alias:
288         return display_alias
289 
290     original_key = REPLACED_KEYS.get(key, key)
291     tentative_unicode_name = _get_unicode_name_from_key(original_key)
292     try:
293         unicode_character = unicodedata.lookup(tentative_unicode_name)
294     except KeyError:
295         return tentative_unicode_name
296 
297     # Check if printable. `delete` for example maps to a control sequence
298     # which we don't want to write to the terminal.
299     if unicode_character.isprintable():
300         return unicode_character
301     return tentative_unicode_name
302 
303 
304 def _character_to_key(character: str) -> str:
305     """Convert a single character to a key value.
306 
307     This transformation can be undone by the function `_get_unicode_name_from_key`.
308     """
309     if not character.isalnum():
310         key = unicodedata.name(character).lower().replace("-", "_").replace(" ", "_")
311     else:
312         key = character
313     key = KEY_NAME_REPLACEMENTS.get(key, key)
314     return key
315
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/textual/keys.py b/src/textual/keys.py
--- a/src/textual/keys.py
+++ b/src/textual/keys.py
@@ -283,6 +283,9 @@
     """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)."""
+    if "+" in key:
+        return "+".join([_get_key_display(key) for key in key.split("+")])
+
     display_alias = KEY_DISPLAY_ALIASES.get(key)
     if display_alias:
         return display_alias

{"golden_diff": "diff --git a/src/textual/keys.py b/src/textual/keys.py\n--- a/src/textual/keys.py\n+++ b/src/textual/keys.py\n@@ -283,6 +283,9 @@\n     \"\"\"Given a key (i.e. the `key` string argument to Binding __init__),\n     return the value that should be displayed in the app when referring\n     to this key (e.g. in the Footer widget).\"\"\"\n+    if \"+\" in key:\n+        return \"+\".join([_get_key_display(key) for key in key.split(\"+\")])\n+\n     display_alias = KEY_DISPLAY_ALIASES.get(key)\n     if display_alias:\n         return display_alias\n", "issue": "Binding for `]` renders incorrectly in `Footer`\nThe binding `Binding(\"ctrl+right_square_bracket\", \"toggle_indent_width\", \"Cycle indent width\")` renders like this:\r\n\r\n<img width=\"431\" alt=\"image\" src=\"https://github.com/Textualize/textual/assets/5740731/2c2bd6fa-288b-4205-aba0-48eb1b6c41e0\">\r\n\r\nIt should probably render as `Ctrl+]`.\n", "before_files": [{"content": "from __future__ import annotations\n\nimport unicodedata\nfrom enum import Enum\n\n\n# Adapted from prompt toolkit https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/keys.py\nclass Keys(str, Enum):  # type: ignore[no-redef]\n    \"\"\"\n    List of keys for use in key bindings.\n\n    Note that this is an \"StrEnum\", all values can be compared against\n    strings.\n    \"\"\"\n\n    @property\n    def value(self) -> str:\n        return super().value\n\n    Escape = \"escape\"  # Also Control-[\n    ShiftEscape = \"shift+escape\"\n    Return = \"return\"\n\n    ControlAt = \"ctrl+@\"  # Also Control-Space.\n\n    ControlA = \"ctrl+a\"\n    ControlB = \"ctrl+b\"\n    ControlC = \"ctrl+c\"\n    ControlD = \"ctrl+d\"\n    ControlE = \"ctrl+e\"\n    ControlF = \"ctrl+f\"\n    ControlG = \"ctrl+g\"\n    ControlH = \"ctrl+h\"\n    ControlI = \"ctrl+i\"  # Tab\n    ControlJ = \"ctrl+j\"  # Newline\n    ControlK = \"ctrl+k\"\n    ControlL = \"ctrl+l\"\n    ControlM = \"ctrl+m\"  # Carriage return\n    ControlN = \"ctrl+n\"\n    ControlO = \"ctrl+o\"\n    ControlP = \"ctrl+p\"\n    ControlQ = \"ctrl+q\"\n    ControlR = \"ctrl+r\"\n    ControlS = \"ctrl+s\"\n    ControlT = \"ctrl+t\"\n    ControlU = \"ctrl+u\"\n    ControlV = \"ctrl+v\"\n    ControlW = \"ctrl+w\"\n    ControlX = \"ctrl+x\"\n    ControlY = \"ctrl+y\"\n    ControlZ = \"ctrl+z\"\n\n    Control1 = \"ctrl+1\"\n    Control2 = \"ctrl+2\"\n    Control3 = \"ctrl+3\"\n    Control4 = \"ctrl+4\"\n    Control5 = \"ctrl+5\"\n    Control6 = \"ctrl+6\"\n    Control7 = \"ctrl+7\"\n    Control8 = \"ctrl+8\"\n    Control9 = \"ctrl+9\"\n    Control0 = \"ctrl+0\"\n\n    ControlShift1 = \"ctrl+shift+1\"\n    ControlShift2 = \"ctrl+shift+2\"\n    ControlShift3 = \"ctrl+shift+3\"\n    ControlShift4 = \"ctrl+shift+4\"\n    ControlShift5 = \"ctrl+shift+5\"\n    ControlShift6 = \"ctrl+shift+6\"\n    ControlShift7 = \"ctrl+shift+7\"\n    ControlShift8 = \"ctrl+shift+8\"\n    ControlShift9 = \"ctrl+shift+9\"\n    ControlShift0 = \"ctrl+shift+0\"\n\n    ControlBackslash = \"ctrl+backslash\"\n    ControlSquareClose = \"ctrl+right_square_bracket\"\n    ControlCircumflex = \"ctrl+circumflex_accent\"\n    ControlUnderscore = \"ctrl+underscore\"\n\n    Left = \"left\"\n    Right = \"right\"\n    Up = \"up\"\n    Down = \"down\"\n    Home = \"home\"\n    End = \"end\"\n    Insert = \"insert\"\n    Delete = \"delete\"\n    PageUp = \"pageup\"\n    PageDown = \"pagedown\"\n\n    ControlLeft = \"ctrl+left\"\n    ControlRight = \"ctrl+right\"\n    ControlUp = \"ctrl+up\"\n    ControlDown = \"ctrl+down\"\n    ControlHome = \"ctrl+home\"\n    ControlEnd = \"ctrl+end\"\n    ControlInsert = \"ctrl+insert\"\n    ControlDelete = \"ctrl+delete\"\n    ControlPageUp = \"ctrl+pageup\"\n    ControlPageDown = \"ctrl+pagedown\"\n\n    ShiftLeft = \"shift+left\"\n    ShiftRight = \"shift+right\"\n    ShiftUp = \"shift+up\"\n    ShiftDown = \"shift+down\"\n    ShiftHome = \"shift+home\"\n    ShiftEnd = \"shift+end\"\n    ShiftInsert = \"shift+insert\"\n    ShiftDelete = \"shift+delete\"\n    ShiftPageUp = \"shift+pageup\"\n    ShiftPageDown = \"shift+pagedown\"\n\n    ControlShiftLeft = \"ctrl+shift+left\"\n    ControlShiftRight = \"ctrl+shift+right\"\n    ControlShiftUp = \"ctrl+shift+up\"\n    ControlShiftDown = \"ctrl+shift+down\"\n    ControlShiftHome = \"ctrl+shift+home\"\n    ControlShiftEnd = \"ctrl+shift+end\"\n    ControlShiftInsert = \"ctrl+shift+insert\"\n    ControlShiftDelete = \"ctrl+shift+delete\"\n    ControlShiftPageUp = \"ctrl+shift+pageup\"\n    ControlShiftPageDown = \"ctrl+shift+pagedown\"\n\n    BackTab = \"shift+tab\"  # shift + tab\n\n    F1 = \"f1\"\n    F2 = \"f2\"\n    F3 = \"f3\"\n    F4 = \"f4\"\n    F5 = \"f5\"\n    F6 = \"f6\"\n    F7 = \"f7\"\n    F8 = \"f8\"\n    F9 = \"f9\"\n    F10 = \"f10\"\n    F11 = \"f11\"\n    F12 = \"f12\"\n    F13 = \"f13\"\n    F14 = \"f14\"\n    F15 = \"f15\"\n    F16 = \"f16\"\n    F17 = \"f17\"\n    F18 = \"f18\"\n    F19 = \"f19\"\n    F20 = \"f20\"\n    F21 = \"f21\"\n    F22 = \"f22\"\n    F23 = \"f23\"\n    F24 = \"f24\"\n\n    ControlF1 = \"ctrl+f1\"\n    ControlF2 = \"ctrl+f2\"\n    ControlF3 = \"ctrl+f3\"\n    ControlF4 = \"ctrl+f4\"\n    ControlF5 = \"ctrl+f5\"\n    ControlF6 = \"ctrl+f6\"\n    ControlF7 = \"ctrl+f7\"\n    ControlF8 = \"ctrl+f8\"\n    ControlF9 = \"ctrl+f9\"\n    ControlF10 = \"ctrl+f10\"\n    ControlF11 = \"ctrl+f11\"\n    ControlF12 = \"ctrl+f12\"\n    ControlF13 = \"ctrl+f13\"\n    ControlF14 = \"ctrl+f14\"\n    ControlF15 = \"ctrl+f15\"\n    ControlF16 = \"ctrl+f16\"\n    ControlF17 = \"ctrl+f17\"\n    ControlF18 = \"ctrl+f18\"\n    ControlF19 = \"ctrl+f19\"\n    ControlF20 = \"ctrl+f20\"\n    ControlF21 = \"ctrl+f21\"\n    ControlF22 = \"ctrl+f22\"\n    ControlF23 = \"ctrl+f23\"\n    ControlF24 = \"ctrl+f24\"\n\n    # Matches any key.\n    Any = \"<any>\"\n\n    # Special.\n    ScrollUp = \"<scroll-up>\"\n    ScrollDown = \"<scroll-down>\"\n\n    # For internal use: key which is ignored.\n    # (The key binding for this key should not do anything.)\n    Ignore = \"<ignore>\"\n\n    # Some 'Key' aliases (for backwardshift+compatibility).\n    ControlSpace = \"ctrl-at\"\n    Tab = \"tab\"\n    Space = \"space\"\n    Enter = \"enter\"\n    Backspace = \"backspace\"\n\n    # ShiftControl was renamed to ControlShift in\n    # 888fcb6fa4efea0de8333177e1bbc792f3ff3c24 (20 Feb 2020).\n    ShiftControlLeft = ControlShiftLeft\n    ShiftControlRight = ControlShiftRight\n    ShiftControlHome = ControlShiftHome\n    ShiftControlEnd = ControlShiftEnd\n\n\n# Unicode db contains some obscure names\n# This mapping replaces them with more common terms\nKEY_NAME_REPLACEMENTS = {\n    \"solidus\": \"slash\",\n    \"reverse_solidus\": \"backslash\",\n    \"commercial_at\": \"at\",\n    \"hyphen_minus\": \"minus\",\n    \"plus_sign\": \"plus\",\n    \"low_line\": \"underscore\",\n}\nREPLACED_KEYS = {value: key for key, value in KEY_NAME_REPLACEMENTS.items()}\n\n# Convert the friendly versions of character key Unicode names\n# back to their original names.\n# This is because we go from Unicode to friendly by replacing spaces and dashes\n# with underscores, which cannot be undone by replacing underscores with spaces/dashes.\nKEY_TO_UNICODE_NAME = {\n    \"exclamation_mark\": \"EXCLAMATION MARK\",\n    \"quotation_mark\": \"QUOTATION MARK\",\n    \"number_sign\": \"NUMBER SIGN\",\n    \"dollar_sign\": \"DOLLAR SIGN\",\n    \"percent_sign\": \"PERCENT SIGN\",\n    \"left_parenthesis\": \"LEFT PARENTHESIS\",\n    \"right_parenthesis\": \"RIGHT PARENTHESIS\",\n    \"plus_sign\": \"PLUS SIGN\",\n    \"hyphen_minus\": \"HYPHEN-MINUS\",\n    \"full_stop\": \"FULL STOP\",\n    \"less_than_sign\": \"LESS-THAN SIGN\",\n    \"equals_sign\": \"EQUALS SIGN\",\n    \"greater_than_sign\": \"GREATER-THAN SIGN\",\n    \"question_mark\": \"QUESTION MARK\",\n    \"commercial_at\": \"COMMERCIAL AT\",\n    \"left_square_bracket\": \"LEFT SQUARE BRACKET\",\n    \"reverse_solidus\": \"REVERSE SOLIDUS\",\n    \"right_square_bracket\": \"RIGHT SQUARE BRACKET\",\n    \"circumflex_accent\": \"CIRCUMFLEX ACCENT\",\n    \"low_line\": \"LOW LINE\",\n    \"grave_accent\": \"GRAVE ACCENT\",\n    \"left_curly_bracket\": \"LEFT CURLY BRACKET\",\n    \"vertical_line\": \"VERTICAL LINE\",\n    \"right_curly_bracket\": \"RIGHT CURLY BRACKET\",\n}\n\n# Some keys have aliases. For example, if you press `ctrl+m` on your keyboard,\n# it's treated the same way as if you press `enter`. Key handlers `key_ctrl_m` and\n# `key_enter` are both valid in this case.\nKEY_ALIASES = {\n    \"tab\": [\"ctrl+i\"],\n    \"enter\": [\"ctrl+m\"],\n    \"escape\": [\"ctrl+left_square_brace\"],\n    \"ctrl+at\": [\"ctrl+space\"],\n    \"ctrl+j\": [\"newline\"],\n}\n\nKEY_DISPLAY_ALIASES = {\n    \"up\": \"\u2191\",\n    \"down\": \"\u2193\",\n    \"left\": \"\u2190\",\n    \"right\": \"\u2192\",\n    \"backspace\": \"\u232b\",\n    \"escape\": \"ESC\",\n    \"enter\": \"\u23ce\",\n    \"minus\": \"-\",\n    \"space\": \"SPACE\",\n}\n\n\ndef _get_unicode_name_from_key(key: str) -> str:\n    \"\"\"Get the best guess for the Unicode name of the char corresponding to the key.\n\n    This function can be seen as a pseudo-inverse of the function `_character_to_key`.\n    \"\"\"\n    return KEY_TO_UNICODE_NAME.get(key, key.upper())\n\n\ndef _get_key_aliases(key: str) -> list[str]:\n    \"\"\"Return all aliases for the given key, including the key itself\"\"\"\n    return [key] + KEY_ALIASES.get(key, [])\n\n\ndef _get_key_display(key: str) -> str:\n    \"\"\"Given a key (i.e. the `key` string argument to Binding __init__),\n    return the value that should be displayed in the app when referring\n    to this key (e.g. in the Footer widget).\"\"\"\n    display_alias = KEY_DISPLAY_ALIASES.get(key)\n    if display_alias:\n        return display_alias\n\n    original_key = REPLACED_KEYS.get(key, key)\n    tentative_unicode_name = _get_unicode_name_from_key(original_key)\n    try:\n        unicode_character = unicodedata.lookup(tentative_unicode_name)\n    except KeyError:\n        return tentative_unicode_name\n\n    # Check if printable. `delete` for example maps to a control sequence\n    # which we don't want to write to the terminal.\n    if unicode_character.isprintable():\n        return unicode_character\n    return tentative_unicode_name\n\n\ndef _character_to_key(character: str) -> str:\n    \"\"\"Convert a single character to a key value.\n\n    This transformation can be undone by the function `_get_unicode_name_from_key`.\n    \"\"\"\n    if not character.isalnum():\n        key = unicodedata.name(character).lower().replace(\"-\", \"_\").replace(\" \", \"_\")\n    else:\n        key = character\n    key = KEY_NAME_REPLACEMENTS.get(key, key)\n    return key\n", "path": "src/textual/keys.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport unicodedata\nfrom enum import Enum\n\n\n# Adapted from prompt toolkit https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/keys.py\nclass Keys(str, Enum):  # type: ignore[no-redef]\n    \"\"\"\n    List of keys for use in key bindings.\n\n    Note that this is an \"StrEnum\", all values can be compared against\n    strings.\n    \"\"\"\n\n    @property\n    def value(self) -> str:\n        return super().value\n\n    Escape = \"escape\"  # Also Control-[\n    ShiftEscape = \"shift+escape\"\n    Return = \"return\"\n\n    ControlAt = \"ctrl+@\"  # Also Control-Space.\n\n    ControlA = \"ctrl+a\"\n    ControlB = \"ctrl+b\"\n    ControlC = \"ctrl+c\"\n    ControlD = \"ctrl+d\"\n    ControlE = \"ctrl+e\"\n    ControlF = \"ctrl+f\"\n    ControlG = \"ctrl+g\"\n    ControlH = \"ctrl+h\"\n    ControlI = \"ctrl+i\"  # Tab\n    ControlJ = \"ctrl+j\"  # Newline\n    ControlK = \"ctrl+k\"\n    ControlL = \"ctrl+l\"\n    ControlM = \"ctrl+m\"  # Carriage return\n    ControlN = \"ctrl+n\"\n    ControlO = \"ctrl+o\"\n    ControlP = \"ctrl+p\"\n    ControlQ = \"ctrl+q\"\n    ControlR = \"ctrl+r\"\n    ControlS = \"ctrl+s\"\n    ControlT = \"ctrl+t\"\n    ControlU = \"ctrl+u\"\n    ControlV = \"ctrl+v\"\n    ControlW = \"ctrl+w\"\n    ControlX = \"ctrl+x\"\n    ControlY = \"ctrl+y\"\n    ControlZ = \"ctrl+z\"\n\n    Control1 = \"ctrl+1\"\n    Control2 = \"ctrl+2\"\n    Control3 = \"ctrl+3\"\n    Control4 = \"ctrl+4\"\n    Control5 = \"ctrl+5\"\n    Control6 = \"ctrl+6\"\n    Control7 = \"ctrl+7\"\n    Control8 = \"ctrl+8\"\n    Control9 = \"ctrl+9\"\n    Control0 = \"ctrl+0\"\n\n    ControlShift1 = \"ctrl+shift+1\"\n    ControlShift2 = \"ctrl+shift+2\"\n    ControlShift3 = \"ctrl+shift+3\"\n    ControlShift4 = \"ctrl+shift+4\"\n    ControlShift5 = \"ctrl+shift+5\"\n    ControlShift6 = \"ctrl+shift+6\"\n    ControlShift7 = \"ctrl+shift+7\"\n    ControlShift8 = \"ctrl+shift+8\"\n    ControlShift9 = \"ctrl+shift+9\"\n    ControlShift0 = \"ctrl+shift+0\"\n\n    ControlBackslash = \"ctrl+backslash\"\n    ControlSquareClose = \"ctrl+right_square_bracket\"\n    ControlCircumflex = \"ctrl+circumflex_accent\"\n    ControlUnderscore = \"ctrl+underscore\"\n\n    Left = \"left\"\n    Right = \"right\"\n    Up = \"up\"\n    Down = \"down\"\n    Home = \"home\"\n    End = \"end\"\n    Insert = \"insert\"\n    Delete = \"delete\"\n    PageUp = \"pageup\"\n    PageDown = \"pagedown\"\n\n    ControlLeft = \"ctrl+left\"\n    ControlRight = \"ctrl+right\"\n    ControlUp = \"ctrl+up\"\n    ControlDown = \"ctrl+down\"\n    ControlHome = \"ctrl+home\"\n    ControlEnd = \"ctrl+end\"\n    ControlInsert = \"ctrl+insert\"\n    ControlDelete = \"ctrl+delete\"\n    ControlPageUp = \"ctrl+pageup\"\n    ControlPageDown = \"ctrl+pagedown\"\n\n    ShiftLeft = \"shift+left\"\n    ShiftRight = \"shift+right\"\n    ShiftUp = \"shift+up\"\n    ShiftDown = \"shift+down\"\n    ShiftHome = \"shift+home\"\n    ShiftEnd = \"shift+end\"\n    ShiftInsert = \"shift+insert\"\n    ShiftDelete = \"shift+delete\"\n    ShiftPageUp = \"shift+pageup\"\n    ShiftPageDown = \"shift+pagedown\"\n\n    ControlShiftLeft = \"ctrl+shift+left\"\n    ControlShiftRight = \"ctrl+shift+right\"\n    ControlShiftUp = \"ctrl+shift+up\"\n    ControlShiftDown = \"ctrl+shift+down\"\n    ControlShiftHome = \"ctrl+shift+home\"\n    ControlShiftEnd = \"ctrl+shift+end\"\n    ControlShiftInsert = \"ctrl+shift+insert\"\n    ControlShiftDelete = \"ctrl+shift+delete\"\n    ControlShiftPageUp = \"ctrl+shift+pageup\"\n    ControlShiftPageDown = \"ctrl+shift+pagedown\"\n\n    BackTab = \"shift+tab\"  # shift + tab\n\n    F1 = \"f1\"\n    F2 = \"f2\"\n    F3 = \"f3\"\n    F4 = \"f4\"\n    F5 = \"f5\"\n    F6 = \"f6\"\n    F7 = \"f7\"\n    F8 = \"f8\"\n    F9 = \"f9\"\n    F10 = \"f10\"\n    F11 = \"f11\"\n    F12 = \"f12\"\n    F13 = \"f13\"\n    F14 = \"f14\"\n    F15 = \"f15\"\n    F16 = \"f16\"\n    F17 = \"f17\"\n    F18 = \"f18\"\n    F19 = \"f19\"\n    F20 = \"f20\"\n    F21 = \"f21\"\n    F22 = \"f22\"\n    F23 = \"f23\"\n    F24 = \"f24\"\n\n    ControlF1 = \"ctrl+f1\"\n    ControlF2 = \"ctrl+f2\"\n    ControlF3 = \"ctrl+f3\"\n    ControlF4 = \"ctrl+f4\"\n    ControlF5 = \"ctrl+f5\"\n    ControlF6 = \"ctrl+f6\"\n    ControlF7 = \"ctrl+f7\"\n    ControlF8 = \"ctrl+f8\"\n    ControlF9 = \"ctrl+f9\"\n    ControlF10 = \"ctrl+f10\"\n    ControlF11 = \"ctrl+f11\"\n    ControlF12 = \"ctrl+f12\"\n    ControlF13 = \"ctrl+f13\"\n    ControlF14 = \"ctrl+f14\"\n    ControlF15 = \"ctrl+f15\"\n    ControlF16 = \"ctrl+f16\"\n    ControlF17 = \"ctrl+f17\"\n    ControlF18 = \"ctrl+f18\"\n    ControlF19 = \"ctrl+f19\"\n    ControlF20 = \"ctrl+f20\"\n    ControlF21 = \"ctrl+f21\"\n    ControlF22 = \"ctrl+f22\"\n    ControlF23 = \"ctrl+f23\"\n    ControlF24 = \"ctrl+f24\"\n\n    # Matches any key.\n    Any = \"<any>\"\n\n    # Special.\n    ScrollUp = \"<scroll-up>\"\n    ScrollDown = \"<scroll-down>\"\n\n    # For internal use: key which is ignored.\n    # (The key binding for this key should not do anything.)\n    Ignore = \"<ignore>\"\n\n    # Some 'Key' aliases (for backwardshift+compatibility).\n    ControlSpace = \"ctrl-at\"\n    Tab = \"tab\"\n    Space = \"space\"\n    Enter = \"enter\"\n    Backspace = \"backspace\"\n\n    # ShiftControl was renamed to ControlShift in\n    # 888fcb6fa4efea0de8333177e1bbc792f3ff3c24 (20 Feb 2020).\n    ShiftControlLeft = ControlShiftLeft\n    ShiftControlRight = ControlShiftRight\n    ShiftControlHome = ControlShiftHome\n    ShiftControlEnd = ControlShiftEnd\n\n\n# Unicode db contains some obscure names\n# This mapping replaces them with more common terms\nKEY_NAME_REPLACEMENTS = {\n    \"solidus\": \"slash\",\n    \"reverse_solidus\": \"backslash\",\n    \"commercial_at\": \"at\",\n    \"hyphen_minus\": \"minus\",\n    \"plus_sign\": \"plus\",\n    \"low_line\": \"underscore\",\n}\nREPLACED_KEYS = {value: key for key, value in KEY_NAME_REPLACEMENTS.items()}\n\n# Convert the friendly versions of character key Unicode names\n# back to their original names.\n# This is because we go from Unicode to friendly by replacing spaces and dashes\n# with underscores, which cannot be undone by replacing underscores with spaces/dashes.\nKEY_TO_UNICODE_NAME = {\n    \"exclamation_mark\": \"EXCLAMATION MARK\",\n    \"quotation_mark\": \"QUOTATION MARK\",\n    \"number_sign\": \"NUMBER SIGN\",\n    \"dollar_sign\": \"DOLLAR SIGN\",\n    \"percent_sign\": \"PERCENT SIGN\",\n    \"left_parenthesis\": \"LEFT PARENTHESIS\",\n    \"right_parenthesis\": \"RIGHT PARENTHESIS\",\n    \"plus_sign\": \"PLUS SIGN\",\n    \"hyphen_minus\": \"HYPHEN-MINUS\",\n    \"full_stop\": \"FULL STOP\",\n    \"less_than_sign\": \"LESS-THAN SIGN\",\n    \"equals_sign\": \"EQUALS SIGN\",\n    \"greater_than_sign\": \"GREATER-THAN SIGN\",\n    \"question_mark\": \"QUESTION MARK\",\n    \"commercial_at\": \"COMMERCIAL AT\",\n    \"left_square_bracket\": \"LEFT SQUARE BRACKET\",\n    \"reverse_solidus\": \"REVERSE SOLIDUS\",\n    \"right_square_bracket\": \"RIGHT SQUARE BRACKET\",\n    \"circumflex_accent\": \"CIRCUMFLEX ACCENT\",\n    \"low_line\": \"LOW LINE\",\n    \"grave_accent\": \"GRAVE ACCENT\",\n    \"left_curly_bracket\": \"LEFT CURLY BRACKET\",\n    \"vertical_line\": \"VERTICAL LINE\",\n    \"right_curly_bracket\": \"RIGHT CURLY BRACKET\",\n}\n\n# Some keys have aliases. For example, if you press `ctrl+m` on your keyboard,\n# it's treated the same way as if you press `enter`. Key handlers `key_ctrl_m` and\n# `key_enter` are both valid in this case.\nKEY_ALIASES = {\n    \"tab\": [\"ctrl+i\"],\n    \"enter\": [\"ctrl+m\"],\n    \"escape\": [\"ctrl+left_square_brace\"],\n    \"ctrl+at\": [\"ctrl+space\"],\n    \"ctrl+j\": [\"newline\"],\n}\n\nKEY_DISPLAY_ALIASES = {\n    \"up\": \"\u2191\",\n    \"down\": \"\u2193\",\n    \"left\": \"\u2190\",\n    \"right\": \"\u2192\",\n    \"backspace\": \"\u232b\",\n    \"escape\": \"ESC\",\n    \"enter\": \"\u23ce\",\n    \"minus\": \"-\",\n    \"space\": \"SPACE\",\n}\n\n\ndef _get_unicode_name_from_key(key: str) -> str:\n    \"\"\"Get the best guess for the Unicode name of the char corresponding to the key.\n\n    This function can be seen as a pseudo-inverse of the function `_character_to_key`.\n    \"\"\"\n    return KEY_TO_UNICODE_NAME.get(key, key.upper())\n\n\ndef _get_key_aliases(key: str) -> list[str]:\n    \"\"\"Return all aliases for the given key, including the key itself\"\"\"\n    return [key] + KEY_ALIASES.get(key, [])\n\n\ndef _get_key_display(key: str) -> str:\n    \"\"\"Given a key (i.e. the `key` string argument to Binding __init__),\n    return the value that should be displayed in the app when referring\n    to this key (e.g. in the Footer widget).\"\"\"\n    if \"+\" in key:\n        return \"+\".join([_get_key_display(key) for key in key.split(\"+\")])\n\n    display_alias = KEY_DISPLAY_ALIASES.get(key)\n    if display_alias:\n        return display_alias\n\n    original_key = REPLACED_KEYS.get(key, key)\n    tentative_unicode_name = _get_unicode_name_from_key(original_key)\n    try:\n        unicode_character = unicodedata.lookup(tentative_unicode_name)\n    except KeyError:\n        return tentative_unicode_name\n\n    # Check if printable. `delete` for example maps to a control sequence\n    # which we don't want to write to the terminal.\n    if unicode_character.isprintable():\n        return unicode_character\n    return tentative_unicode_name\n\n\ndef _character_to_key(character: str) -> str:\n    \"\"\"Convert a single character to a key value.\n\n    This transformation can be undone by the function `_get_unicode_name_from_key`.\n    \"\"\"\n    if not character.isalnum():\n        key = unicodedata.name(character).lower().replace(\"-\", \"_\").replace(\" \", \"_\")\n    else:\n        key = character\n    key = KEY_NAME_REPLACEMENTS.get(key, key)\n    return key\n", "path": "src/textual/keys.py"}]}