Skip to content
Shell & TerminalDeep Dive Published Updated 8 min readViews unavailable

Kitty Keyboard Protocol: Unambiguous Key Events Without Breaking Legacy Input

Use kitty's opt-in CSI-u keyboard reporting with capability checks, event decoding, screen-scoped state, and reliable terminal-mode cleanup.

Traditional terminal input is a byte stream, not a portable stream of rich key events. A plain a may arrive as UTF-8 text, while an arrow key is commonly represented by an escape sequence. Some legacy encodings overlap: Escape is also the prefix that begins a control sequence, and combinations of modifiers can collapse into bytes that are hard to distinguish from other input. The kitty keyboard protocol provides an opt-in way for a terminal emulator to report more precise key identity, modifiers, event type, alternate layout keys, and associated text.

The word kitty names the protocol’s origin, not a guarantee that the installed terminal implements it. Support varies across terminal emulators, terminal multiplexers, SSH paths, libraries, and versions. The protocol is separate from POSIX termios, terminfo, and shell syntax. A terminal application should query or otherwise establish the capability it needs, retain a legacy fallback, and restore the state it changed when leaving the screen.

Why a byte stream loses key identity

In a traditional terminal mode, printable keys are generally delivered as text, often UTF-8, and functional keys use historical escape encodings. This works well for line-oriented interaction, but it gives a TUI limited information. A program may not know whether a repeated byte is a physical key repeat or repeated text, may not receive key-release events, and may have trouble differentiating a literal Escape key from the beginning of a longer escape sequence. The application may use timing heuristics to decide whether more bytes are coming, which introduces latency and ambiguity.

CSI-u represents a key event in an escape sequence whose final byte is u. In the protocol’s general form, the sequence can include a Unicode key code, optional alternate key codes, modifier bits, an event type, and optional text as code points. The protocol keeps ordinary behavior as the default so older programs continue to receive legacy encodings. An application opts into enhancements by sending a control sequence to the terminal; this is a negotiated terminal behavior, not a kernel keyboard driver setting.

For a familiar example, the protocol represents the a key by its unshifted code point, 97. The modifier value encodes a bit field plus one: Shift alone is 2, and Control plus Shift is 6. So a shifted Control-A press can be represented in the compact form CSI 97;6u. The value 97 is intentional even though the produced text may be uppercase: key identity and associated text are distinct pieces of information. Layout, input method, and operating-system handling can affect which information the terminal actually receives.

Progressive features let an application ask for only what it needs

The protocol uses a bitmask called progressive enhancement flags. The principal flags are: 1 to disambiguate overlapping legacy codes, 2 to report event types, 4 to report alternate keys, 8 to report all keys as escape sequences, and 16 to include associated text with key events. A simple editor might request disambiguation and event types; a game that needs key-up events for movement may additionally need all keys reported. Requesting every flag is not automatically best: reporting printable text only as control sequences changes how the application must decode and render it.

When event types are enabled, the modifier field can have a subfield identifying 1 for press, 2 for repeat, or 3 for release. A consumer should not assume every text-producing event carries that subfield in the default mode. To receive event types for printable keys, the application needs the feature that reports all keys as escape sequences; it can request associated text as an additional enhancement if text input is also required. The spec explicitly notes that Enter, Tab, and Backspace have special compatibility treatment, so keyboard test cases should include these common keys rather than assuming all keys use identical framing.

The terminal can answer a query for its current enhancement flags. The protocol also defines pushing and popping flag state with a stack, which lets a program change modes temporarily and restore the prior state without guessing what the caller had selected. There are separate stacks for the main and alternate screens. These details matter for editors, pagers, and TUIs that enter an alternate screen: keyboard state should have the same lifecycle boundaries as screen state.

A safe lifecycle is more important than the enable sequence

First determine that the program is attached to a terminal and that it can read the response path it intends to use. A program writing to a log file, a pipe, a CI runner, or a non-interactive job should not blindly send terminal control strings. Next, identify the exact terminal path: a local emulator may implement a feature that a multiplexer or remote hop does not pass through. $TERM and terminfo can help select classic capabilities, but they are not proof that this particular extension round-trips through the entire stack.

Use the protocol’s documented query procedure or a library with a tested capability probe. Bound the wait for replies; otherwise a missing response can make startup hang. Be careful about consuming unrelated user input while waiting. Send queries only at a point where the application can route replies and keystrokes correctly, and retain bytes that are not part of the reply. If support is not established, continue with the legacy path. Do not interpret silence as an authorization signal or infer support from the terminal’s brand string alone.

When support is established, push the mode the application needs. Entering an alternate screen and changing keyboard reporting are separate operations, so sequence them intentionally and follow the spec’s recommendation for the corresponding screen. On normal exit, pop the keyboard state before yielding control back to a parent application or shell. Do the same in error handlers, cancellation paths, and suspend/resume transitions. The shell should regain a usable keyboard even if the application failed to parse its last input.

This cleanup cannot be guaranteed after an uncatchable process termination, power loss, terminal disconnection, or a crash that prevents cleanup code from running. That is why applications should request the smallest reasonable mode, use the protocol’s stack-based restoration rather than hard-coding a “default” state, and preserve a documented recovery path. Testing only a clean return statement misses the incidents users actually report: panic, SIGTERM, Ctrl-Z, resume, nested TUIs, or closing an SSH connection mid-session.

An illustrative lifecycle skeleton is below. It is not a complete capability probe or a terminal library; use a maintained TUI/input library when possible. The CSI sequence is sent only after an application has confirmed the relevant terminal path and arranged response parsing:

import sys

CSI = "\x1b["

def run_interactive_ui(terminal_support_confirmed: bool) -> None:
    if not sys.stdin.isatty() or not sys.stdout.isatty():
        raise RuntimeError("interactive terminal required")

    enabled = False
    try:
        if terminal_support_confirmed:
            # Push a mode with disambiguation enabled; retain the terminal's
            # prior state so the matching pop can restore it.
            sys.stdout.write(CSI + ">1u")
            sys.stdout.flush()
            enabled = True

        run_event_loop_with_legacy_fallback()
    finally:
        if enabled:
            sys.stdout.write(CSI + "<u")
            sys.stdout.flush()

The placeholder run_event_loop_with_legacy_fallback() is deliberately not implemented here: reading and parsing terminal input correctly requires incremental byte framing, UTF-8 handling, timeouts, signal policy, and preservation of bytes belonging to the user. A production application should also restore alternate-screen, cursor, mouse, and termios state that it owns. These are separate state machines. Popping keyboard mode does not restore the terminal’s line discipline, and restoring termios does not pop the keyboard-mode stack.

Parse incrementally and test the path, not just the encoding

Control sequences can be split across multiple read() calls or coalesced with other bytes. A parser must retain partial sequences between reads, enforce a maximum sequence length, reject malformed numeric fields safely, and avoid treating arbitrary user input as an unbounded integer. It should distinguish a recognized protocol response from text or unrelated terminal output. If it sees unsupported or malformed input, it should fall back without discarding the user’s keystrokes.

Tests should cover a plain printable key, Shift and Control modifiers, a functional key, Escape versus an escape-sequence prefix, press/repeat/release, alternate key reporting for a non-US layout, associated text, the special Enter/Tab/Backspace behavior, and partial sequences across read boundaries. Test inside each supported terminal and multiplexer combination, locally and over SSH where relevant. Record terminal and multiplexer versions with failures, because an extension can be implemented by the outer emulator but blocked or transformed by an intermediary.

For accessibility and international input, do not equate physical key codes with text. An input method editor may produce text without an ordinary one-key event; a key may produce different text under a layout; and the protocol can report a key identity separately from associated Unicode code points. Applications should decide whether a shortcut is based on the physical/base-layout key, shifted key, modifier state, or resulting text. That is a product behavior choice, not something solved by parsing CSI-u syntax alone.

Keep the compatibility promise honest

The protocol is designed to coexist with legacy terminal applications by making richer reporting opt-in. That does not mean every terminal supports every flag, that every program recognizes CSI-u, or that a terminal multiplexer necessarily passes the sequence unchanged. The specification lists a range of implementations, but that list is not a certification matrix for every released version or every operating environment. Verify the exact terminal path your application supports.

Treat kitty keyboard reporting as an optional feature with a robust fallback. Probe deliberately, request the minimum capability, parse incrementally, keep text and key identity conceptually separate, and restore state on every exit path you can handle. The gain is more reliable input for compatible applications; the cost is owning the negotiation and lifecycle carefully.

Related:

Sources:

Comments