^[control-codes live in libghostty-vt

Control sequences

CSI = u Kitty Keyboard Protocol

Ask the terminal to report keys unambiguously, with every modifier and with releases, through a stack of flags.

CSI = Ps ; Pm u
Specification
Comprehensive keyboard handling in terminals, kitty documentation

The usual way of sending keys loses information: Esc is the same byte that starts an escape sequence, Control and i is the same as Tab, most keys with Control and Shift together cannot be sent at all, and a key’s release is never sent. The kitty terminal’s keyboard protocol fixes this, and a program turns on as much of it as it needs with a set of flags:

FlagAsks for
1disambiguate: send Esc, and keys with Control or Alt, as CSI code ; modifiers u
2report event types: presses, repeats and releases
4report alternate keys: the shifted key and the key’s place on a US layout
8report all keys as escape codes, ordinary text included
16report the text a key produces along with it

The control sequences that set them, all with the final u, are defined in kitty’s Comprehensive keyboard handling in terminals:

SequenceDoes
CSI = flags ; mode uset the flags: mode 1, the default, replaces them; 2 turns the given ones on; 3 turns them off
CSI ? uask for the flags; answered CSI ? flags u
CSI > flags upush the current flags on a stack and set flags, 0 if left out
CSI < n upop n entries, 1 if left out; emptying the stack clears the flags

The main screen and the alternate screen each have their own stack, so that an editor can change the flags on the alternate screen without knowing what the shell had set. A program finds out whether a terminal has the protocol by sending CSI ? u and then DA: a terminal that answers DA without answering CSI ? u does not.

A default xterm does not have it. Its parse tables send CSI ? u, CSI > u and CSI = u to the ground state, and ignore any sequence that starts CSI < (dec_table, dec2_table, dec3_table, csi_table). xterm has its own way of sending keys with modifiers, XTMODKEYS, which the protocol’s documentation argues against. The one CSI … u it does have is plain CSI u, with no prefix, which restores the cursor saved by CSI s, as SCO’s console did (CASE_ANSI_RC); that works the same in libghostty-vt.

libghostty-vt implements the protocol, with the two stacks, so it answers CSI ? u and the flags change what its key encoder sends. The cases follow a default xterm, which leaves every question unanswered, so those that ask are known differences.

Validation

KITTY-1: The query is not answered

\e[?u
|________|
reply none

KITTY-2: Detecting the protocol

\e[?u     # the flags, if it has them
\e[c      # DA, which every terminal answers
|________|
reply \e[?62;22c

KITTY-3: Setting flags

\e[=5u    # 1 and 4
\e[=2;2u  # and 2
\e[?u
|________|
reply none

KITTY-4: Push and pop

\e[>1u
\e[>3u
\e[<u
\e[?u
|________|
reply none

KITTY-5: Nothing on screen

A
\e[>1u
\e[=31u
\e[<u
B
|AB______|
cursor 1,3

KITTY-6: Without a prefix, CSI u restores the cursor

\e[2;3H
\e[s      # save
\e[1;1H
\e[u      # restore
X
|________|
|__X_____|
cursor 2,4
Every example on these pages runs in your browser, in libghostty-vt compiled to WebAssembly.