Dumps and tools
Everything Tuimark renders can be printed as data. The CLI commands on this page all build the same frame the terminal would show, deterministically, and never read the environment, so their output is the same on every machine and in CI. That is what makes them useful to an agent, to a test, and to you.
dump
tuimark dump FILE renders one frame at --cols × --rows (80 × 24 by default), with --data FILE.json as the store. The text form has three sections: the grid, the nodes, the diagnostics.
<tui>
<style>
#card { border: rounded; }
.ok { color: $ok; bold: true; }
</style>
<screen id="main">
<box id="card" title="build">
<text class="ok">passing</text>
</box>
</screen>
</tui>$ tuimark dump app.tui --cols 24 --rows 4
=== grid 24x4 ===
╭─ build ──────────────╮
│passing │
╰──────────────────────╯
=== nodes ===
main screen 24x4 @(0,0)
card box 24x3 @(0,0)
- text 22x1 @(1,1)
=== errors ===
okEach node line is id tag WxH @(x,y), with - for a node without an id, and ends with focus, selected, or checked when those apply. The === errors === section is ok or one diagnostic per line.
The JSON dump
--format json is the same frame for a program:
// $ tuimark dump app.tui --cols 24 --rows 4 --format json
{
"cols": 24,
"rows": 4,
"ok": true,
"focus": null,
"errors": [],
"nodes": [
{
"id": "main",
"tag": "screen",
"x": 0,
"y": 0,
"w": 24,
"h": 4
},
{
"id": "card",
"tag": "box",
"x": 0,
"y": 0,
"w": 24,
"h": 3
},
{
"tag": "text",
"x": 1,
"y": 1,
"w": 22,
"h": 1,
"text": "passing",
"classes": [
"ok"
]
}
],
"grid": [
"╭─ build ──────────────╮",
"│passing │",
"╰──────────────────────╯",
" "
]
}| Field | Holds |
|---|---|
cols, rows | the size rendered |
ok | false when any diagnostic is an error |
focus | the focused node's id, or null |
errors | every diagnostic, warnings included, each with severity, code, and msg, plus file, line, col, path, and id when known |
nodes | every laid-out node in document order, modals last |
grid | the painted frame: rows strings, each exactly cols columns wide, no ANSI |
wide | present and true when a row holds a wide or multi-code-point character; see --cells |
A node has tag, its outer rect x, y, w, h, and, when they apply: id, text (a text's content, an input's value), key (a row's key), focused, selected, checked, scroll (a viewport's offset and extent), and classes (version 2 documents). Nodes scrolled out of a viewport are still listed, with coordinates outside it.
The format is versioned and only grows: a new field is optional, and no field changes type or meaning. JSON schemas for it ship in the repository's schema/ directory.
Exit status: 0 when the frame is ok, 2 when it has an error, 1 for a usage or I/O problem.
--cells
When text has wide characters, a row of grid no longer has one character per column. --cells adds a per-column map: one entry per cell, row by row, with its cluster ("" for the second column of a wide one), its width, and the id of the box that painted it.
<tui>
<screen id="main">
<text id="name">東京 ok</text>
</screen>
</tui>// $ tuimark dump wide.tui --cols 8 --rows 1 --format json --cells
// excerpt: "grid", "wide", "cells"
{
"grid": [
"東京 ok "
],
"wide": true,
"cells": [
{
"x": 0,
"y": 0,
"ch": "東",
"id": "name",
"w": 2
},
{
"x": 1,
"y": 0,
"ch": "",
"id": "name"
},
{
"x": 2,
"y": 0,
"ch": "京",
"id": "name",
"w": 2
},
{
"x": 3,
"y": 0,
"ch": "",
"id": "name"
},
{
"x": 4,
"y": 0,
"ch": " ",
"id": "name"
},
{
"x": 5,
"y": 0,
"ch": "o",
"id": "name"
},
{
"x": 6,
"y": 0,
"ch": "k",
"id": "name"
},
{
"x": 7,
"y": 0,
"ch": " ",
"id": "name"
}
]
}Index cells by column whenever wide is true. The same map decides which node a mouse click lands on.
--styles
The grid shows characters, not colors. --styles (JSON only) adds the effective theme and, per row, the runs of cells that share a foreground, background, and attributes:
// $ tuimark dump app.tui --cols 24 --rows 4 --format json --styles
// excerpt: "theme", "styles"
{
"theme": "dark",
"styles": [
[
{
"x": 0,
"w": 24,
"fg": "default",
"bg": "default"
}
],
[
{
"x": 0,
"w": 1,
"fg": "default",
"bg": "default"
},
{
"x": 1,
"w": 7,
"fg": "green",
"bg": "default",
"a": [
"bold"
]
},
{
"x": 8,
"w": 16,
"fg": "default",
"bg": "default"
}
],
[
{
"x": 0,
"w": 24,
"fg": "default",
"bg": "default"
}
],
[
{
"x": 0,
"w": 24,
"fg": "default",
"bg": "default"
}
]
]
}Colors are exact: #rrggbb, an ANSI name, or default. a lists bold, dim, italic, underline, and reverse when set. This is how a test checks that :focus or a theme token actually changed a cell, by diffing JSON instead of reading ANSI by eye. Add --theme light to check the other palette.
play
tuimark play FILE replays input against a document with no terminal: the same key decoding, key handling, focus, and event rules as a running app. It prints the final frame plus every action that fired.
// $ tuimark play app.tui --data sample.json --cols 48 --rows 10 --input "down enter" --format json
// excerpt: "focus", "events"
{
"focus": "tasks",
"events": [
{
"step": 1,
"action": "moved",
"source": "tasks",
"keys": {
"t": "t2"
},
"value": null
},
{
"step": 2,
"action": "open",
"source": "tasks",
"keys": {
"t": "t2"
},
"value": null
}
]
}Steps come from --input "STEP STEP …" (separated by spaces) or --script FILE.ndjson (one JSON object per line):
--input step | --script line | Does |
|---|---|---|
enter, q, ctrl+a, shift+tab, … | {"key": "enter"} | one key: a key token |
text:deploy | {"text": "deploy"} | typed text; a run of characters into an input is one edit |
paste:STR | {"paste": "…"} | one bracketed paste; it never reaches the keymap |
set:PATH=JSON | {"set": {"path": "…", "value": …}} | Set(PATH, value) from the host |
focus:#id | {"focus": "#id"} | Set("@focus", "#id") |
set:@theme="light" | {"theme": "light"} | the host's theme |
resize:100x30 | {"resize": [100, 30]} | the terminal is resized |
click:X,Y | {"click": [X, Y]} | a left click at a cell (version 2, mouse on) |
wheel-up:X,Y, wheel-down:X,Y | {"wheel": "up", "at": [X, Y]} | one wheel step at a cell |
- Host actions are recorded, never run. A
set:step stands in for what a handler would do, so a whole interaction can be scripted:--input "enter set:confirm_open=true tab enter". quitends the session; later steps are skipped.--format jsonadds"events", each{step, action, source, keys, value};--framesadds one settled frame per step.--stylesand--cellswork as fordump.- A step that cannot be applied (an unknown key, a click outside the grid, a bad path) exits 1 and names the step on stderr.
inspect
tuimark inspect answers "why does this look like that?" for one node. Pick it by id, or by a cell with --at X,Y, the node a click there would start from:
$ tuimark inspect app.tui --data sample.json --cols 48 --rows 10 --id tasks
tasks list 48x5 @(0,1) focus
path: /screen#main/list#tasks
pseudo: :focus :focus-within
layout: column ua <ua>:8:10 list (specificity 1)
color: (none) inherited
border-color: cyan author theme.tcss:3:16 #tasks:focus (specificity 110)
overrides $border author theme.tcss:2:27 #tasks (specificity 100)
border: rounded author theme.tcss:2:10 #tasks (specificity 100)
bold: false inherited
dim: false inherited
italic: false inherited
underline: false inherited
reverse: false inherited
wrap: (none) inherited
visibility: visible inheritedIt prints the node, its layout path, and its pseudo-classes, then one line per property that is not at its initial value: the computed value, where it came from (ua for the built-in sheet, attribute, author, inline, or inherited), the winning rule with its file, line, selector, and specificity, and the rules it overrode. --json prints the same as JSON, plus every class the node could have, with the guard behind it and whether any rule uses it: the quickest way to find a class that no selector matches.
validate
tuimark validate FILE reports every diagnostic without printing a frame. It checks the whole document, including other screens, closed modals, and false if branches, and lays it out at 40, 80, and 120 columns to find layout problems at the sizes that matter (under both themes, for theme="auto").
tuimark validate app.tui # parse and style checks
tuimark validate app.tui --data sample.json # plus every path against the data
tuimark validate app.tui --strict # missing paths become errors
tuimark validate app.tui --catalog actions.json # unknown action names become B004
tuimark validate app.tui --json # diagnostics and the action catalog as JSONBind checks need data: without --data, every path would read as missing, so they are skipped, and the text output ends with ok (bindings not checked: no --data) instead of ok. With --data the line stays ok, and warnings are still printed before it. --json has the members actions, data, diagnostics, file, and ok, where data is the --data argument as given, or null without it. --catalog takes a JSON array of action names, or {"actions": [...]}.
catalog
tuimark catalog prints the closed vocabulary as one JSON object: every tag with its attributes (and an enumerated attribute's keywords), every CSS property with its syntax and keywords, the pseudo-classes, the @media features, the key tokens, the built-in actions, and the diagnostic codes. On every item, since is the lowest document version that accepts it. It is generated from the runtime's own tables, takes no flags, and reads no environment variable, so an editor or an agent can read it instead of scraping AGENTS.md. catalog is the format version (1): adding members, items, or versions keeps it.
tuimark catalog | jq '.tags[] | select(.name == "list") | .attrs[] | select(.since > 1)'new
tuimark new NAME [--go | --ts] creates a project in the directory NAME, which must not exist or must be empty. It writes app.tui, theme.tcss, sample.json, and AGENTS.md (the briefing tuimark agents --project prints); --go adds main.go and go.mod, and --ts adds parent.ts and package.json. No file is ever overwritten, and a usage error writes nothing. It prints the files and the command that runs the project, such as tuimark run.
tuimark new todo
cd todo && tuimark run app.tui --data sample.json--ts and npm
A --ts project depends on @tuimark/host, which is published to npm separately from the tuimark binary. If bun install cannot find it yet, install the copy in a clone of the repository instead: bun add @tuimark/host@file:/path/to/tuimark/sdk/ts.
preview
tuimark preview FILE prints the text dump and, when stdout is a terminal, paints the frame with ANSI colors first. --watch repaints whenever the document, its stylesheets, or the data file change. --color truecolor|256|16|none picks the color depth; otherwise it follows TUIMARK_COLOR, then the terminal's own capabilities.
fmt
tuimark fmt FILE prints the document in canonical form; --write rewrites it and --check exits 2 when it is not formatted. See formatting.
ir
tuimark ir FILE prints the parsed document as JSON: every node with its attributes, bindings, actions, and its own inline style, before any stylesheet is applied, plus the keymap and the resolved theme tokens. It is the document's structure as the runtime understands it. A schema for it ships in schema/.
Editor support (tuimark lsp)
tuimark lsp is a Language Server Protocol (3.17) server on stdin/stdout for .tui and .tcss files, so an editor shows what validate says as you type. It takes no arguments, uses only the standard library, and reads no environment variable. The VS Code extension starts it from PATH, and any editor with an LSP client can run tuimark lsp as a stdio server for the two file types. Editors shows how to set it up in VS Code, Neovim, and Helix.
What it offers:
- Diagnostics. The checks
tuimark validateruns, with the same messages, codes, and severities (error and warning), under both themes for atheme="auto"document. A diagnostic in a stylesheet shows on the stylesheet, not on the.tuithat loads it: the server reads each<style src>asLoaddoes (..included) and sends a diagnostic to the file it is in. It sends them after you open or change a file, at most once every 150 ms per file. - Your unsaved text. A document is checked as the editor has it, and so is a stylesheet it loads when that stylesheet is open too; one that is not open is read from disk.
- Bindings. A
sample.jsonnext to the.tuiis used as if you had passed--data, soB001toB003andB008toB010appear. Without one they are skipped, asvalidateskips them without--data, and the same goes for asample.jsonthat is not valid JSON. - Completion. In a
.tui: tag names, the attributes of the tag, the keywords of an enumerated attribute,#idtargets forto,when, andfocus, action names (the built-in ones and the ones the document already uses), key tokens inkeys, and the paths ofsample.jsonin{path},each,bind, andif. In a.tcss: property names and their keywords,$tokens, pseudo-classes,@mediafeatures, and the#ids and.classes of the documents that load it. Everything is filtered by the document'sversion; a stylesheet takes the newest version among the documents that load it. - Hover. The catalog entry of the tag, attribute, property, keyword, or diagnostic code under the cursor: its value syntax or keywords, and the version that introduced it.
- Go to definition. An
#id(in a selector,to,when, orfocus) goes to its element, a.classto every element that has it,<style src>to its file, and a$tokento where:rootdeclares it. - Formatting. For a
.tui, one edit with whattuimark fmtprints; none when the file is already formatted or is not well-formed XML. A.tcsshas no formatter.
A stylesheet is "loaded by" every open .tui that names it, and every .tui in its directory that does. Rename, code actions, semantic tokens, and workspace symbols are not offered. Positions are the protocol's: a 0-based line and a UTF-16 offset, converted from the 1-based line and code-point column validate prints. The server exits 0 on exit after shutdown (or when its input ends after one), and 1 otherwise.
Environment variables
Only a running app (Run(), and the tuimark run and tuimark host sessions built on it) and preview read the environment. The tools above never do.
| Variable | Values | Effect |
|---|---|---|
TUIMARK_THEME | dark, light | overrides the theme, like --theme |
TUIMARK_COLOR | truecolor, 256, 16, none | forces the color depth instead of detecting it from TERM, COLORTERM, NO_COLOR, and friends |
TUIMARK_SYNC | 0, 1 | turns synchronized output off or on instead of asking the terminal |
TUIMARK_LOG | a file path | writes an NDJSON log of the session |
COLORFGBG | set by some terminals | the fallback for theme="auto" when the terminal does not answer the background query |
Values are exact; an empty value counts as unset. An invalid value makes Run return an error before it touches the terminal.
TUIMARK_LOG
A running app owns the terminal, so it cannot print diagnostics there. TUIMARK_LOG=session.ndjson ./myapp writes one JSON object per line instead: the start, the terminal's capabilities and the resolved theme, then every key, paste, mouse event, action, frame (with how long it took and how many bytes it wrote), and resize, and finally why it ended.
TUIMARK_LOG=session.ndjson ./myapp
jq -c 'select(.ev == "action")' session.ndjsonThe file is created with mode 0600. While a secret input has focus, keys and pastes are logged as redacted, and that input's events never carry its value.