# Controls Inside Sequences

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

- **Sequence:** `CSI P… BS P… F`
- **DEC STD 070:** [§3.5.1.1 Precedence of Control Functions, p. 3-18](https://archive.org/details/bitsavers_decstandar0VideoSystemsReferenceManualDec91_74264381/page/n122/mode/1up)
- **xterm:** [Definitions](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)

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](https://archive.org/details/bitsavers_decstandar0VideoSystemsReferenceManualDec91_74264381/page/n122/mode/1up)).
xterm's parse tables do it, giving each C0 control its usual action in every state
of a control sequence
([`csi_table`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/VTPrsTbl.c#L477-L512)).
DEL, `7F`, is ignored. The exceptions are [CAN](https://control-codes.page/c0/can/index.html.md) and
[SUB](https://control-codes.page/c0/sub/index.html.md), which abandon the sequence, and [ESC](https://control-codes.page/c0/esc/index.html.md), 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`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/VTPrsTbl.c#L7619-L7780)), but it keeps the rest in the
string, DEL and NUL among them ([`doparsing`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/charproc.c#L3384-L3456)).
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`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/misc.c#L6181-L6235),
[`NonLatin1`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/ptyx.h#L383-L386)), 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](https://control-codes.page/c0/bel/index.html.md) 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](https://control-codes.page/esc/st/index.html.md), `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`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/charproc.c#L3484-L3488)).

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

| Terminal | Version | Support | Notes |
| --- | --- | --- | --- |
| VT220 | [manual](https://vt100.net/docs/vt220-rm/), 2nd ed., 1984 | Partly | [§4.2 Control Characters](https://vt100.net/docs/vt220-rm/chapter4.html#S4.2): CAN, SUB and ESC cancel a sequence and DEL is ignored; other controls inside one are not addressed |
| VT510 | [manual](https://vt100.net/docs/vt510-rm/), 1st ed., 1993 | Partly | [§4.3.5](https://vt100.net/docs/vt510-rm/chapter4.html#S4.3.5) and [Table 4–1](https://vt100.net/docs/vt510-rm/chapter4.html#T4-1): ESC, CAN and SUB cancel a sequence; other controls inside one are not addressed |
| xterm | patch 412, [xterm-411a](https://github.com/ThomasDickey/xterm-snapshots/tree/xterm-411a) | Yes | keeps controls in a string; in a title they become `?`, but for LF and tab |
| libghostty-vt | [0f171f6](https://github.com/ghostty-org/ghostty/tree/0f171f650a1cbbd6df4a2573b6b0c7de57cc6d90) | Yes | CTL-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 |
| kitty | 0.49.1, [run in a VM](https://git.jcollie.dev/jeff/control-codes/src/branch/main/emulators/results/kitty.json) | Partly | fails CTL-4 of the 4 cases that show it working |
| foot | 1.28.0, [run in a VM](https://git.jcollie.dev/jeff/control-codes/src/branch/main/emulators/results/foot.json) | Yes | passes the 4 cases that show it working |
| pyte | 0.8.2, [run as a library](https://git.jcollie.dev/jeff/control-codes/src/branch/main/emulators/results/pyte.json) | Partly | fails CTL-4 of the 4 cases that show it working |
| xterm.js headless | 6.0.0, [run as a library](https://git.jcollie.dev/jeff/control-codes/src/branch/main/emulators/results/xtermjs.json) | Yes | passes the 4 cases that show it working |
| alacritty_terminal | 0.26.0, [run as a library](https://git.jcollie.dev/jeff/control-codes/src/branch/main/emulators/results/alacritty.json) | Yes | passes the 4 cases that show it working |
| rxvt-unicode | 9.31, [run in a VM](https://git.jcollie.dev/jeff/control-codes/src/branch/main/emulators/results/urxvt.json) | Partly | fails CTL-4 of the 4 cases that show it working |
| VTE | 0.84.1, [run as a library](https://git.jcollie.dev/jeff/control-codes/src/branch/main/emulators/results/vte.json) | Yes | passes the 4 cases that show it working |

## Validation

### CTL-1: Backspace inside a control sequence

Input, one step per line:

```text
ABCD
\e[2\bC   # BS, then CUF 2
X
```

Expected screen:

```text
|ABCD_X__|
cursor 1,7
```

### CTL-2: Carriage return

Input, one step per line:

```text
ABCD
\e[\r2C   # CR, then CUF 2
X
```

Expected screen:

```text
|ABXD____|
cursor 1,4
```

### CTL-3: Line feed, between the parameters

Input, one step per line:

```text
AB
\e[\n2C
X
```

Expected screen:

```text
|AB______|
|____X___|
cursor 2,6
```

### CTL-4: DEL is ignored

Input, one step per line:

```text
AB
\e[1\x7f;5H
X
```

Expected screen:

```text
|AB__X___|
cursor 1,6
```

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

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e]2;a\bb\nc\x00d\x1fe\a
```

Expected screen:

```text
|________|
title "a?b\nc"
```

### CTL-6: DEL becomes ? too

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e]2;a\x7fb\a
```

Expected screen:

```text
|________|
title "a?b"
```

### CTL-7: An ESC abandons a string

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e]2;cut\e7   # ESC 7, DECSC, not ST
X
```

Expected screen:

```text
|X_______|
title ""
```

---

This is the Markdown version of <https://control-codes.page/parser/controls/>. On that page every validation case runs live in libghostty-vt, the terminal emulation core of Ghostty, compiled to WebAssembly.

The validation cases are written in the notation described in <https://control-codes.page/notation/index.html.md>.
