Skip to content

CSI anatomy and parsing

Status: Standard (ECMA-48 §5.4).

Syntax

ESC [ parameter-bytes intermediate-bytes final-byte

ESC is 0x1b and [ is 0x5b. An eight-bit environment may use the single C1 byte 0x9b, but the two-byte form is the interoperable choice in UTF-8 sessions, where 0x9b is a valid continuation byte.

Part Byte range Purpose
Parameter bytes 0x30–0x3f Digits 0–9, separators ; and :, private markers <, =, >, ?
Intermediate bytes 0x20–0x2f Select a subfamily of the final command; SP means literal 0x20
Final byte 0x40–0x7e Terminates and identifies the control function

A private marker, if present, must be the first parameter byte. ECMA-48 reserves final bytes 0x70–0x7e (p–~) for private use, which is why so many DEC and xterm commands end in p, q, t, or ~.

Parameters and defaults

Ps is one numeric parameter, Pm several separated by ;, Pt text. An omitted or empty parameter is not universally equivalent to zero; the command definition decides. CSI H moves to row 1, column 1 because the default is 1; CSI J erases to the end of the screen because the default is 0.

Parameters are decimal, non-negative, and unbounded in the grammar. Parsers impose limits: xterm caps the count at 30 and values at 65535; other emulators use 16 or 32 parameters. Values past the limit are discarded, not wrapped.

Sub-parameters

ECMA-48 allows : to separate sub-parameters within one parameter. SGR is the main user; the Kitty keyboard protocol also carries modifiers and event types as sub-parameters of CSI … u. The SGR forms:

CSI 4 : 3 m                curly underline
CSI 58 : 2 : : 255 : 0 : 0 m   underline color, truecolor
CSI 38 : 2 : : r : g : b m     foreground, with empty color-space id

The colon form is unambiguous when parameters are combined, because the parser knows how many sub-parameters belong to 38. The semicolon form 38;2;r;g;b is far more widely supported but cannot be skipped by a terminal that does not understand it. See SGR.

Terminals that predate sub-parameters may treat : as a parameter terminator or ignore the whole sequence. Even emulators that handle colons in SGR do not always handle them in every other CSI function, because the sub-parameter support was retrofitted for colors rather than added to the parser generally. Applications should consult the terminfo Smulx/Setulc capabilities or probe before relying on colons.

Private markers and function identity

ECMA-48 intended a private marker to select a private variant of the same function: SM and RM say that private modes may be implemented through private parameters, which is exactly what CSI ? Pm h is. SCO and xterm instead used the markers to define distinct functions (CSI > Pm m has nothing to do with SGR), and every emulator since has followed. A function is therefore identified by the marker, the intermediates, and the final byte together, and a parser must keep all three.

ECMA-48 reserves the standard final bytes @–o and hands out p–~ for private use, which is why DEC's commands end there. The unused space between the two is the intermediate bytes: an intermediate plus a standard final byte defines a function that collides with nothing, and newer sequences use it. xterm's color stack is CSI # P, CSI # Q, CSI # R; kitty's unscroll is CSI Ps + T. ECMA-48 itself planned ! for no-parameter functions and never assigned one. The historical alternative, private functions on bare standard finals (SCO's CSI U for reset, colliding with NP), is why some ECMA-48 functions are unusable on terminals that claim to support them.

Two marker placements that look plausible are not accepted: a marker in the middle of a parameter string (CSI 4 ; ? 7 h) and a marker after the parameters (CSI 4 ? p). VT320 and VT420 hardware ignored both; the reference parser treats a marker after a digit as an error and consumes the sequence without acting on it.

Reading examples

CSI 31 m         SGR foreground color 1
CSI ? 25 l       reset DEC private mode 25 (hide cursor)
CSI 2 SP q       DECSCUSR steady block
CSI > 4 ; 2 m    xterm modifyOtherKeys level 2
CSI 1 ; 5 A      cursor up, modifier 5 (Ctrl) — this is a key report, not a command

The last example shows why direction matters: the same grammar carries key reports from the emulator to the application.

Parser requirements

A PTY read is not a protocol message. One sequence may be split across reads and one read may contain text plus many sequences. A parser therefore keeps state between writes and dispatches only on the final byte.

The de facto reference is Paul Williams' VT500-series state machine, which most emulators implement directly. Its properties worth knowing:

  • ESC inside a sequence aborts it and starts a new escape;
  • CAN (0x18) and SUB (0x1a) abort the sequence;
  • C0 controls other than those two are executed inside a CSI sequence without aborting it (so CSI 3 LF 1 m is legal, if unwise);
  • DEL (0x7f) is ignored;
  • an unrecognized final byte dispatches to nothing; the bytes are consumed silently and never printed;
  • ECMA-48 §9 says that bytes 0xa0–0xff inside a control sequence are treated as their 0x20–0x7f counterparts (the eight-bit "column 10–15" rule). No emulator applies this in a UTF-8 session, where those bytes are continuation bytes; parsers either reject them or abort the sequence.

Malformed or unbounded parameter strings must not consume unlimited memory. Emulators typically stop accumulating after a fixed number of parameter bytes and drop the sequence.

Sending probes

tools/sendcsi list
tools/sendcsi steady-block
printf '\033[4:3m curly \033[0m\n'

The helpers write only the requested bytes, so tools/sendcsi blink-bar | od -c shows exactly what a parser receives.

Sources