- JavaScript 65.6%
- Nix 14.6%
- CSS 7.7%
- Python 4.9%
- Rust 4%
- Other 3.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Now that the page is headed "Kitty Keyboard Protocol" with all four of its codes, the name on `CSI = u` labels that sequence's own diagram, beside Query, Report, Push and Pop Flags, so it says what the sequence does. It keeps the protocol's name in parentheses, since it is also the page's entry in the sidebar and the section index, where Set Flags alone would not say whose flags. The table by final byte follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRwJbTU9K6SEu66NAKSoxd |
||
| .forgejo/workflows | ||
| emulators | ||
| LICENSES | ||
| tools | ||
| website | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| COVERAGE.md | ||
| flake.lock | ||
| flake.nix | ||
| ghostty-vt-wasm.nix | ||
| mermaid.nix | ||
| README.md | ||
| REUSE.toml | ||
| website.nix | ||
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, 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. Its Forgejo workflow
publishes it from main to https://control-codes.page/.
Where it lives
- https://git.jcollie.dev/jeff/control-codes — the primary home.
- https://tangled.org/jcollie.dev/control-codes — mirror.
git clone https://git.jcollie.dev/jeff/control-codes.git
On Radicle, 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:
rad clone rad:z2dBmdsQqFL24uX7hpoHgxe9N2QdU
Cloning also seeds the repository, which helps keep it available on the network.
What is covered
241 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, SD, SGR, SIMD, SL/SR, SM, SU, TBC, VPA, VPB, VPR, XTMODKEYS/XTQMODKEYS, 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, ?2004, ?2026, ?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: OSC 88, the Terminal Resume Protocol, and OSC 7501, the Program Status Protocol. 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 80 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 850 cases in all. 274 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 |
|---|---|---|
| BS-5 | BS is a one-column CUB, so it moves past the left margin | it stays at the left margin (VT510 manual, esctest2) |
| CUB-4 | CSI D stops at column 1, ignoring the left margin, unless reverse wraparound is on (cursorLeft) |
it stops at the left margin (DEC STD 070, esctest2) |
| 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) |
| BLINK-4 | DECSTR leaves mode 12 set |
DECSTR stops the cursor blinking (xterm) |
| SYNC-1 | mode 2026 is implemented, and DECRQM answers 1 or 2 |
a default xterm does not recognize it and answers 0 |
| OSC4-8 | DECSTR is not implemented, so the palette survives it | DECSTR resets the palette (xterm) |
| 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) |
| 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) |
| CHA-5 | in origin mode, moves the cursor down by the top margin as well (cursor_col in stream_terminal.zig passes the absolute row back to setCursorPos) |
the line does not change (esctest2) |
| HPR-4, VPR-4 | the same, through cursor_col_relative and cursor_row_relative |
the cursor moves only along its own axis (esctest2) |
| DECRQM-6 | an ANSI mode it does not have, such as GATM, is reported as not recognized: modes.zig reports 0 for any mode outside its table |
GATM is reported as permanently reset (DEC STD 070, VT510 manual, xterm, esctest2) |
| the ANSI mode pages for GATM, CRM, SRTM, VEM, HEM, PUM, FEAM, FETM, MATM, TTM, SATM, TSM and EBM (the first two cases of each) | the same: every ANSI mode but KAM, IRM, SRM and LNM is reported as not recognized, 0 |
xterm reports these as permanently reset, 4, as the VT510 manual lists them, and CRM as reset, 2 |
| 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) |
| DECSTR-2 to DECSTR-7 | DECSTR is not implemented, so none of the soft reset happens | insert mode, origin mode, rendition, margins, cursor visibility and the saved cursor are reset (DEC STD 070, VT510 manual, esctest2) |
| 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-1 to DECDSR-8 | DEC's private status requests (CSI ? Ps n) get no answer: only 5, 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, -7 | 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) |
| RIS-6 | DECSTR (CSI ! p), the soft reset recommended instead of RIS, is not implemented: the p dispatch in stream.zig handles only DECRQM |
the soft reset restores normal rendition, among other things (DEC STD 070, VT510 manual) |
| 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) |
| TBC-2 | CSI g with no parameter is ignored: stream.zig handles only exactly one parameter |
the default clears the stop at the cursor (ECMA-48, DEC STD 070, VT510 manual, esctest2) |
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 |
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
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:
## 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 source:
>[]($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 builds with Zig 0.16, and Zine 0.14 needs a Zig 0.17 development
build. So the wasm is its own Nix derivation, 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.
$ 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.mjsstarts 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.mjsruns 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 withxdotool.- 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.
xtermis the above.kittyfences 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 bykitty-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. footfences the same way. foot has no way to be asked for its screen but a key binding, so the flake binds itspipe-visibleaction to Ctrl+Shift+F9, writing the visible text to a file, andcase.mjspresses the key withwtype, through sway's virtual keyboard;foot-screen.mjsturns the text back into a grid, recovering soft-wrapped rows from the column count. The text has no attributes, soattrchecks are untested, and tabs are lost as for kitty. The title comes fromswaymsg.urxvtis 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 twoOSC 777commands with an acknowledgement, which is the fence.startcounts bells from then on, anddumpwrites 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 ownlerpis ambiguous withstd::lerp, so the flake builds it as C++17, which changes nothing about how it behaves.summarize.mjsprints the counts and each failure fromresults.json.
The checks are flake checks, Linux only:
$ 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.mjssends every case throughemulators/pyte_driver.py, one Python process. - xterm.js, headless:
emulators/xtermjs.mjsruns@xterm/headlessin 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.mjssends every case through a small Rust driver inemulators/alacritty/, which the flake builds from itsCargo.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.mjssends every case throughemulators/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 withget_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.
$ 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.
Credits
The CBT cases are adapted from Ghostty's VT reference (https://ghostty.org/docs/vt), 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.
References cited
- The Alacritty authors. alacritty_terminal (0.26.0). https://github.com/alacritty/alacritty
- Bothner, P. Semantic prompts (proposal; commit 4d2e1d7). https://gitlab.freedesktop.org/Per_Bothner/specifications/-/blob/4d2e1d75d4861a1d924895e106f8f016880e12a7/proposals/semantic-prompts.md
- ConEmu. ANSI Escape Codes, "ConEmu specific OSC". ConEmu documentation. https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
- Contour Terminal. Synchronized Output. VT extensions. https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md
- delthas. (2024). Custom App ID in terminal emulators (gist revision 3650e65). https://gist.github.com/delthas/d451e2cc1573bb2364839849c7117239/3650e6597ca67302fce717c775ea844c730200b5
- Dickey, T. E. XTERM - Change Log. https://invisible-island.net/xterm/xterm.log.html
- Dickey, T. E. XTerm Control Sequences. https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
- Digital Equipment Corporation. (1984). VT220 Programmer Reference Manual (2nd ed., EK-VT220-RM-002). Online reproduction: https://vt100.net/docs/vt220-rm/
- Digital Equipment Corporation. (1988). VT330/VT340 Programmer Reference Manual, Volume 2: Graphics Programming (2nd ed., EK-VT3XX-GP-002). Online reproduction: https://vt100.net/docs/vt3xx-gp/
- Digital Equipment Corporation. (1991). DEC STD 070 Video Systems Reference Manual (Rev H). https://archive.org/details/bitsavers_decstandar0VideoSystemsReferenceManualDec91_74264381
- Digital Equipment Corporation. (1993). VT510 Video Terminal Programmer Information (1st ed.). Online reproduction: https://vt100.net/docs/vt510-rm/
- Digital Equipment Corporation. CBT—Cursor Backward Tabulation. VT510 Video Terminal Programmer Information. https://vt100.net/docs/vt510-rm/CBT.html
- Digital Equipment Corporation. CHT—Cursor Horizontal Forward Tabulation. VT510 Video Terminal Programmer Information. https://vt100.net/docs/vt510-rm/CHT.html
- ECMA International. (1991). Control Functions for Coded Character Sets (ECMA-48, 5th ed.). https://archive.org/details/ecma-48-5th-edition-june-1991; also https://ecma-international.org/publications-and-standards/standards/ecma-48/
- Eklöf, D. foot (1.28.0). https://codeberg.org/dnkl/foot
- Eklöf, D. foot-ctlseqs(7) (foot 1.28.0). https://codeberg.org/dnkl/foot/src/tag/1.28.0/doc/foot-ctlseqs.7.scd
- Ghostty contributors. libghostty-vt. https://github.com/ghostty-org/ghostty
- Ghostty. Cursor Backward Tabulation (CBT). Ghostty Docs. https://ghostty.org/docs/vt/csi/cbt
- Ghostty. Features. Ghostty Docs. https://ghostty.org/docs/features
- The GNOME Project. VTE (0.84.1). https://gitlab.gnome.org/GNOME/vte/-/tree/0.84.1
- Goyal, K. Color control. kitty documentation. https://sw.kovidgoyal.net/kitty/color-stack/
- Goyal, K. Comprehensive keyboard handling in terminals. kitty documentation. https://sw.kovidgoyal.net/kitty/keyboard-protocol/
- Goyal, K. Copying all data types to the clipboard. kitty documentation. https://sw.kovidgoyal.net/kitty/clipboard/
- Goyal, K. Desktop notifications. kitty documentation. https://sw.kovidgoyal.net/kitty/desktop-notifications/
- Goyal, K. Mouse pointer shapes. kitty documentation. https://sw.kovidgoyal.net/kitty/pointer-shapes/
- Goyal, K. The Drag and Drop protocol. kitty documentation. https://sw.kovidgoyal.net/kitty/dnd-protocol/
- Goyal, K. The text sizing protocol. kitty documentation. https://sw.kovidgoyal.net/kitty/text-sizing-protocol/
- Hashimoto, M. (2026). Program Status Protocol (OSC 7501) (draft 0.1; gist revision f838e73). https://gist.github.com/mitchellh/7acae3abd8355c1c00287d67e96c913a/f838e7364cf4fa56e74814df517cac8c716d82d1
- Hashimoto, M. (2026). Program Status Protocol (OSC 7501) (revision 0.4). Rex documentation, Superlogical. https://www.superlogical.com/rex/docs/build/program-status
- iTerm2. Badges. iTerm2 documentation. https://iterm2.com/documentation-badges.html
- iTerm2. Feature Reporting. iTerm2 documentation. https://iterm2.com/feature-reporting/
- iTerm2. Images. iTerm2 documentation. https://iterm2.com/documentation-images.html
- iTerm2. Proprietary Escape Codes. iTerm2 documentation. https://iterm2.com/documentation-escape-codes.html
- ITU-T. (1993). Information technology – Open Document Architecture (ODA) and interchange format: Character content architectures (ITU-T T.416). https://www.itu.int/rec/T-REC-T.416-199303-I
- KDE. Konsole (commit a24c3d71). https://invent.kde.org/utilities/konsole
- Lehmann, M. rxvt(7), rxvt-unicode's reference of its control sequences. http://pod.tst.eu/http://cvs.schmorp.de/rxvt-unicode/doc/rxvt.7.pod
- Lehmann, M. urxvtperl, rxvt-unicode's Perl extension manual. https://manpages.debian.org/testing/rxvt-unicode/urxvtperl.3.en.html
- The Linux man-pages project. console_codes(4). Linux manual pages. https://man7.org/linux/man-pages/man4/console_codes.4.html
- Microsoft. Terminal shell integration. Visual Studio Code documentation. https://code.visualstudio.com/docs/terminal/shell-integration
- Nachman, G., & Dickey, T. E. esctest2. https://github.com/ThomasDickey/esctest2
- Otty. (2026). Terminal Resume Protocol (TRP) — OSC 88 Specification (v1, proposal; commit aabe19c). https://github.com/Otty-sh/osc-88/blob/aabe19cd7a581d4b9c2b7ae3454ca7d294f64d2c/SPEC.md
- Selectel and the pyte authors. pyte (0.8.2). https://github.com/selectel/pyte
- Stably AI. Orca (1.4.219). https://github.com/stablyai/orca/tree/e705cac04a1db7e7e2184746e912142d34ca838b
- UAPI Group. OSC 3008: Hierarchical Context Signalling (UAPI.15). https://uapi-group.org/specifications/specs/osc_context/
- Wolff, T. Control Sequences. mintty wiki. https://github.com/mintty/mintty/wiki/CtrlSeqs
- The xterm.js authors. @xterm/headless (6.0.0). https://github.com/xtermjs/xterm.js