Skip to content

Commit 3fb44b4

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 3fb44b4

13 files changed

Lines changed: 1140 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: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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+
Note that despite it's name, the kitty protocol is support by a wide range of
11+
terminal emulator on various platform and is far from being limited to only
12+
the kitty terminal emulator.
13+
14+
Currently only part of the protocol is implemented (to disambiguate modifiers
15+
on some keys), but it is could be possible to request progressive enhancement,
16+
allowing to actually receive key press, and release as well as many other
17+
distinctions, but this is not implemented
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+
127+
What could be done in the future
128+
--------------------------------
129+
130+
Higher flags
131+
~~~~~~~~~~~~
132+
133+
The protocol defines further enhancement flags beyond "disambiguate":
134+
135+
- **Flag 2 — report event types.** Distinguishes press / release /
136+
repeat. Useful for full-screen apps and tooling; not for a shell.
137+
Would require adding an ``event_type`` field to ``KeyPress``, which
138+
is a coordinated API change.
139+
- **Flag 4 — report alternate keys.** Sends the base-layout keycode
140+
alongside the current-layout one; helpful for non-US keyboard
141+
layouts where a binding is conceptually on the "unshifted key at
142+
that position".
143+
- **Flag 8 — report all keys as escape codes.** Even unmodified
144+
letters arrive as ``CSI u``. Dramatically changes input and needs
145+
corresponding decoder work.
146+
- **Flag 16 — report associated text.** Only meaningful with flag 8.
147+
148+
More functional keys
149+
~~~~~~~~~~~~~~~~~~~~
150+
151+
The decoder's ``_FUNCTIONAL`` table stops at :kbd:`f12` and omits
152+
keypad / media / system keys (Play, Mute, brightness, Left Super, …)
153+
that Kitty can report. Extending the table and adding matching
154+
``Keys`` values is mechanical.
155+
156+
Press/release-aware bindings
157+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
158+
159+
Bindings today fire on press only. If flag 2 support is ever added,
160+
``KeyBindings.add`` would need an opt-in parameter for release/repeat
161+
events, and the key processor would need to carry the event type. A
162+
large surface; best tackled together with any flag-2 work.
163+
164+
165+
Wire format reference
166+
---------------------
167+
168+
**Push flags.**
169+
``CSI = <flags> ; <mode> u``. The spec defines mode 1 (set),
170+
mode 2 (OR into current), and mode 3 (AND-NOT into current).
171+
prompt_toolkit always hardcodes mode 1 with ``flags=1``; the other
172+
modes exist in the spec but aren't used here.
173+
174+
**Pop flags.**
175+
``CSI < u`` pops one entry. ``CSI < N u`` pops N.
176+
177+
**Query flags.**
178+
``CSI ? u``; terminal answers ``CSI ? <flags> u`` if supported,
179+
silence otherwise.
180+
181+
**Key event.**
182+
``CSI <keycode>[:<alt>] ; <modifiers>[:<event-type>] ;
183+
<text-codepoints> u``. Every key, functional or not, terminates in
184+
``u`` — that's the whole point of the protocol versus the legacy
185+
``CSI <n> ~`` encoding it replaces. Modifiers encode as
186+
``1 + bitmask`` — the ``+1`` ensures an omitted modifier field
187+
can't be confused with "Shift pressed". Keycode is a Unicode
188+
codepoint for printable keys, functional-key codes otherwise
189+
(Enter=13, Escape=27, F1=57364, …). Event-type is ``1`` for press,
190+
``2`` for repeat, ``3`` for release; under flag 1 only press
191+
events are sent, but the decoder defensively drops the other two.
192+
193+
194+
Known sharp edges
195+
-----------------
196+
197+
- **Tmux pass-through.** Requires ``set -g extended-keys on`` *and* an
198+
underlying terminal that supports the protocol. If the underlying
199+
terminal doesn't, tmux swallows the query and ``kitty_support``
200+
stays at ``UNKNOWN``.
201+
- **Detection latency.** The query response is asynchronous; if a
202+
terminal is slow, the first few keys may arrive before
203+
``kitty_support`` flips to ``SUPPORTED``. That only affects the
204+
capability signal — the push itself applies immediately, so the
205+
terminal's first keystroke is already in the new encoding.
206+
- **Functional-key codes are not universal.** The Kitty spec pins
207+
Enter=13 (which coincides with ``\r``) but implementations disagree
208+
on some rarer functional codes. Worth spot-checking new ones against
209+
kitty, ghostty, wezterm, foot.
210+
- **Alt vs. Esc-prefix.** Kitty reports Alt as a modifier; the legacy
211+
path reports it as ``(Esc, letter)``. The decoder emits
212+
``(Keys.Escape, base_key)`` for Alt-prefixed keys to match legacy
213+
convention — so a binding registered as ``('escape', 'b')`` matches
214+
Alt-b either way.

examples/prompts/modified-enter.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python
2+
"""
3+
Demo for modified-Enter key bindings (Ctrl-Enter, Ctrl-Shift-Enter,
4+
Shift-Enter).
5+
6+
prompt_toolkit pushes the Kitty keyboard protocol (flag 1) on startup,
7+
so terminals that implement it (kitty, ghostty, wezterm, foot,
8+
Alacritty, recent iTerm2 with CSI u reporting enabled, …) can
9+
distinguish these from plain Enter.
10+
11+
Run this and try pressing each combination. Plain Enter still submits
12+
(the `c-m` / `enter` binding shipped by ``PromptSession`` defaults
13+
fires as usual — our custom bindings below don't override it).
14+
Terminals that don't support the protocol will just submit on any Enter
15+
variant — that's the expected fallback.
16+
"""
17+
18+
from prompt_toolkit import prompt
19+
from prompt_toolkit.application import run_in_terminal
20+
from prompt_toolkit.key_binding import KeyBindings
21+
22+
23+
def main():
24+
bindings = KeyBindings()
25+
26+
def _announce(label):
27+
def _print():
28+
print(f"[{label}] pressed")
29+
30+
run_in_terminal(_print)
31+
32+
@bindings.add("c-enter")
33+
def _(event):
34+
_announce("Ctrl-Enter")
35+
36+
@bindings.add("s-enter")
37+
def _(event):
38+
_announce("Shift-Enter")
39+
40+
@bindings.add("c-s-enter")
41+
def _(event):
42+
_announce("Ctrl-Shift-Enter")
43+
44+
print("Try Ctrl-Enter, Shift-Enter, Ctrl-Shift-Enter. Plain Enter submits.")
45+
text = prompt("> ", key_bindings=bindings)
46+
print(f"You said: {text!r}")
47+
48+
49+
if __name__ == "__main__":
50+
main()

src/prompt_toolkit/input/ansi_escape_sequences.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,17 @@
122122
"\x1b[23;2~": Keys.F23,
123123
"\x1b[24;2~": Keys.F24,
124124
# --
125-
# CSI 27 disambiguated modified "other" keys (xterm)
125+
# xterm `modifyOtherKeys` CSI 27 disambiguated modified Enter.
126+
# prompt_toolkit does not implement xterm's modifyOtherKeys protocol
127+
# itself
126128
# Ref: https://invisible-island.net/xterm/modified-keys.html
127-
# These are currently unsupported, so just re-map some common ones to the
128-
# unmodified versions
129+
# (we disambiguate via the Kitty keyboard protocol instead),
130+
# but a user whose terminal or tmux has it enabled independently
131+
# will still send these sequences. Map them all to plain Enter so
132+
# modifier+Enter at least submits the form, rather than doing
133+
# nothing at all. Users who want a distinct Ctrl-Enter / Shift-Enter
134+
# binding need a Kitty-capable terminal, where the `CSI u` decoder
135+
# produces the richer Keys.
129136
"\x1b[27;2;13~": Keys.ControlM, # Shift + Enter
130137
"\x1b[27;5;13~": Keys.ControlM, # Ctrl + Enter
131138
"\x1b[27;6;13~": Keys.ControlM, # Ctrl + Shift + Enter

0 commit comments

Comments
 (0)