^[control-codes
live in libghostty-vt

Parsing

C0 in sequences Controls Inside Sequences

View as Markdown

What a control character does when it arrives in the middle of a control sequence or a string.

CSI P… BS P… F
DEC STD 070
§3.5.1.1 Precedence of Control Functions, p. 3-18
xterm
Definitions

A control sequence is not always unbroken. A program can be interrupted, or a sequence can be written in two pieces with something else in between, and the terminal then meets a control character part-way through one.

In a control sequence a C0 control is carried out at once, and the sequence then carries on as if it had not been there: CSI 2 BS C moves back one column and then forward two. DEC STD 070 asks for this: control characters take precedence over the sequences around them (§3.5.1.1, p. 3-18). xterm’s parse tables do it, giving each C0 control its usual action in every state of a control sequence (csi_table). DEL, 7F, is ignored. The exceptions are CAN and SUB, which abandon the sequence, and ESC, which abandons it and starts another.

In a string, the text of an OSC, DCS, SOS, PM or APC, xterm acts on no C0 control except the ones that end or abandon it (sos_table), but it keeps the rest in the string, DEL and NUL among them (doparsing). What becomes of them depends on what the string is for. In a title, xterm turns each control into ?, except LF and tab, which it keeps, and DEL into ? too (ChangeGroup, NonLatin1), and a NUL ends the title there. So OSC 2 ; a BS b LF c NUL d ST sets the title a?b, a line feed, and c. BEL ends an OSC, as xterm has always allowed, and rings the bell inside a DCS. An ESC ends the string only as the first half of ST, ESC \. Followed by anything else, it abandons the string and starts an escape sequence, and the string is thrown away: xterm keeps collecting it only while it waits for the byte after the ESC, and carries out an OSC only on its terminator (doparsing).

libghostty-vt leaves the C0 controls out of a string instead, and keeps a DEL as it is, so a title comes out differently from xterm’s. And an ESC that is not part of ST ends an OSC and carries it out before starting the escape sequence, so a title cut short that way is still set. Those cases are known differences.

Support

TerminalVersionSupportNotes
VT220manual, 2nd ed., 1984Partly§4.2 Control Characters: CAN, SUB and ESC cancel a sequence and DEL is ignored; other controls inside one are not addressed
VT510manual, 1st ed., 1993Partly§4.3.5 and Table 4–1: ESC, CAN and SUB cancel a sequence; other controls inside one are not addressed
xtermpatch 412, xterm-411aYeskeeps controls in a string; in a title they become ?, but for LF and tab
libghostty-vt0f171f6YesCTL-5, CTL-6: leaves C0 controls out of a title and keeps DEL; CTL-7: an ESC that is not ST still carries out the OSC
kitty0.49.1, run in a VMPartlyfails CTL-4 of the 4 cases that show it working
foot1.28.0, run in a VMYespasses the 4 cases that show it working
pyte0.8.2, run as a libraryPartlyfails CTL-4 of the 4 cases that show it working
xterm.js headless6.0.0, run as a libraryYespasses the 4 cases that show it working
alacritty_terminal0.26.0, run as a libraryYespasses the 4 cases that show it working
rxvt-unicode9.31, run in a VMPartlyfails CTL-4 of the 4 cases that show it working
VTE0.84.1, run as a libraryYespasses the 4 cases that show it working

Validation

CTL-1: Backspace inside a control sequence

ABCD
\e[2\bC   # BS, then CUF 2
X
|ABCD_X__|
cursor 1,7

CTL-2: Carriage return

ABCD
\e[\r2C   # CR, then CUF 2
X
|ABXD____|
cursor 1,4

CTL-3: Line feed, between the parameters

AB
\e[\n2C
X
|AB______|
|____X___|
cursor 2,6

CTL-4: DEL is ignored

AB
\e[1\x7f;5H
X
|AB__X___|
cursor 1,6

CTL-5: Controls in a title become ?, but for LF, and NUL ends it

\e]2;a\bb\nc\x00d\x1fe\a
|________|
title "a?b\nc"

CTL-6: DEL becomes ? too

\e]2;a\x7fb\a
|________|
title "a?b"

CTL-7: An ESC abandons a string

\e]2;cut\e7   # ESC 7, DECSC, not ST
X
|X_______|
title ""
Every example on these pages runs in your browser, in libghostty-vt compiled to WebAssembly.