Key Encoding Table

The specification for terminal-input and for Lab 13. Every row should be a unit test.

Bytes are shown in hex and as escapes. CSI = 1b 5b · SS3 = 1b 4f.

Modifier parameter = 1 + shift(1) + alt(2) + ctrl(4) + super(8).


Ordinary Characters

KeyBytesSource
a61the text event — handles layouts, dead keys, IME
A (Shift+a)41text
é (dead key or IME)c3 a9text
日 (IME)e6 97 a5text
Space20text

Use the text event for these. Physical keys break on Dvorak and AZERTY; text handles composition and IME correctly.


Named Keys, Unmodified

KeyBytesEscapeNote
Enter0d\rCR, not LF. ICRNL converts it kernel-side.
Tab09\t
Shift+Tab1b 5b 5aCSI ZCBT (back-tab)
Backspace7fDELNot 08. DEC terminals sent DEL.
Ctrl+Backspace08BSConfigurable; some setups expect the reverse
Alt+Backspace1b 7fESC DEL
Escape1bESCAmbiguous with sequence starts
Delete1b 5b 33 7eCSI 3~
Insert1b 5b 32 7eCSI 2~
Home1b 5b 48CSI Hxterm form
End1b 5b 46CSI Fxterm form
Page Up1b 5b 35 7eCSI 5~
Page Down1b 5b 36 7eCSI 6~

Arrow Keys (Mode-Dependent)

KeyDECCKM reset (normal)DECCKM set (application)
Up1b 5b 41 CSI A1b 4f 41 SS3 A
Down1b 5b 42 CSI B1b 4f 42 SS3 B
Right1b 5b 43 CSI C1b 4f 43 SS3 C
Left1b 5b 44 CSI D1b 4f 44 SS3 D

Modified arrows always use the CSI form, even when DECCKM is set. That asymmetry is xterm's behavior, and programs depend on it:

KeyBytesEscape
Shift+Up1b 5b 31 3b 32 41CSI 1;2 A
Alt+Up1b 5b 31 3b 33 41CSI 1;3 A
Shift+Alt+Up...3b 34 41CSI 1;4 A
Ctrl+Up...3b 35 41CSI 1;5 A
Ctrl+Shift+Up...3b 36 41CSI 1;6 A
Ctrl+Alt+Up...3b 37 41CSI 1;7 A
Ctrl+Alt+Shift+Up...3b 38 41CSI 1;8 A

Home and End take the same form: CSI 1;5 H for Ctrl+Home.


Function Keys

KeyBytesEscapeLineage
F11b 4f 50SS3 PVT100
F21b 4f 51SS3 QVT100
F31b 4f 52SS3 RVT100
F41b 4f 53SS3 SVT100
F51b 5b 31 35 7eCSI 15~VT220
F61b 5b 31 37 7eCSI 17~VT220
F71b 5b 31 38 7eCSI 18~
F81b 5b 31 39 7eCSI 19~
F91b 5b 32 30 7eCSI 20~
F101b 5b 32 31 7eCSI 21~
F111b 5b 32 33 7eCSI 23~
F121b 5b 32 34 7eCSI 24~

Note the gaps: there is no 16~ and no 22~. They are historical, and getting them wrong shifts every subsequent function key.

Modified: the modifier goes after the number — CSI 15;2~ for Shift+F5, CSI 1;5 P for Ctrl+F1.


Control Combinations

The rule: Ctrl + char = uppercase(char) & 0x1F.

KeyByteNameNote
Ctrl+Space, Ctrl+@00NUL
Ctrl+A01SOH
Ctrl+B02STXtmux's default prefix
Ctrl+C03ETX→ SIGINT when ISIG
Ctrl+D04EOTVEOF
Ctrl+G07BEL
Ctrl+H08BS
Ctrl+I09HTThis IS Tab
Ctrl+J0aLFEnter when ICRNL is off
Ctrl+M0dCRThis IS Enter
Ctrl+Q11DC1VSTART
Ctrl+S13DC3VSTOP
Ctrl+U15NAKVKILL
Ctrl+V16SYNVLNEXT
Ctrl+W17ETBVWERASE
Ctrl+Z1aSUB→ SIGTSTP
Ctrl+[1bESCThis IS Escape
Ctrl+\1cFS→ SIGQUIT
Ctrl+]1dGS
Ctrl+^1eRS
Ctrl+_1fUS
Ctrl+?7fDEL

Ctrl+M, Ctrl+I, and Ctrl+[ are not analogous to Enter, Tab, and Escape — they are the same bytes. A program cannot distinguish them, which is why vim cannot bind Ctrl+M separately from Enter.

Ctrl with a digit or punctuation (Ctrl+1, Ctrl+5) has no legacy encoding. Send nothing, or use modifyOtherKeys. Sending a wrong byte is worse than sending none.


Alt / Meta

The rule: ESC prefix, then the key's normal bytes.

KeyBytesEscape
Alt+a1b 61ESC a
Alt+Shift+A1b 41ESC A
Alt+Enter1b 0dESC CR
Ctrl+Alt+c1b 03ESC ETX

Never use 8-bit meta (0x62 | 0x80 = 0xe2) — it is a valid UTF-8 lead byte and collides with text.

macOS: Option is a composing modifier by default (Option+B gives ∫). Provide an "Option as Meta" setting, default it on for the left Option key, and use the logical key rather than the text.


Keypad (DECKPAM / DECKPNM)

KeyNumeric mode (ESC >)Application mode (ESC =)
Keypad Enter0d1b 4f 4d SS3 M
Keypad +2b1b 4f 6b SS3 k
Keypad -2d1b 4f 6d SS3 m
Keypad *2a1b 4f 6a SS3 j
Keypad /2f1b 4f 6f SS3 o
Keypad 0–930–39SS3 p … SS3 y
Keypad .2e1b 4f 6e SS3 n

Paste

ConditionBytes
?2004 set1b 5b 32 30 30 7e + text + 1b 5b 32 30 31 7e (CSI 200~ … CSI 201~)
?2004 resetThe text alone

Always:

  • Normalize \r\n and \n to \r — the keyboard sends CR, and ICRNL converts. A pasted \n bypasses that path.
  • Strip CSI 201~ from the payload. Otherwise a crafted clipboard escapes the envelope and the remainder runs as typed input. This is a real, exploited attack.
  • Consider warning on a multi-line paste when the mode is off — it executes immediately.

Mouse

Only sent when a mouse mode (?1000/?1002/?1003) is enabled. Shift always bypasses reporting so the user can select text.

SGR encoding (?1006) — use this

   Press:    CSI < b ; x ; y M       (uppercase M)
   Release:  CSI < b ; x ; y m       (lowercase m)

   b = button + modifiers:
     0 = left   1 = middle   2 = right
     +4 shift   +8 alt       +16 ctrl
     +32 motion
     64 = wheel up   65 = wheel down
   x, y = 1-BASED cell coordinates. Decimal, so no limit.
EventBytes
Left press at (10, 5)1b 5b 3c 30 3b 31 30 3b 35 4d = CSI <0;10;5M
Left releaseCSI <0;10;5m
Right pressCSI <2;10;5M
Wheel upCSI <64;10;5M
Drag with left heldCSI <32;11;5M
Ctrl+left pressCSI <16;10;5M

Legacy X10 (the default without ?1006)

   CSI M Cb Cx Cy      each byte = value + 32

   FATAL LIMIT: one byte per coordinate, so columns/rows above 223 CANNOT be
   encoded. This is why ?1006 exists. Suppress the event rather than emit
   garbage.

Focus (?1004)

EventBytes
Focus gained1b 5b 49 CSI I
Focus lost1b 5b 4f CSI O

Used by vim (reload changed files) and tmux. Two lines to implement.


Beyond the Legacy Protocol

modifyOtherKeys (xterm)

   Enable:  CSI > 4 ; 2 m        Disable: CSI > 4 ; 0 m

   Keys with no legacy encoding become:
     CSI 27 ; <modifier> ; <codepoint> ~

   Ctrl+1        → CSI 27;5;49~
   Ctrl+Shift+A  → CSI 27;6;65~

The kitty keyboard protocol

   CSI > <flags> u    push    CSI < u    pop    CSI ? u    query

   flags: 1 = disambiguate escape codes   (fixes the Escape ambiguity)
          2 = report event types (press/repeat/release)
          4 = report alternate keys
          8 = report all keys as escape codes
         16 = report associated text

   Keys become:  CSI <unicode-key> [; <mods> [: <event>]] u

Recommended scope: the legacy table above, plus modifyOtherKeys, plus kitty flag 1 if you have the appetite. Full kitty support is a stretch goal.


The Escape Ambiguity

   Escape:    1b
   Up arrow:  1b 5b 41
              ^^ identical first byte

A program reading bytes cannot distinguish them without a timeout (25–50 ms is the modern recommendation; tmux's 500 ms default is why "vim feels laggy in tmux").

Your GUI does not have this problem — it receives real key events. A terminal-based mux client does. That is a genuine architectural advantage of a graphical client.


The Test

#![allow(unused)]
fn main() {
#[test]
fn key_encoding_table() {
    let d = TerminalModes::default();
    let mut app = d; app.insert(Mode::APP_CURSOR_KEYS);

    let cases: &[(Key, Modifiers, &TerminalModes, &[u8])] = &[
        (Named(Enter),      NONE,  &d,   b"\r"),
        (Named(Tab),        NONE,  &d,   b"\t"),
        (Named(Tab),        SHIFT, &d,   b"\x1b[Z"),
        (Named(Backspace),  NONE,  &d,   b"\x7f"),
        (Named(Backspace),  CTRL,  &d,   b"\x08"),
        (Named(Escape),     NONE,  &d,   b"\x1b"),
        (Named(ArrowUp),    NONE,  &d,   b"\x1b[A"),
        (Named(ArrowUp),    NONE,  &app, b"\x1bOA"),
        (Named(ArrowUp),    CTRL,  &d,   b"\x1b[1;5A"),
        (Named(ArrowUp),    CTRL,  &app, b"\x1b[1;5A"),   // CSI even under DECCKM
        (Named(Home),       NONE,  &d,   b"\x1b[H"),
        (Named(End),        NONE,  &d,   b"\x1b[F"),
        (Named(Delete),     NONE,  &d,   b"\x1b[3~"),
        (Named(Delete),     CTRL,  &d,   b"\x1b[3;5~"),
        (Named(F1),         NONE,  &d,   b"\x1bOP"),
        (Named(F5),         NONE,  &d,   b"\x1b[15~"),
        (Named(F12),        NONE,  &d,   b"\x1b[24~"),
        (Named(F5),         SHIFT, &d,   b"\x1b[15;2~"),
        (Char('c'),         CTRL,  &d,   b"\x03"),
        (Char('['),         CTRL,  &d,   b"\x1b"),
        (Named(Space),      CTRL,  &d,   b"\x00"),
        (Char('b'),         ALT,   &d,   b"\x1bb"),
        (Char('c'),         CTRL_ALT, &d, b"\x1b\x03"),
    ];
    for (key, mods, modes, want) in cases {
        assert_eq!(encode_key(*key, *mods, modes).as_deref(), Some(*want),
                   "key={key:?} mods={mods:?}");
    }
}
}

Verifying Against a Real Terminal

# Run your Lab 1 inspector in xterm and in your terminal; press the same keys
# in the same order; diff.
cargo run -p raw-inspector | tee /tmp/reference.txt     # in xterm
cargo run -p raw-inspector | tee /tmp/mine.txt          # inside your terminal
diff /tmp/reference.txt /tmp/mine.txt

Every difference is a bug or a deliberate decision. Write the table: key, reference bytes, your bytes, verdict.


See also: Input Encoding · Trace a Keystroke · Escape Sequence Cheat Sheet