Skip to content

Commit f967ec1

Browse files
committed
Support the Kitty keyboard protocol (flag 1, disambiguate)
Push the Kitty keyboard protocol on startup and pop it on exit so supporting terminals deliver modified keys — Ctrl-Enter, Shift-Enter, Ctrl-Shift-Enter, Alt-letter, and full modifier coverage on the navigation block — as distinct CSI u sequences instead of being collapsed into their unmodified equivalents. Terminals that don't implement the protocol silently ignore the push, so there is no regression for them. Detection mirrors the existing CPR machinery: the renderer emits `CSI ? u`, a new binding under key_binding/bindings/ consumes the response and flips `Renderer.kitty_support` from UNKNOWN to SUPPORTED, letting callers branch on capability if they want. The bulk of the code lives in new files: - input/kitty_keyboard.py — CSI u decoder, functional-key table, query-response parser - output/kitty_keyboard.py — push/pop context manager with reference-counted depth, sequence constants - key_binding/bindings/kitty_keyboard.py — response-consuming binding - docs/pages/advanced_topics/kitty_keyboard_protocol.rst — maintainer notes on wire format, capability detection, and known sharp edges Touch-points in existing code are small and mirror the CPR pattern: one import + one regex + one dispatch branch in vt100_parser.py, a push/query/pop trio in renderer.py, one binding registration in key_binding/defaults.py, four new enum values in keys.py. prompt_toolkit does not push xterm's modifyOtherKeys. Users whose terminal or tmux has it enabled independently still get the existing `CSI 27` Enter fallback in ansi_escape_sequences.py, which folds all three modifier+Enter variants to plain Keys.ControlM so the form at least submits rather than silently doing nothing. New Keys: ControlEnter, ControlShiftEnter, ShiftEnter, KittyKeyboardResponse. Bindings registered as `c-enter` / `s-enter` / `c-s-enter` fire on Kitty-capable terminals; on non-Kitty terminals they don't fire (plain Enter fires instead, as before).
1 parent 940af53 commit f967ec1

15 files changed

Lines changed: 1496 additions & 10 deletions

File tree

CHANGELOG

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
CHANGELOG
22
=========
33

4+
Unreleased
5+
----------
6+
7+
New features:
8+
- Support the Kitty keyboard protocol (flag 1, "disambiguate") on
9+
terminals that implement it — kitty, ghostty, wezterm, foot,
10+
Alacritty, iTerm2 (with "Report modifiers using CSI u" enabled in
11+
Preferences → Profiles → Keys), and others. The renderer pushes the
12+
protocol on startup and pops it on exit, so modified keys like
13+
Ctrl-Enter, Shift-Enter, Ctrl-Shift-Enter, and Alt-letter arrive as
14+
distinct `CSI u` sequences. New key values `Keys.ControlEnter`,
15+
`Keys.ControlShiftEnter`, and `Keys.ShiftEnter` can be used in
16+
bindings (e.g. `@bindings.add("c-enter")`). Terminals that don't
17+
implement the protocol silently ignore the push — no regression,
18+
plain Enter continues to submit.
19+
420
3.0.52: 2025-08-27
521
------------------
622

docs/pages/advanced_topics/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Advanced topics
88
:maxdepth: 1
99

1010
key_bindings
11+
kitty_keyboard_protocol
1112
styling
1213
filters
1314
rendering_flow
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
.. _kitty_keyboard_protocol:
2+
3+
4+
Kitty keyboard protocol
5+
=======================
6+
7+
Maintainer-facing notes on prompt_toolkit's support for the `Kitty
8+
keyboard protocol <https://sw.kovidgoyal.net/kitty/keyboard-protocol/>`_.
9+
10+
Despite its name, the Kitty protocol is supported by a wide range of
11+
terminal emulators across platforms and is not limited to the Kitty
12+
terminal itself.
13+
14+
Only flag 1 ("disambiguate escape codes") is currently implemented.
15+
The spec also defines progressive-enhancement flags for reporting
16+
press/release/repeat events, alternate keys, all keys as escape codes,
17+
and associated text; none of those are implemented here.
18+
19+
20+
Why
21+
---
22+
23+
Under legacy terminal encodings, many modifier+key combinations are
24+
ambiguous or impossible — :kbd:`c-enter` sends the same ``\r`` as plain
25+
:kbd:`enter`, :kbd:`s-enter` is indistinguishable from :kbd:`enter` on
26+
most terminals, :kbd:`m-b` (Alt-b) is reported as an Esc-prefix that
27+
collides with pressing :kbd:`escape` followed by ``b``. The Kitty
28+
protocol fixes all of this by escaping modified keys into ``CSI u``
29+
sequences with explicit modifier bits.
30+
31+
prompt_toolkit pushes flag 1 ("disambiguate escape codes") on startup
32+
and pops it on exit, so supporting terminals deliver modified keys as
33+
distinct ``Keys`` values, and non-supporting terminals silently keep
34+
their existing behavior.
35+
36+
37+
38+
What the code does
39+
------------------
40+
41+
Output
42+
~~~~~~
43+
44+
``src/prompt_toolkit/output/kitty_keyboard.py`` owns the wire-format
45+
constants and exposes ``kitty_keyboard_protocol(output, flags)`` — a
46+
context manager that pushes the given flags on entry and pops on exit.
47+
A depth counter (lazily attached to the ``Output`` instance by the
48+
context manager, not a first-class field on ``Output``) ensures nested
49+
holders compose correctly: outermost enter pushes and flushes,
50+
outermost exit pops and flushes. Entering a nested context with a
51+
different ``flags`` value raises ``ValueError`` rather than silently
52+
corrupting the terminal's flag stack.
53+
54+
Input
55+
~~~~~
56+
57+
``src/prompt_toolkit/input/kitty_keyboard.py`` owns the ``CSI u``
58+
decoder. Covered:
59+
60+
- 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
64+
nearest existing ``Keys`` value with Shift / Ctrl / Ctrl-Shift
65+
promotion where an enum exists.
66+
- Printable Unicode keys with Ctrl (mapped to ``Keys.ControlX``) and
67+
Ctrl+Shift digits (mapped to ``Keys.ControlShift1`` …).
68+
- Alt as a meta prefix: emitted as ``(Keys.Escape, base_key)`` to match
69+
prompt_toolkit's long-standing convention for meta-prefixed keys, so
70+
existing bindings like ``('escape', 'b')`` keep working.
71+
- CapsLock and NumLock modifier bits are stripped before decoding so
72+
terminals that report them don't break bindings.
73+
74+
``src/prompt_toolkit/input/vt100_parser.py`` dispatches ``CSI … u``
75+
sequences to the decoder (after the static ``ANSI_SEQUENCES`` lookup,
76+
so pre-existing fixed-form entries still win) and recognizes the
77+
``CSI ? <flags> u`` query response as ``Keys.KittyKeyboardResponse``.
78+
79+
Renderer and capability detection
80+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
81+
82+
``Renderer`` pushes flag 1 on first render and pops it on reset. At the
83+
same time it writes a ``CSI ? u`` query. The binding in
84+
``src/prompt_toolkit/key_binding/bindings/kitty_keyboard.py`` consumes
85+
the response and flips ``renderer.kitty_support`` from ``UNKNOWN`` to
86+
``SUPPORTED``. Terminals that don't implement the protocol silently
87+
ignore both the push and the query; ``kitty_support`` stays at
88+
``UNKNOWN`` and the terminal keeps sending legacy byte sequences.
89+
Callers that want to branch on capability (e.g. to surface a hint to
90+
the user) can read ``app.renderer.kitty_support`` — ``Application``
91+
exposes its renderer as a public attribute, and the value is one of
92+
``KittySupport.UNKNOWN`` or ``KittySupport.SUPPORTED`` (imported from
93+
``prompt_toolkit.renderer``).
94+
95+
Legacy xterm ``modifyOtherKeys`` fallback
96+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
97+
98+
prompt_toolkit does **not** push ``\x1b[>4;Nm`` to enable xterm's
99+
``modifyOtherKeys``. But the parser still folds
100+
``CSI 27 ; <mods> ; 13 ~`` (Shift-, Ctrl-, and Ctrl-Shift-Enter under
101+
``modifyOtherKeys``) back to ``Keys.ControlM``. That's a passive
102+
compatibility shim: if a user's terminal or tmux has
103+
``modifyOtherKeys`` enabled independently, modified :kbd:`enter` still
104+
submits the form instead of silently doing nothing. Users who want
105+
distinct bindings for :kbd:`c-enter` / :kbd:`s-enter` need a
106+
Kitty-capable terminal.
107+
108+
New ``Keys`` values
109+
~~~~~~~~~~~~~~~~~~~
110+
111+
- ``Keys.ControlEnter``, ``Keys.ControlShiftEnter``,
112+
``Keys.ShiftEnter`` — Kitty-only modifier+Enter distinctions. On
113+
non-Kitty terminals these bindings don't fire; plain :kbd:`enter`
114+
fires instead (the protocol-less fallback).
115+
- ``Keys.ControlTab``, ``Keys.ControlShiftTab`` — Kitty-only
116+
modifier+Tab distinctions. Plain :kbd:`tab` (``Keys.ControlI``) and
117+
Shift-Tab (``Keys.BackTab``) were already distinguishable; Ctrl-Tab
118+
and Ctrl-Shift-Tab only come through under the protocol. On
119+
non-Kitty terminals they fold back to their legacy equivalents.
120+
- ``Keys.ControlEscape``, ``Keys.ControlShiftEscape`` — Kitty-only
121+
modifier+Escape distinctions, alongside the pre-existing
122+
``Keys.ShiftEscape``. Same non-Kitty fallback behavior.
123+
- ``Keys.KittyKeyboardResponse`` — internal sentinel for the query
124+
response parser-to-binding dispatch.
125+
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+
132+
133+
What could be done in the future
134+
--------------------------------
135+
136+
Higher flags
137+
~~~~~~~~~~~~
138+
139+
The protocol defines further enhancement flags beyond "disambiguate":
140+
141+
- **Flag 2 — report event types.** Distinguishes press / release /
142+
repeat. Useful for full-screen apps and tooling; not for a shell.
143+
Would require adding an ``event_type`` field to ``KeyPress``, which
144+
is a coordinated API change.
145+
- **Flag 4 — report alternate keys.** Sends the base-layout keycode
146+
alongside the current-layout one; helpful for non-US keyboard
147+
layouts where a binding is conceptually on the "unshifted key at
148+
that position".
149+
- **Flag 8 — report all keys as escape codes.** Even unmodified
150+
letters arrive as ``CSI u``. Dramatically changes input and needs
151+
corresponding decoder work.
152+
- **Flag 16 — report associated text.** Only meaningful with flag 8.
153+
154+
More functional keys
155+
~~~~~~~~~~~~~~~~~~~~
156+
157+
The decoder's ``_FUNCTIONAL`` table stops at :kbd:`f12` and omits
158+
keypad / media / system keys (Play, Mute, brightness, Left Super, …)
159+
that Kitty can report. Extending the table and adding matching
160+
``Keys`` values is mechanical.
161+
162+
Press/release-aware bindings
163+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
164+
165+
Bindings today fire on press only. If flag 2 support is ever added,
166+
``KeyBindings.add`` would need an opt-in parameter for release/repeat
167+
events, and the key processor would need to carry the event type. A
168+
large surface; best tackled together with any flag-2 work.
169+
170+
171+
Wire format reference
172+
---------------------
173+
174+
**Push flags.**
175+
``CSI > <flags> u`` pushes ``<flags>`` onto the terminal's stack so
176+
the pop on exit restores the pre-push state. prompt_toolkit always
177+
pushes ``flags=1`` (disambiguate). The spec also defines
178+
``CSI = <flags> ; <mode> u`` which *modifies* the top of the stack
179+
in place (mode 1 set, 2 OR, 3 AND-NOT); we don't use it because it
180+
offers no clean restore.
181+
182+
**Pop flags.**
183+
``CSI < u`` pops one entry. ``CSI < N u`` pops N.
184+
185+
**Query flags.**
186+
``CSI ? u``; terminal answers ``CSI ? <flags> u`` if supported,
187+
silence otherwise.
188+
189+
**Key event.**
190+
``CSI <keycode>[:<alt>] ; <modifiers>[:<event-type>] ;
191+
<text-codepoints> u``. Every key, functional or not, terminates in
192+
``u`` — that's the whole point of the protocol versus the legacy
193+
``CSI <n> ~`` encoding it replaces. Modifiers encode as
194+
``1 + bitmask`` — the ``+1`` ensures an omitted modifier field
195+
can't be confused with "Shift pressed". Keycode is a Unicode
196+
codepoint for printable keys, functional-key codes otherwise
197+
(Enter=13, Escape=27, F1=57364, …). Event-type is ``1`` for press,
198+
``2`` for repeat, ``3`` for release; under flag 1 only press
199+
events are sent, but the decoder defensively drops the other two.
200+
201+
202+
Known sharp edges
203+
-----------------
204+
205+
- **Modified Enter does not submit by default.** :kbd:`c-enter`,
206+
:kbd:`s-enter`, and :kbd:`c-s-enter` are delivered as distinct keys
207+
on a Kitty-capable terminal, but no binding is attached to them out
208+
of the box — the default ``accept-line`` handler stays on plain
209+
:kbd:`enter` only. This is deliberate: if we routed modified Enter
210+
to ``accept-line``, anyone who has long-standing muscle memory around
211+
"Ctrl-Enter inserts a newline in a multi-line prompt" would suddenly
212+
find their input submitted on a Kitty terminal but not elsewhere —
213+
the same physical gesture doing two different things depending on
214+
the terminal. Users who want :kbd:`c-enter` to submit can bind it
215+
explicitly::
216+
217+
bindings = KeyBindings()
218+
219+
@bindings.add("c-enter")
220+
def _(event):
221+
event.current_buffer.validate_and_handle()
222+
223+
The same applies to :kbd:`c-tab`, :kbd:`c-s-tab`, :kbd:`c-escape`,
224+
and :kbd:`c-s-escape` — they're available as distinct keys under the
225+
protocol, but we don't assign them any default semantics.
226+
227+
- **Tmux pass-through.** Requires ``set -g extended-keys on`` *and* an
228+
underlying terminal that supports the protocol. If the underlying
229+
terminal doesn't, tmux swallows the query and ``kitty_support``
230+
stays at ``UNKNOWN``.
231+
- **Detection latency.** The query response is asynchronous; if a
232+
terminal is slow, the first few keys may arrive before
233+
``kitty_support`` flips to ``SUPPORTED``. That only affects the
234+
capability signal — the push itself applies immediately, so the
235+
terminal's first keystroke is already in the new encoding.
236+
- **Functional-key codes are not universal.** The Kitty spec pins
237+
Enter=13 (which coincides with ``\r``) but implementations disagree
238+
on some rarer functional codes. Worth spot-checking new ones against
239+
kitty, ghostty, wezterm, foot.
240+
- **Alt vs. Esc-prefix.** Kitty reports Alt as a modifier; the legacy
241+
path reports it as ``(Esc, letter)``. The decoder emits
242+
``(Keys.Escape, base_key)`` for Alt-prefixed keys to match legacy
243+
convention — so a binding registered as ``('escape', 'b')`` matches
244+
Alt-b either way.

0 commit comments

Comments
 (0)