Skip to content

Commit 4591051

Browse files
committed
checklist
1 parent f967ec1 commit 4591051

6 files changed

Lines changed: 127 additions & 38 deletions

File tree

docs/pages/advanced_topics/kitty_keyboard_protocol.rst

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,15 @@ Input
5858
decoder. Covered:
5959

6060
- Functional keys from the Kitty spec: :kbd:`enter`, :kbd:`tab`,
61-
:kbd:`escape`, :kbd:`backspace`, arrows, navigation block
62-
(:kbd:`home` / :kbd:`end` / :kbd:`pageup` / :kbd:`pagedown` /
63-
:kbd:`insert` / :kbd:`delete`), :kbd:`f1`–:kbd:`f12`. Mapped to the
61+
:kbd:`escape`, :kbd:`backspace`, :kbd:`f1`–:kbd:`f12`. Mapped to the
6462
nearest existing ``Keys`` value with Shift / Ctrl / Ctrl-Shift
65-
promotion where an enum exists.
63+
promotion where an enum exists. Arrow keys and the navigation block
64+
(:kbd:`home` / :kbd:`end` / :kbd:`pageup` / :kbd:`pagedown` /
65+
:kbd:`insert` / :kbd:`delete`) are **not** handled here — under
66+
flag 1 the Kitty spec keeps them in their legacy
67+
``CSI <n> ~`` / ``CSI <letter>`` / ``SS3 <letter>`` encoding even
68+
when modified, so they continue to travel through
69+
``ANSI_SEQUENCES`` (which already has the full modifier matrix).
6670
- Printable Unicode keys with Ctrl (mapped to ``Keys.ControlX``) and
6771
Ctrl+Shift digits (mapped to ``Keys.ControlShift1`` …).
6872
- Alt as a meta prefix: emitted as ``(Keys.Escape, base_key)`` to match
@@ -120,15 +124,16 @@ New ``Keys`` values
120124
- ``Keys.ControlEscape``, ``Keys.ControlShiftEscape`` — Kitty-only
121125
modifier+Escape distinctions, alongside the pre-existing
122126
``Keys.ShiftEscape``. Same non-Kitty fallback behavior.
127+
- ``Keys.ControlBackspace``, ``Keys.ShiftBackspace``,
128+
``Keys.ControlShiftBackspace`` — Kitty-only modifier+Backspace
129+
distinctions. Unlike modified Enter, there is no safe legacy
130+
fallback: on most non-Kitty terminals Ctrl-Backspace is
131+
indistinguishable from plain Backspace or from Ctrl-H, so we do
132+
not fold these down — a binding on one of them will simply not
133+
fire on non-Kitty terminals.
123134
- ``Keys.KittyKeyboardResponse`` — internal sentinel for the query
124135
response parser-to-binding dispatch.
125136

126-
Backspace (:kbd:`backspace`) is decoded from the Kitty functional-key
127-
code 127, but prompt_toolkit has no distinct ``Keys`` values for
128-
:kbd:`c-backspace` / :kbd:`s-backspace`, so modified Backspace silently
129-
folds back to plain Backspace (``Keys.ControlH``) — same behavior as on
130-
legacy terminals.
131-
132137

133138
What could be done in the future
134139
--------------------------------
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python
2+
"""
3+
Interactive checklist for Kitty-only key gestures.
4+
5+
prompt_toolkit pushes the Kitty keyboard protocol (flag 1) on startup,
6+
so terminals that implement it (kitty, ghostty, wezterm, foot,
7+
Alacritty, recent iTerm2 with CSI u reporting enabled, …) can
8+
distinguish modifier+Enter, modifier+Tab, modifier+Escape, and
9+
modifier+Backspace combinations that collapse to a single byte on
10+
legacy terminals.
11+
12+
The bottom toolbar lists every such gesture. Press one and its row
13+
turns green. On terminals without the protocol, rows stay grey —
14+
that's the expected fallback, not a bug. Press plain Enter to exit.
15+
"""
16+
17+
from prompt_toolkit import prompt
18+
from prompt_toolkit.key_binding import KeyBindings
19+
from prompt_toolkit.styles import Style
20+
21+
KITTY_KEYS: list[tuple[str, str]] = [
22+
("c-enter", "Ctrl-Enter"),
23+
("s-enter", "Shift-Enter"),
24+
("c-s-enter", "Ctrl-Shift-Enter"),
25+
("c-tab", "Ctrl-Tab"),
26+
("c-s-tab", "Ctrl-Shift-Tab"),
27+
("c-escape", "Ctrl-Escape"),
28+
("c-s-escape", "Ctrl-Shift-Escape"),
29+
("c-backspace", "Ctrl-Backspace"),
30+
("s-backspace", "Shift-Backspace"),
31+
("c-s-backspace", "Ctrl-Shift-Backspace"),
32+
]
33+
34+
35+
def main():
36+
pressed: set[str] = set()
37+
38+
def toolbar():
39+
lines = [("", "Kitty-only gestures — press each to turn it green:\n")]
40+
for binding, label in KITTY_KEYS:
41+
if binding in pressed:
42+
lines.append(("class:done", f" [x] {label}\n"))
43+
else:
44+
lines.append(("class:todo", f" [ ] {label}\n"))
45+
remaining = len(KITTY_KEYS) - len(pressed)
46+
if remaining:
47+
lines.append(("", f"\n{remaining} remaining — plain Enter to exit."))
48+
else:
49+
lines.append(("class:done", "\nAll gestures recorded. Enter to exit."))
50+
return lines
51+
52+
bindings = KeyBindings()
53+
54+
def make_handler(binding: str):
55+
def handler(event):
56+
pressed.add(binding)
57+
event.app.invalidate()
58+
59+
return handler
60+
61+
for binding, _label in KITTY_KEYS:
62+
bindings.add(binding)(make_handler(binding))
63+
64+
style = Style.from_dict(
65+
{
66+
"bottom-toolbar": "noreverse",
67+
"bottom-toolbar.text": "",
68+
"done": "fg:ansigreen bold",
69+
"todo": "fg:ansibrightblack",
70+
}
71+
)
72+
73+
prompt(
74+
"> ",
75+
bottom_toolbar=toolbar,
76+
key_bindings=bindings,
77+
style=style,
78+
refresh_interval=0.5,
79+
)
80+
81+
82+
if __name__ == "__main__":
83+
main()

src/prompt_toolkit/input/kitty_keyboard.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,6 @@
9090
}
9191

9292

93-
# Navigation keys that have distinct `Keys` values for every
94-
# (Shift, Ctrl, Ctrl-Shift) variant. Tuple order: (shift, ctrl, ctrl+shift).
95-
_NAV_MAP: dict[Keys, tuple[Keys, Keys, Keys]] = {
96-
Keys.Up: (Keys.ShiftUp, Keys.ControlUp, Keys.ControlShiftUp),
97-
Keys.Down: (Keys.ShiftDown, Keys.ControlDown, Keys.ControlShiftDown),
98-
Keys.Left: (Keys.ShiftLeft, Keys.ControlLeft, Keys.ControlShiftLeft),
99-
Keys.Right: (Keys.ShiftRight, Keys.ControlRight, Keys.ControlShiftRight),
100-
Keys.Home: (Keys.ShiftHome, Keys.ControlHome, Keys.ControlShiftHome),
101-
Keys.End: (Keys.ShiftEnd, Keys.ControlEnd, Keys.ControlShiftEnd),
102-
Keys.PageUp: (Keys.ShiftPageUp, Keys.ControlPageUp, Keys.ControlShiftPageUp),
103-
Keys.PageDown: (
104-
Keys.ShiftPageDown,
105-
Keys.ControlPageDown,
106-
Keys.ControlShiftPageDown,
107-
),
108-
Keys.Insert: (Keys.ShiftInsert, Keys.ControlInsert, Keys.ControlShiftInsert),
109-
Keys.Delete: (Keys.ShiftDelete, Keys.ControlDelete, Keys.ControlShiftDelete),
110-
}
111-
112-
11393
_DecodeResult = Keys | str | tuple[Keys | str, ...] | None
11494

11595

@@ -271,16 +251,15 @@ def _apply_modifiers(base: Keys, ctrl: bool, shift: bool) -> Keys:
271251
if shift:
272252
return Keys.ShiftEscape
273253
return Keys.Escape
274-
_ctrl_key: Keys | None
275-
if base in _NAV_MAP:
276-
shift_key, _ctrl_key, ctrl_shift_key = _NAV_MAP[base]
254+
255+
if base is Keys.ControlH: # Backspace (keycode 127)
277256
if ctrl and shift:
278-
return ctrl_shift_key
257+
return Keys.ControlShiftBackspace
279258
if ctrl:
280-
return _ctrl_key
259+
return Keys.ControlBackspace
281260
if shift:
282-
return shift_key
283-
return base
261+
return Keys.ShiftBackspace
262+
return Keys.ControlH
284263

285264
# F1..F24 — Ctrl+ is a distinct enum; Shift+ is mapped to FN+12 in
286265
# prompt_toolkit's existing convention (F1 → F13 etc.), but we don't

src/prompt_toolkit/keys.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ class Keys(str, Enum):
137137
ControlShiftEnter = "c-s-enter"
138138
ShiftEnter = "s-enter"
139139

140+
# Modified Backspace keys. Only distinguishable from plain Backspace
141+
# under the Kitty keyboard protocol. On terminals that don't
142+
# implement Kitty, these don't fire at all — a bound shortcut is
143+
# silently inert there. Unlike modified Enter, there is no safe
144+
# legacy fallback: Ctrl-Backspace on most legacy terminals is
145+
# indistinguishable from plain Backspace or from Ctrl-H, so we
146+
# don't fold it down to either.
147+
ControlBackspace = "c-backspace"
148+
ShiftBackspace = "s-backspace"
149+
ControlShiftBackspace = "c-s-backspace"
150+
140151
F1 = "f1"
141152
F2 = "f2"
142153
F3 = "f3"

src/prompt_toolkit/renderer.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,10 +662,17 @@ def render(
662662
# that was never fully pushed (which would decrement the
663663
# Output's depth counter below zero and permanently corrupt
664664
# it for this Output's remaining lifetime).
665+
#
666+
# Mark as pushed *before* entering so a raise from
667+
# `kitty_keyboard_protocol` (e.g. nested flags mismatch) does
668+
# not put us in an infinite retry loop — the guard at line
669+
# 648 would otherwise keep re-firing the same exception on
670+
# every render. The stack stays empty in that case, so the
671+
# matching `close()` in `reset()` remains a no-op.
672+
self._kitty_keyboard_pushed = True
665673
self._kitty_keyboard_stack.enter_context(
666674
kitty_keyboard_protocol(self.output)
667675
)
668-
self._kitty_keyboard_pushed = True
669676
# Flush so the query and push reach the terminal before any
670677
# input arrives — otherwise the response can race the screen
671678
# render's flush further down and a fast user keystroke can

tests/test_kitty_keyboard.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
("\x1b[27;2u", Keys.ShiftEscape),
3838
("\x1b[27;5u", Keys.ControlEscape),
3939
("\x1b[27;6u", Keys.ControlShiftEscape),
40+
("\x1b[127u", Keys.ControlH), # plain Backspace
41+
("\x1b[127;2u", Keys.ShiftBackspace),
42+
("\x1b[127;5u", Keys.ControlBackspace),
43+
("\x1b[127;6u", Keys.ControlShiftBackspace),
4044
# NOTE: arrow keys, navigation block (Insert / Delete / Home /
4145
# End / PageUp / PageDown), and F1–F4 intentionally do *not*
4246
# appear here. Under flag 1 (disambiguate) — the only flag

0 commit comments

Comments
 (0)