|
| 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. |
0 commit comments