Most terminal UIs are programs. Layout, colors, state, and key handling live in the same code, and the only way to see what a change did is to run it in a terminal and look. That is slow for a person, and it is worse for a coding agent, which cannot look at all: it edits the code, runs it, and gets back a stream of escape sequences.
Tuimark takes the view out of the program. A screen is a document, the runtime draws it, and any frame can be read back as data.
A screen is three files
The structure is a .tui file, the style is a .tcss file, and the content is JSON. This is a whole working view:
<tui>
<style src="theme.tcss"/>
<keymap>
<bind keys="q" action="quit" label="quit"/>
<bind keys="enter" action="open" when="#tasks:focus" label="open"/>
</keymap>
<screen id="main" focus="#tasks">
<text id="title">{project}</text>
<list id="tasks" each="tasks as t" key="t.id" bind="selected" on:select="moved">
<item>
<text>{t.title}</text>
</item>
</list>
<hints id="keys"/>
</screen>
</tui>
There is no code in it. {project} and each="tasks as t" read the data, bind="selected" ties the list's cursor to a value in it, and on:select="moved" only names what happened. A host program, in Go, in TypeScript, or in any language that can write JSON lines, decides what moved and open mean and writes new data back. The markup never runs anything.
A closed vocabulary
Tuimark has a fixed set of tags, attributes, CSS properties, and key names, and every one of them is listed in the reference. Nothing outside it means anything, so nothing outside it is silently ignored: a typo is a diagnostic with a code, a position, and often a suggestion.
$ tuimark validate broken.tui
error V003 broken.tui:3:14 width: bad size "30px" (want N, N%, Nfr, or auto)
error V001 broken.tui:6:5 /screen/lsit unknown tag <lsit>; did you mean <list>?
error V012 broken.tui:7:5 /screen/input <input> is focusable and needs an id
That matters most to an agent. A model that invents a plausible tag or property gets an error it can act on, instead of a screen that quietly looks wrong. The V, L, and B codes (parse, layout, and binding problems) are all in one table.
Frames are data
tuimark dump lays the document out at a size you choose and prints the frame: the grid of cells as text, and every node with its rectangle, its focus, and its selection. tuimark play does the same after replaying keys, and lists the actions that fired:
$ tuimark play app.tui --data sample.json --cols 48 --rows 10 --input "down down enter"
=== grid 48x10 ===
launch checklist
╭──────────────────────────────────────────────╮
│write the release notes │
│tag v0.2.0 │
│publish the docs │
╰──────────────────────────────────────────────╯
q quit enter open
=== nodes ===
main screen 48x10 @(0,0)
title text 48x1 @(0,0)
tasks list 48x5 @(0,1) focus
- item 46x1 @(1,2)
- text 46x1 @(1,2)
- item 46x1 @(1,3)
- text 46x1 @(1,3)
- item 46x1 @(1,4) selected
- text 46x1 @(1,4)
keys hints 48x1 @(0,9)
- row 6x1 @(0,9)
- text 1x1 @(0,9)
- text 4x1 @(2,9)
- row 10x1 @(8,9)
- text 5x1 @(8,9)
- text 4x1 @(14,9)
=== errors ===
ok
=== events ===
1 moved tasks {"t":"t2"} null
2 moved tasks {"t":"t3"} null
3 open tasks {"t":"t3"} null
With --format json, the same frame is JSON, and --styles adds the colors and attributes of every cell. An agent checks its work by reading this: is the list 48 cells wide, did the cursor land on the third row, did enter fire open with the right key? Nobody has to eyeball a screenshot, and every picture on this site is drawn from exactly this output.
The loop
Put together, designing a screen becomes a loop that needs no terminal:
tuimark validatethe document, with--datato check the bindings too.tuimark dumpit at 80 and at 120 columns, and read the grid and the nodes.tuimark playthe keys a person would press, and readevents.- Edit the markup or the stylesheet, and repeat until both widths match the intent and there are no errors.
Only then does anyone write a handler. tuimark agents prints a briefing, AGENTS.md, generated from the binary you have installed, so an agent learns this exact vocabulary and this loop, and tuimark new writes it into every new project. tuimark mcp serves the same commands as MCP tools for an agent without a shell. The agents guide has the setup.
The same frame in tests
Because a frame is text, it can be committed. tuimark test renders each document again and compares it with a saved dump, a golden, so a change to the geometry, the text, or the colors of a screen shows up as a failing test and a readable diff in review. The same Dump, Validate, and Play are methods on the Go API, for tests next to the host code. See testing.
What stays in the host
Markup has no expressions, on purpose. Filtering a list, formatting a number, or deciding which pane to show is host work: the Go API's Derive keeps a computed value at a path in the data, and the document reads it like any other value.
ui.On("open", func(ev tuimark.Event) error {
id, _ := ev.Keys["t"].(string)
return ui.Set("current", lookup(id))
})That split is the point. The document says what the screen is, the host says what the app does, and both can be checked on their own.
If that sounds like the way you want to build terminal tools, or the way you want your agent to, start with getting started. It takes a few minutes, and the first thing you write is one of these documents.