# 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).