# control-codes
A small reference for terminal control sequences. Each page documents one
sequence and gives validation cases: some input, and the screen it should
leave behind. The cases run live. When a page is opened, the browser loads
[libghostty-vt](https://github.com/ghostty-org/ghostty), the terminal emulation
core of Ghostty, compiled to WebAssembly. It feeds each case into a real
terminal and draws the result next to the expected screen. You can step
through the input one line at a time.
The site is built with [Zine](https://zine-ssg.io). Its Forgejo workflow
publishes it from `main` to .
## Where it lives
- — the primary home.
- — mirror.
```console
git clone https://git.jcollie.dev/jeff/control-codes.git
```
On [Radicle](https://radicle.xyz/), the peer-to-peer forge, the repository is
`rad:z2dBmdsQqFL24uX7hpoHgxe9N2QdU`. A Radicle repository can only be found by
its ID, so this is how to seed or clone it:
```sh
rad clone rad:z2dBmdsQqFL24uX7hpoHgxe9N2QdU
```
Cloning also seeds the repository, which helps keep it available on the
network.
## What is covered
250 pages in nine groups:
- C0 controls: BEL, BS, CAN, CR, DC1/DC3, ENQ, ESC, FF, HT, LF, NUL, SO/SI,
SUB, VT
- Escape sequences and C1 controls: CSI, DCS, DECALN, DECAUPSS/DECRQUPSS,
DECBI/DECFI, DECDWL/DECDHLT/DECDHLB/DECSWL, DECKPAM/DECKPNM, DECSC/DECRC,
HTS, LS2/LS3, NEL, RI, RIS, SOS/PM/APC, SPA/EPA, SS2/SS3, ST
- Control sequences: CBT, CHA, CHT, CNL, CPL, CSI ? u/CSI = u/CSI > u/CSI < u,
CTC, CUB, CUD, CUF, CUP, CUU, DA, DCH, DECCARA, DECCRA, DECDSR,
DECELR/DECSLE/DECRQLP/DECEFR/DECLKD, DECERA, DECFRA, DECIC/DECDC, DECRQCRA,
DECRQM, DECRQPSR, DECRQTSR/DECTSR/DECRSTS, DECRST, DECSASD/DECSSDT,
DECSCA/DECSED/DECSEL, DECSCL, DECSCPP, DECSCUSR, DECSET, DECSLRM, DECSNLS,
DECSR/DECSRC, DECSTBM, DECSTR, DECTST, DL, DSR, ECH, ED, EL, HPA, HPB, HPR,
HVP, ICH, IL, MC, NP/PP/PPA/PPR/PPB, REP, RM, SCP, SD, SGR, SIMD, SL/SR,
SM, SPD, SU, TBC, VPA, VPB, VPR, XTMODKEYS/XTQMODKEYS, XTPUSHSGR/XTPOPSGR,
XTRESTORE, XTSAVE, XTVERSION, XTWINOPS
- ANSI modes: BDSM, CRM, DCSM, EBM, ERM, FEAM, FETM, GATM, GRCM, HEM, IRM,
KAM, LNM, MATM, PUM, SATM, SRM, SRTM, TSM, TTM, VEM, ZDM
- Private modes: ?1004, ?12, ?1243/?2500/?2501, ?2004, ?2026, ?2028, ?2031,
?2033, ?46, ?47/?1047/?1048/?1049, ?9/?1000 to ?1003/?1005 to ?1007/?1015/?1016,
DECARM, DECAWM, DECCKM, DECCOLM, DECHCCM/DECVCCM/DECPCCM, DECKBUM, DECNRCM,
DECOM, DECPEX/DECPFF, DECSCLM, DECSCNM, DECSDM, DECTCEM
- Device control strings: DECDLD, DECRQSS/DECRPSS, DECUDK, ReGIS, Sixel,
XTGETTCAP, XTGETXRES, XTSETTCAP
- Operating system commands: OSC 0, OSC 1, OSC 10, OSC 104, OSC 105, OSC 106,
OSC 11, OSC 110, OSC 111, OSC 112, OSC 113, OSC 114, OSC 115, OSC 116, OSC
117, OSC 118, OSC 119, OSC 12, OSC 13, OSC 133, OSC 1337, OSC 14, OSC 15,
OSC 16, OSC 17, OSC 176, OSC 18, OSC 19, OSC 2, OSC 21, OSC 21337, OSC 22,
OSC 3, OSC 30, OSC 30001, OSC 3008, OSC 30101, OSC 31, OSC 32, OSC 34, OSC
4, OSC 440, OSC 46, OSC 5, OSC 50, OSC 51, OSC 52, OSC 5522, OSC 555, OSC 6,
OSC 60, OSC 61, OSC 62, OSC 633, OSC 66, OSC 666, OSC 7, OSC 701, OSC 702,
OSC 710, OSC 711, OSC 712, OSC 713, OSC 72, OSC 720, OSC 721, OSC 776, OSC
777, OSC 7770, OSC 7777, OSC 8, OSC 9, OSC 9 ; 1, OSC 9 ; 10, OSC 9 ; 11,
OSC 9 ; 12, OSC 9 ; 2, OSC 9 ; 3, OSC 9 ; 4, OSC 9 ; 5, OSC 9 ; 6, OSC 9 ;
7, OSC 9 ; 8, OSC 9 ; 9, OSC 99, OSC 9999, OSC I, OSC L, OSC P, OSC R, OSC l
- Parsing: Controls Inside Sequences, Parameters, UTF-8 and C1 Controls
- Proposals: APC 25a1, the Glyph Protocol; OSC 88, the Terminal Resume
Protocol; OSC 533, Screenshot; OSC 7501, the Program Status Protocol;
?2034 and SBQUERY, the Semantic Block Query; and XTCAPTURE, Buffer
Capture. A proposal is summarized as its specification is written, with a
link to the version read, and has no support table and no cases, since few
terminals implement one yet.
The other 78 ECMA-48 functions, the ones for data communications, printing,
forms and bidirectional text, are listed by purpose on one page, "The Rest of
ECMA-48", whose cases show that libghostty-vt ignores them.
There are 878 cases in all. 241 are marked as *known differences*:
libghostty-vt is expected to fail them, and each page explains why.
| Case | What libghostty-vt does | What the sources expect |
| --- | --- | --- |
| DCS-6 | BEL inside a device control string is ignored, as DEC STD 070 recommends for a control there | it is ignored as a terminator but still rings the bell (xterm, `CASE_BELL`) |
| CAN-5 | CAN does not cancel a device control string: leaving a DCS always unhooks it (`Parser.zig`), so a request still completes | CAN cancels the string in progress (VT510 manual, DEC STD 070) |
| SL-1 to SL-7 | SL and SR (`CSI Ps SP @`, `CSI Ps SP A`) are ignored | the scrolling region moves left or right by `Ps` columns (ECMA-48, xterm) |
| SIMD-1 to SIMD-4 | `CSI Ps ^` is ignored | xterm takes it as SD and scrolls down `Ps` lines |
| ALT-15, ALT-16 | DECRQM answers each of `47`, `1047`, `1048` and `1049` from its own flag | `47`, `1047` and `1049` report whether the alternate screen is showing, and `1048` whether a cursor is saved (xterm) |
| ALT-17, ALT-18 | mode `1046` is unknown: DECRQM answers `0`, and resetting it does not stop the switch to the alternate screen | xterm reports the mode backwards, and resetting it forbids the switch |
| MOUSE-3 to MOUSE-5, MOUSE-7 | DECRQM answers each mouse mode from its own flag, so a replaced mode is still reported as set | one event mode and one encoding at a time, and resetting any event mode turns tracking off (xterm) |
| MOUSE-6 | mode `1001`, highlight tracking, is unknown | xterm has it |
| MOUSE-10 | alternate scroll, `1007`, is on to begin with | off, unless the `alternateScroll` resource is set (xterm) |
| SYNC-1 | mode `2026` is implemented, and DECRQM answers `1` or `2` | a default xterm does not recognize it and answers `0` |
| COLORSCHEME-1 | mode `2031` is implemented, and DECRQM answers `1` or `2` | a default xterm does not recognize it and answers `0` |
| VISIBILITY-1 to VISIBILITY-3 | mode `2033` is implemented: DECRQM answers `1` or `2`, and setting the mode or asking with `CSI ? 998 n` sends a visibility report | a default xterm does not recognize the mode, answers `0`, and sends no report |
| OSC10-4 | an empty color in `OSC 10 ; ; blue` is passed over, so `blue` sets the foreground | the empty one is skipped and `blue` sets the background (xterm) |
| OSC12-3, and the OSC 13 to 19 and 113 to 119 pages | `OSC 13` to `OSC 19` and their resets are ignored | xterm answers for the mouse pointer's, the Tektronix window's and the highlight colors, and resets them |
| OSC104-5 to OSC104-7 | `OSC 104` skips an entry that is not a number and goes on, resets the whole palette when no number is left, and has no special colors | xterm stops at the first entry that is not a number, and `OSC 104` alone tries the special colors too, which it cannot reset |
| OSC2-8 | an empty title clears the title | a default xterm sets the title `xterm` |
| OSC50-1, OSC60-1, OSC61-1, OSC62-1 | xterm's font and allowed-feature queries are not answered | xterm answers them |
| OSC50-3 | Konsole's `OSC 50 ; CursorShape=1` is ignored | xterm takes it for a font name, cannot load it, and rings the bell |
| OSC5-1, OSC5-2, OSC105-1 to OSC105-3 | `OSC 5` and `OSC 105`, xterm's special colors, and palette numbers past 255 are ignored | xterm answers for its special colors (xterm) |
| OSC7-2 | the working directory is kept, and given to the host | a default xterm ignores `OSC 7` |
| OSC8-2 | the text becomes a hyperlink | a default xterm ignores `OSC 8` |
| OSC133-2, OSC133-3 | cells are marked as prompt or input | a default xterm ignores `OSC 133` |
| PROGRESS-2, PROGRESS-3, CONEMU9-1, CONEMU12-1 | ConEmu's progress reports go to the host, `OSC 9 ; 9` sets the working directory, and `OSC 9 ; 12` marks a prompt | a default xterm ignores `OSC 9` |
| ITERM2-1 | `OSC 1337 ; CurrentDir` sets the working directory | a default xterm ignores `OSC 1337` |
| OSC22-5, OSC22-6 | a CSS name sets that shape, and an unknown name is ignored | xterm knows only X cursor names, and gives the I-beam for any other |
| OSC21-1 to OSC21-3 | kitty's color protocol is implemented on the same colors as OSC 4 and OSC 10 | a default xterm ignores `OSC 21` |
| OSC72-1, OSC5522-1, OSC5522-2 | `OSC 72 ; t=q` is answered, and `OSC 5522` requests are answered with `EPERM` or `ENOSYS` | a default xterm ignores them |
| OSC-l-1, OSC-l-2 | the Sun and dtterm form, `OSC l title ST`, is ignored | it sets the window title (xterm) |
| XTWINOPS-2 | `CSI 11 t` is not answered | `CSI 1 t`, not iconified (xterm) |
| XTWINOPS-4, XTWINOPS-5 | there is no title stack, so `CSI 23 t` does not bring the title back | the title pushed with `CSI 22 t` is restored (xterm) |
| XTPUSHSGR-1 to XTPUSHSGR-3, XTPUSHSGR-5 to XTPUSHSGR-7 | `CSI # {` and `CSI # }` are ignored | the attributes are pushed onto a stack ten deep and popped from it, some or all of them, and a reset leaves the stack alone (xterm) |
| XTVERSION-1, XTVERSION-2 | the answer is libghostty's own name, and is given for any parameter | `DCS > \| XTerm(412) ST`, and only for `0` (xterm) |
| DECRQCRA-7 | with only `Pi` and `Pp`, the rectangle's bounds are left out and are the whole screen | xterm reads the two parameters as the top and left, so `CSI 7 ; 1 * y` sums nothing and answers `0000` |
| DECRQSS-3, DECRQSS-4 | SGR attributes are reported in numerical order, and double underline as `4:2` | xterm's fixed order, and `21` |
| DECRQSS-6 | DECSLRM is reported only once left and right margins are enabled | always (xterm) |
| DECRQSS-8 to DECRQSS-11 | DECSCA, DECSCL, DECSLPP, DECSCPP, DECSNLS and DECSACE are not reported | xterm, a VT420, reports them, as DEC STD 070 lists |
| XTGETTCAP-4, XTGETTCAP-6 | a key's answer is a fixed string from Ghostty's terminfo, as if DECCKM were set | what the key sends now, `CSI A` until DECCKM is set (xterm) |
| XTGETTCAP-8 to XTGETTCAP-12 | termcap names and `TN` are not answered, an unknown name gets no answer, each name gets its own `DCS … ST`, and names come back in upper case | xterm answers termcap names and `TN=xterm`, answers `DCS 0 + r ST` for an unknown name, puts several names in one answer, and writes each name back as it was sent |
| XTSETTCAP-2, XTSETTCAP-3 | `DCS + p` is ignored | an empty name, or one it cannot find, rings the bell (xterm) |
| XTGETXRES-1 to XTGETXRES-3 | `DCS + Q` is ignored | xterm answers with its resources' values |
| XTMODKEYS-1 to XTMODKEYS-6 | only `modifyOtherKeys` 2 is kept, and XTQMODKEYS and its DECRQSS form are not answered | xterm keeps every modifier resource, answers for them, and changes what keys send (xterm) |
| XTRESTORE-5 to XTRESTORE-7 | a mode never saved is restored to its default, and RIS returns every saved state to the default | xterm's saved states start out zeroed and survive RIS, so restoring an unsaved mode resets it (xterm) |
| KITTY-1 to KITTY-4 | the kitty keyboard protocol is implemented, so `CSI ? u` is answered | a default xterm ignores `CSI ? u`, `CSI = u`, `CSI > u` and `CSI < u` |
| SGR-1 | bold red keeps color 1 | bold text in one of the eight basic colors takes the bright one, under `boldColors` (xterm) |
| DECELR-1, DECELR-3 | the letter after DECELR, DECSLE or DECEFR is printed | a default xterm, built without the locator, ignores the rest of the sequence up to the next final byte, swallowing the letter |
| OSC104-2 | `OSC 104 ; 1 ; 3` resets colors 1 and 3 | xterm reads past the end of the list into what an earlier OSC left in its buffer, and resets color 2 too |
| OSC-P-1 to OSC-P-3, OSC-R-1, OSC-R-2 | the Linux console's `OSC P` and `OSC R` are ordinary OSCs, ignored | xterm's `brokenLinuxOSC` abandons them at their length, rings the bell, and prints what follows |
| SGR-4 | the underline styles `4:2` to `4:5` are drawn | a default xterm takes subparameters only for `38` and `48`, and skips the rest |
| CTL-5 to CTL-7 | C0 controls are left out of a title and a DEL is kept, and an ESC that cuts an OSC short carries it out | controls and DEL become `?` but for LF and tab, a NUL ends the title, and the cut-short OSC is thrown away (xterm) |
| PRM-2, PRM-3 | a sequence with more than 24 parameters is ignored | xterm keeps 30, and runs the digits of any more into the 30th |
| U8-3 to U8-5, CSI-5, NEL-4 | every invalid piece of UTF-8 is U+FFFD | a surrogate is one U+FFFD, and a stray byte before printable text is read as Latin-1 or as a C1 control, which is ignored (xterm) |
| CRM-1, CRM-2 | CRM is reported as not recognized, `0` | reported as reset, `2` (xterm) |
| DECSCUSR-5 | `CSI 0 SP q` gives the host's default cursor, which is not blinking unless the host says so | `0` is a blinking block (VT510 manual, xterm) |
| the VT420 column and rectangle pages (DECIC, DECBI, DECCRA, DECERA, DECFRA, DECCARA) and DECSCA-10 | DECIC, DECDC, DECBI, DECFI, DECCRA, DECERA, DECFRA, DECSERA, DECCARA, DECRARA and DECSACE are ignored: libghostty-vt identifies itself as a VT220 and has no handlers for them | xterm, a VT420 by default (`DFT_DECID`), implements them all |
| DECDWL-1 to DECDWL-3 | line size (`ESC # 3` to `ESC # 6`) is ignored, so a double-width line still holds every column | text wraps and the cursor is clamped at half the width (xterm, DEC STD 070) |
| DECDSR-3 to DECDSR-8 | DEC's private status requests (`CSI ? Ps n`) other than DECXCPR get no answer: only 5, 6, `?6` and libghostty-vt's own are in `device_status.zig` | xterm, as a VT420, answers them all |
| DECRQPSR-1 to DECRQPSR-5 | DECRQPSR and DECRSPS (cursor information and tab stop reports) are not implemented | xterm answers and restores them |
| DECSCL-1, DECSCL-2, DECSCL-4 | DECSCL is not implemented, so the conformance level and its soft reset never change | xterm limits its features to the selected level and soft-resets |
| DECSDM-1, DECSDM-2 | there is no mode 80, so DECRQM reports it as not recognized, `0` | reset, `2`, and still reset after `CSI ? 80 h`, which a default xterm ignores without sixel graphics |
| DECSCPP-1, DECSCPP-2 | DECSCPP is ignored, so a cursor past the new width stays put | xterm moves it to column 80 at once |
| DECSASD-1, -2, -5 | after `CSI 1 $ }` text is no longer printed on the main display | a default xterm has no status line and ignores DECSASD and DECSSDT, so text prints as usual |
| DECAUPSS-1 to DECAUPSS-3 | DECRQUPSS and DECAUPSS are not implemented | xterm answers and, after `CSI & u`, swallows the next character, a bug the case keeps |
| the DEC mode pages for DECARM, DECNRCM, DECKBUM, DECHCCM and DECPEX | these modes are reported as not recognized, and there are no national character sets | xterm reports them as set, reset or permanently reset, and has the national sets |
| CTC-1 to CTC-4 | CTC (`CSI W`) sets and clears tab stops, and DECST8C (`CSI ? 5 W`) resets them | a default xterm ignores `CSI W`, and runs DECST8C only as a VT510 or later |
| HPB-1 to HPB-3, VPB-1 to VPB-3 | HPB (`CSI j`) and VPB (`CSI k`) move the cursor | a default xterm ignores both |
| DECUDK-2 | no answer to `CSI ? 25 n` | xterm answers that its user-defined keys are unlocked, `CSI ? 20 n` |
| DA-5 | `CSI 1 c` is answered like a request: the DA dispatch in `stream.zig` never looks at the parameter | a non-zero DA identifies the sender and asks nothing (ECMA-48, DEC STD 070, xterm) |
| SUB-1, SUB-2 | SUB abandons a sequence but shows nothing: `stream.zig` only returns the parser to ground | SUB shows a reversed question mark (DEC STD 070, VT510 manual, xterm) |
Each known difference is still run on every page load. If Ghostty fixes one,
the page shows "passes now" and the case checker fails, which is the signal
to remove the marker.
`COVERAGE.md` is the checklist for what to add next. It lists all 162
control functions in ECMA-48 and the DEC private functions DEC STD 070 adds,
with which sources define each, whether libghostty-vt implements it, and
whether this site documents it yet.
## Layout
| Path | What it is |
| --- | --- |
| `website/content/` | one SuperMD page per sequence, under `c0/`, `esc/`, `csi/`, `ansi/`, `modes/`, `dcs/` and `osc/`, the pages on parsing under `parser/`, and the guide pages |
| `website/layouts/` | the SuperHTML templates |
| `website/assets/vt.js` | the wasm wrapper: a `Terminal` with `write`, `cursor`, `title`, `pwd`, `colors`, `screen` (each cell's text, style, hyperlink and semantic mark), and the replies and bells it sends back |
| `website/assets/vtcase.js` | parses the case notation, runs a case and compares the result |
| `website/assets/cases.js` | the case widget on each page, and the pass tallies on the index pages |
| `website/assets/sequence.js` | the byte-by-byte syntax diagram |
| `website/assets/diagram.js` | draws a page's Mermaid diagrams, such as DECCOLM's decision tree, in the site's colors; Mermaid is loaded only on a page that has one |
| `website/assets/toc.js` | the page's table of contents in the right-hand column, built from its headings, with each case's verdict and the part of the page in view |
| `tools/check-cases.mjs` | runs every case under Node with the same code as the pages |
| `tools/markdown.mjs` | writes a Markdown copy of every page, and `llms.txt` indexing them |
| `tools/sitemap.mjs` | writes `sitemap.xml`, dating each page by its source's last commit |
| `tools/favicon.sh` | renders `favicon.ico` and `apple-touch-icon.png` from `website/assets/favicon.svg`, the `^[` of the masthead |
| `tools/order.mjs` | works out the order of each section's pages, by mnemonic, mode number or OSC number, and writes it into the section's `index.smd`; `--check` fails if one is out of date |
| `tools/cases.mjs` | finds the validation cases in the SuperMD sources, for the case checker and the harness |
| `emulators/` | runs the cases in real terminals, in NixOS virtual machines, and in pyte, xterm.js, alacritty_terminal and VTE, in-process, and checks the reference xterm's run in CI; see [Running the cases in other terminals](#running-the-cases-in-other-terminals) |
| `tools/support.mjs` | writes the Support table into each sequence page from `website/support.json` and the measured results, and the Support page (`website/content/support.smd`), every sequence against every terminal, below its hand-written introduction; `--check` fails if a page has no data or a table is out of date |
| `tools/finals.mjs` | writes the table of control sequences by final byte into `website/content/finals.smd`, from every page's `sequence` and `also_sequences` and from the ECMA-48 functions `COVERAGE.md` lists without a page; `--check` fails if it is out of date, or if a documented function is on no page's list |
| `tools/pages.mjs` | the list of pages and their frontmatter, shared by the tools above |
| `ghostty-vt-wasm.nix` | builds `ghostty-vt.wasm` from the pinned Ghostty source |
| `mermaid.nix` | takes `mermaid.min.js` from Mermaid's npm package, pinned, so that the site serves its own copy |
| `website.nix` | builds the site |
libghostty-vt reports replies and the bell through C callbacks. A callback
is an index into the wasm module's function table, and a JavaScript function
cannot go in that table directly, so `vt.js` wraps each callback in a tiny
wasm module of its own. The module imports the function and exports it again
with the right type. For the answers that are the host's to choose, such as
Device Attributes, it gives the ones Ghostty gives. The colors a terminal
starts with and its size reports are the host's choice too, and for those it
follows a default xterm, as the cases do: xterm's palette, black on white,
and the case's own size for `CSI 18 t`.
`vt.js` takes every struct offset and enum value from the layout description
that libghostty-vt publishes about itself (`ghostty_type_json`). No offsets are
hard-coded, so a Ghostty update that moves a field is picked up rather than
misread.
## For search engines and LLMs
Every page carries a `rel="canonical"` link and Open Graph tags, both with
its absolute URL. It also links a plain Markdown copy of itself, published
beside it as `index.html.md`, which is where [llms.txt](https://llmstxt.org/)
readers look for one. `llms.txt` at the root of the site indexes those copies.
The copies come from the SuperMD sources: the validation cases become a
plain input block and expected screen, and links between pages point at the
other pages' copies.
`sitemap.xml` gives each page the date of the last commit that changed its
source. A Nix build sees no git history, so the sitemap is not part of
`nix build .#website`. The publish job adds it to a copy of the build, from a
checkout with the full history, and `tools/sitemap.mjs` refuses to run in a
shallow clone. `robots.txt` allows every crawler and names the sitemap.
## When the sources disagree
Where DEC STD 070 and xterm disagree, a case's expectation follows xterm,
bugs included, and the page names what DEC STD 070 asks for instead. That is
Ghostty's documented policy: the first principle on its features page is
xterm compatibility. A case is marked a known difference when libghostty-vt
differs from xterm or, where xterm does not implement the function, from the
documents the site cites. "xterm" means a default build with its default
resources, which is a VT420: where that xterm ignores a function, because its
parser drops the sequence or the feature needs a configure option, the case
expects the function to be ignored, and libghostty-vt implementing it is a
known difference.
## Writing a case
A case is a block directive holding two plain code blocks, placed after a
heading:
````markdown
## CBT-3: From exactly on a tab stop
>[]($block.attrs("vt-case"))
> ```
> \e[?5W # reset tab stops
> \e[1;9H
> X
> ```
> ```
> |________X_|
> cursor 1,10
> ```
````
Each line of input is one step. Escapes include `\e`, `\xNN`, `\NNN` and
`\u{NNNN}`, and a `#` after whitespace starts a comment. The expected screen
gives one `|…|` row per line, with `_` for an empty cell. Its size sets the
size of the terminal. The screen can be followed by `cursor`, `pending-wrap`,
`title` and `attr` checks. Add `"known-difference"` to the attributes to mark
a case libghostty-vt is known to fail, `"checksum-report"` to run it with
DECRQCRA allowed, which libghostty-vt, like a default xterm, leaves off, and
`"no-column-resize"` to run it with DECCOLM kept from changing the width, as
xterm is with `ColumnMode` disallowed. `parseCase` in
`website/assets/vtcase.js` turns these into settings for `vt.terminal`, and
the VM harness's terminal command can name them too (see
`emulators/run.mjs`). libghostty-vt has no setting for `no-column-resize`.
The site's notation page has the full syntax.
A diagram is a block with the `diagram` attribute holding one code block of
[Mermaid](https://mermaid.js.org/) source:
````markdown
>[]($block.attrs("diagram"))
> ```
> flowchart TD
> a{"Mode 40?"} -- set --> b["Clear the screen"]
> ```
````
The source is what shows without scripts, and what the page's Markdown copy
carries, as a `mermaid` code block.
## Building
Ghostty and Zine both build with Zig 0.17.0. The wasm is its own Nix
derivation, built from Ghostty's source tree with Ghostty's own dependencies,
and the site build is given its path. Mermaid, which draws the diagrams, comes in the same way.
The devshell exports the wasm's path as `$VT_WASM`, Mermaid's as
`$MERMAID_JS`, and the Ghostty commit as `$GHOSTTY_REVISION`.
```console
$ nix develop
$ zig build serve -Dvt-wasm="$VT_WASM" -Dmermaid="$MERMAID_JS" -Dghostty-revision="$GHOSTTY_REVISION"
$ node tools/check-cases.mjs "$VT_WASM"
$ node tools/order.mjs website # after adding a page, to place it in its section
$ node tools/support.mjs website # after changing website/support.json
$ node tools/finals.mjs website # after changing a sequence or also_sequences
$ node tools/sitemap.mjs website zig-out/public # after a release build
$ nix build .#website # the site, in result/public
$ nix build .#ghostty-vt-wasm # just the wasm
```
Ghostty is pinned as the flake input `ghostty`. Running
`nix flake update ghostty` moves the site to a newer libghostty-vt. After an
update, run the case checker first: it reports any case whose result changed.
The wasm must be served over HTTP. A page opened from `file://` cannot fetch
it.
## Running the cases in other terminals
`emulators/` runs every validation case in a real terminal emulator, inside a
NixOS virtual machine, with X11 or, for a Wayland terminal, a headless sway,
with no judgment involved: each check is a comparison of bytes.
- `run.mjs` starts a fresh terminal for each case, sized to the case's screen
and titled with a sentinel, since a reset does not clear everything a case
can change (the dynamic colors, saved modes, the saved cursor).
- `case.mjs` runs in that terminal in place of a shell. It writes the case's
input to its own tty, waits for a fence (a DECRQCRA no case uses, which the
terminal answers only once everything before it is processed), and reads
the terminal's state back with sequences it answers: the cursor with CPR,
each cell's character and attributes with a DECRQCRA per cell using
xterm's XTCHECKSUM extension, the title from X with `xdotool`.
- What cannot be read back that way is reported as untested, never as a
failure: the bell, the working directory, progress reports, the pointer,
and colors, italic and faint.
- How a terminal is fenced and read is its *profile*. `xterm` is the above.
`kitty` fences with an XTGETTCAP for a name no case asks for, which kitty
answers with the name, and reads the whole screen with its remote control,
`kitten @ get-text --ansi`, parsed by `kitty-screen.mjs`, which gives
every attribute and color too. kitty writes back a run of blanks that a
tab made as one tab, losing how many it stood for, so a screen with a tab
that does not match is untested rather than failed.
- `foot` fences the same way. foot has no way to be asked for its screen but
a key binding, so the flake binds its `pipe-visible` action to
Ctrl+Shift+F9, writing the visible text to a file, and `case.mjs` presses
the key with `wtype`, through sway's virtual keyboard; `foot-screen.mjs`
turns the text back into a grid, recovering soft-wrapped rows from the
column count. The text has no attributes, so `attr` checks are untested,
and tabs are lost as for kitty. The title comes from `swaymsg`.
- `urxvt` is for rxvt-unicode, which answers no request that reads its
screen. The harness loads a Perl extension of its own into it,
`emulators/urxvt/cc-harness`, and nothing else; it answers two `OSC 777`
commands with an acknowledgement, which is the fence. `start` counts bells
from then on, and `dump` writes the screen, cell by cell with each cell's
rendition, along with the cursor and the bell count, to a file, through
rxvt-unicode's own Perl API (`ROW_t`, `ROW_r`, `screen_cur`, `on_bell`).
So its colors, bold, italic, blink, inverse and underline are judged, as
are its bells; a 24-bit color is kept where the API cannot turn it back
into RGB, so a cell with one is untested. The title comes from X.
nixpkgs' rxvt-unicode 9.31 does not build with a compiler that defaults
to C++20, where its own `lerp` is ambiguous with `std::lerp`, so the flake
builds it as C++17, which changes nothing about how it behaves.
- `summarize.mjs` prints the counts and each failure from `results.json`.
The checks are flake checks, Linux only:
```console
$ nix build .#checks.x86_64-linux.xterm-default # xterm-411a, xterm's own defaults
$ node emulators/summarize.mjs result/results.json
$ nix build .#checks.x86_64-linux.xterm # nixpkgs' xterm
$ nix build .#checks.x86_64-linux.kitty # nixpkgs' kitty
$ nix build .#checks.x86_64-linux.foot # nixpkgs' foot, under sway
$ nix build .#checks.x86_64-linux.urxvt # nixpkgs' rxvt-unicode
$ nix build .#checks.x86_64-linux.pyte # pyte, in-process
$ nix build .#checks.x86_64-linux.xtermjs # xterm.js headless, in-process
$ nix build .#checks.x86_64-linux.alacritty # alacritty_terminal, in-process
$ nix build .#checks.x86_64-linux.vte # VTE, in-process, under Xvfb
```
Each terminal is a package, a profile and the command that opens it, in
`flake.nix`, run by `emulators/vm.nix`. kitty, foot and rxvt-unicode are
given a default xterm's colors, as libghostty-vt is, so that color answers
can be compared
with the cases. A Wayland terminal runs under sway on wlroots' headless
backend, drawing with pixman, so it needs no display or GPU; its window
floats with no border and no minimum size, so that it is exactly the size
of the case's screen. Weston has a headless backend too, but no virtual
keyboard to press foot's key with.
Four emulators are libraries, so their checks need no VM and no terminal
to read back. Each case's input goes to a fresh instance, the screen, cursor,
title, replies and bells it is left with are read from it directly, and
`emulators/library.mjs` judges that state with `runCase` from
`website/assets/vtcase.js`, the same comparison the site runs on
libghostty-vt. Every check is judged but the working directory, progress
reports and pointer shape, which none of them has, and an exception or a
panic from the emulator fails the case. A case may read its state back with
DECRQM or DECRQSS; an emulator that answers none of that request's own
cases does not have it, so elsewhere a case whose reply should have been
nothing but its answers, and was nothing, is untested rather than failed,
unless something else about it fails. `library.mjs` applies this to finished
results, so `run.mjs` applies it to a terminal in a VM as well.
- **pyte**, the Python terminal emulator: `emulators/pyte.mjs` sends every
case through `emulators/pyte_driver.py`, one Python process.
- **xterm.js**, headless: `emulators/xtermjs.mjs` runs `@xterm/headless`
in the same Node process, from the npm tarball the flake fetches. Cursor
visibility and underline styles are read from its internals, as its API
does not expose them. Headless, its parser hands color changes and color
queries to an event only the browser build's theme service handles, so
OSC 4, 10, 11 and the rest do nothing, and the rows are named *xterm.js
headless* to say that they are not about xterm.js in a browser.
- **alacritty_terminal**, the crate Alacritty is built on:
`emulators/alacritty.mjs` sends every case through a small Rust driver in
`emulators/alacritty/`, which the flake builds from its `Cargo.lock`. The
crate leaves to its host the colors it was not told and the size of a
cell in pixels; the driver answers as a default xterm would, with xterm's
palette, black on white, and 6×13 cells. It feeds the input a byte at a
time, so a color query answers with the palette as it stood then, and
lets through any input still held for synchronized output when the case
ends. HT leaves a tab character in the blank cells it passes over, for
copying; the driver reports those as the blanks they are drawn as.
- **VTE**, the widget GNOME Terminal is built on: `emulators/vte.mjs` sends
every case through `emulators/vte_driver.py`, which drives VTE through
PyGObject. VTE is a GTK widget and needs a display, which the check gives
it with Xvfb; no window is shown. The terminal sits in an offscreen
window, since VTE processes its input only once realized, and is forced
to the case's size with a size request. The screen is read cell by cell
with `get_text_range_format`, as text and as HTML for the attributes, with
VTE given xterm's palette. The HTML has no faint, blink, invisible or
overline, shows inverse only as swapped colors, and says nothing of a
blank cell, so those are untested, as are cursor visibility, the pointer
and progress. As in alacritty_terminal, a tab character in a blank cell is
read as a blank.
`xterm-default` is the reference: the xterm the cases follow, so every case
should pass apart from those the harness's one change to a default xterm
affects. It lets xterm answer DECRQCRA, which `disallowedWindowOps` refuses
by default, so the DECRQCRA and OSC 61 cases differ. The harness reads the
screen with DECRQCRA, so for now it can test only terminals that answer it.
CI runs `xterm-default` on every push, on a runner with KVM, and checks the
run with `emulators/verify.mjs`: every case must pass, apart from the ones
`emulators/xterm-default.expected.json` lists with the status they are
expected to have and why. A case that stops failing as listed fails the
check too, so the list cannot go stale. The site is published only when
this passes as well, so its cases always agree with a real xterm.
Other terminals are not run in CI. A maintainer runs their checks when a
terminal's version changes in nixpkgs or the cases change, and commits the
`results.json` that comes out under `emulators/results/`, named for the
check; what the site needs from them is built from those files.
A measured terminal's row in a page's Support table is derived from its
results rather than written by hand. `website/support.json` names the
results file under `measured`, and `emulators/roles.json` gives every case
a role:
- **probe**: the case shows the function working as its specification
says, so a terminal that implements it passes, and one that ignores it
fails.
- **follow**: the case pins something a default xterm does where a correct
terminal may differ: the function being ignored, xterm's quirks and bugs,
answers whose content is the host's choice (identification, default
colors, how an SGR is spelled), and parser edge cases. A case that a
terminal ignoring the function would also pass is a follow case too.
The row is *Yes* when every probe on the page passes, *Partly* when some
do and *No* when none do. A case the harness could judge only in part is
left out along with those it could not judge at all, since what it could
not read may be the whole point of the case, and a page with no probe it
could judge keeps its hand-written row. The version cell says *run in a VM*
or, for the emulators run as libraries, *run as a library*, and links to the results. A reply
that differs from xterm's only in form counts as a pass, and the row says
so: one ending its strings with ST where xterm used BEL, since either
terminator is valid, and one writing a color's 16-bit components as `ff00`
where xterm repeats the 8 bits as `ffff`, since both are the same color;
colors are compared by their top 8 bits. `support.mjs`
fails if a case has no role, so a new case needs one.
```console
$ nix build .#checks.x86_64-linux.xterm-default
$ node emulators/verify.mjs result/results.json emulators/xterm-default.expected.json
```
## Sources
Each sequence page cites its definition in ECMA-48, DEC STD 070, the VT220
manual and the VT510 manual, where it has one, and the graphics pages cite
the VT330/VT340 graphics manual. A citation gives the section and the printed page,
and the ECMA-48 and DEC STD 070 citations link to that page of the scan at the
Internet Archive. The site's Sources page describes the documents.
The citations are in each page's frontmatter: `ecma48`, `ecma48_name` and
`ecma48_page` give the section, the name and the printed page; `dec070`,
`dec070_name`, `dec070_page` and `dec070_pdf` give the same plus the PDF page;
`vt220` and `vt220_name` give the VT220 manual's chapter and section anchor
on VT100.net and the section's title; `vt340` and `vt340_name` do the same
for the VT330/VT340 graphics manual; `vt510` and `vt510_name` give the
VT100.net page and its title. The layout
builds the links. The Internet Archive numbers a scan's pages from 0, so
ECMA-48's printed page *N* is the scan's page *N* + 13, and DEC STD 070's PDF
page *N* is the scan's page *N* − 1.
A page that covers more than one number or mnemonic, such as
rxvt-unicode's fonts, OSC 710 to 713, or DECRQCRA with its reply DECCKSR and
XTCHECKSUM, lists the others in `index_also`, each as
`"OSC 711|Set Bold Font"`: the label, a `|`, and the name. `tools/order.mjs`
sorts each into the section's order with the pages, as `page|label|name`,
and the sidebar, the home page and the section page give it an entry of its
own, leading to the page, so that every number and mnemonic can be found
where it falls in the list. Such a page lists the other sequences it covers
in `also_sequences` too, each a map,
`{ "mnemonic": "DECCKSR", "name": "Memory Checksum Report", "syntax": "DCS Pi ! ~ D…D ST" }`,
written on one line; a map rather than a string with a delimiter, since a
syntax can contain any byte, as DECRQLP's `CSI Ps ' |` does. The layout
draws each one's syntax diagram under its name, below the page's own, and
`tools/finals.mjs` lists the CSI ones by their final byte.
A page whose title names several codes, as "Set Fonts (OSC 710 to 713)"
and "Save and Restore Cursor (DECSC, DECRC)" do, copies the list in the
title's parentheses into `codes`. The page is then headed with that list
and the rest of its title rather than with its first code, and the first
code's own `mnemonic` and `name` move down to label its diagram, the way
the others in `also_sequences` are labeled. A page whose title names one
code, such as DECRQCRA, is headed with that code, and its reply is a
diagram further down.
A page's entry in the sidebar, the home page and its section page shows its
`mnemonic` and `name`, unless it gives `index_mnemonic` and `index_name` to
show instead. The kitty keyboard page does: its first code is
`CSI = u`, Set Flags, but its entry reads `CSI u`, Kitty Keyboard Protocol,
and `tools/order.mjs` sorts it by that.
## Credits
The CBT cases are adapted from Ghostty's VT reference
(), which is MIT licensed. The prose, the
other cases and the site design are this project's own.
## License
MIT; see `LICENSES/MIT.txt`. The project follows [REUSE](https://reuse.software/).
## References cited
- The Alacritty authors. *alacritty_terminal* (0.26.0).
- Amorim, R. (2026). *Glyph Protocol* (v1.11; commit 1eb4130).
- Bothner, P. *Semantic prompts* (proposal; commit 4d2e1d7).
- ConEmu. *ANSI Escape Codes*, "ConEmu specific OSC". ConEmu documentation.
- Contour Terminal. *Buffer Capture*. VT extensions (commit 3cdeed6).
- Contour Terminal. *Dark and Light Mode detection*. VT extensions (commit
ab57ab3).
- Contour Terminal. *Query or Change Font Settings*. VT extensions (commit
b7caade).
- Contour Terminal. *Line Reflow Reconfiguration*. VT extensions (commit
5a6be84).
- Contour Terminal. *Save and Restore SGR attributes*. VT extensions (commit
d5541aa).
- Contour Terminal. *Semantic Block Query (DEC Mode 2034)*. VT extensions
(commit 24072f8).
- Contour Terminal. *Synchronized Output*. VT extensions.
- Culverhouse, T. (2026, July 25). *Private Mode for Terminal Visibility
Reports*. rockorager.dev.
- delthas. (2024). *Custom App ID in terminal emulators* (gist revision
3650e65).
- Dickey, T. E. *XTERM - Change Log*.
- Dickey, T. E. *XTerm Control Sequences*.
- Digital Equipment Corporation. (1984). *VT220 Programmer Reference Manual*
(2nd ed., EK-VT220-RM-002). Online reproduction:
- Digital Equipment Corporation. (1988). *VT330/VT340 Programmer Reference
Manual, Volume 2: Graphics Programming* (2nd ed., EK-VT3XX-GP-002). Online
reproduction:
- Digital Equipment Corporation. (1991). *DEC STD 070 Video Systems Reference
Manual* (Rev H).
- Digital Equipment Corporation. (1993). *VT510 Video Terminal Programmer
Information* (1st ed.). Online reproduction:
- Digital Equipment Corporation. *CBT—Cursor Backward Tabulation*. VT510
Video Terminal Programmer Information.
- Digital Equipment Corporation. *CHT—Cursor Horizontal Forward Tabulation*.
VT510 Video Terminal Programmer Information.
- ECMA International. (1991). *Control Functions for Coded Character Sets*
(ECMA-48, 5th ed.).
; also
- Eklöf, D. *foot* (1.28.0).
- Eklöf, D. *foot-ctlseqs(7)* (foot 1.28.0).
- Ghostty contributors. *libghostty-vt*.
- Ghostty. *Cursor Backward Tabulation (CBT)*. Ghostty Docs.
- Ghostty. *Features*. Ghostty Docs.
- The GNOME Project. *VTE* (0.84.1).
- Goyal, K. *Color control*. kitty documentation.
- Goyal, K. *Comprehensive keyboard handling in terminals*. kitty
documentation.
- Goyal, K. *Copying all data types to the clipboard*. kitty documentation.
- Goyal, K. *Desktop notifications*. kitty documentation.
- Goyal, K. *Mouse pointer shapes*. kitty documentation.
- Goyal, K. *The Drag and Drop protocol*. kitty documentation.
- Goyal, K. *The text sizing protocol*. kitty documentation.
- Hashimoto, M. (2026). *Program Status Protocol (OSC 7501)* (draft 0.1;
gist revision f838e73).
- Hashimoto, M. (2026). *Program Status Protocol (OSC 7501)* (revision 0.4).
Rex documentation, Superlogical.
- iTerm2. *Badges*. iTerm2 documentation.
- iTerm2. *Feature Reporting*. iTerm2 documentation.
- iTerm2. *Images*. iTerm2 documentation.
- iTerm2. *Proprietary Escape Codes*. iTerm2 documentation.
- ITU-T. (1993). *Information technology – Open Document Architecture (ODA)
and interchange format: Character content architectures* (ITU-T T.416).
- KDE. *Konsole* (commit a24c3d71).
- Koblinger, E. (2019). *BiDi in Terminal Emulators* (v0.1; commit cc8686a).
; source
- Lehmann, M. *rxvt(7)*, rxvt-unicode's reference of its control sequences.
- Lehmann, M. *urxvtperl*, rxvt-unicode's Perl extension manual.
- The Linux man-pages project. *console_codes(4)*. Linux manual pages.
- Microsoft. *Terminal shell integration*. Visual Studio Code documentation.
- Nachman, G., & Dickey, T. E. *esctest2*.
- Otty. (2026). *Terminal Resume Protocol (TRP) — OSC 88 Specification*
(v1, proposal; commit aabe19c).
- Parpart, C. (2021). *VT sequence: screenshot* (Contour issue #161).
- Parpart, C. (2026). *OSC 533 — Screenshot* (Contour pull request #2088;
commit 11a819c).
- Selectel and the pyte authors. *pyte* (0.8.2).
- Stably AI. *Orca* (1.4.219).
- UAPI Group. *OSC 3008: Hierarchical Context Signalling* (UAPI.15).
- Wolff, T. *Control Sequences*. mintty wiki.
- The xterm.js authors. *@xterm/headless* (6.0.0).