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:
ESCinside a sequence aborts it and starts a new escape;CAN(0x18) andSUB(0x1a) abort the sequence;- C0 controls other than those two are executed inside a CSI sequence
without aborting it (so
CSI 3LF1 mis 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–0xffinside a control sequence are treated as their0x20–0x7fcounterparts (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.