Skip to content

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
<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>
term
$ 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 ===
ok

Each 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:

jsonc
// $ 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               │",
    "╰──────────────────────╯",
    "                        "
  ]
}
FieldHolds
cols, rowsthe size rendered
okfalse when any diagnostic is an error
focusthe focused node's id, or null
errorsevery diagnostic, warnings included, each with severity, code, and msg, plus file, line, col, path, and id when known
nodesevery laid-out node in document order, modals last
gridthe painted frame: rows strings, each exactly cols columns wide, no ANSI
widepresent 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
<tui>
  <screen id="main">
    <text id="name">東京 ok</text>
  </screen>
</tui>
jsonc
// $ 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:

jsonc
// $ 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.

jsonc
// $ 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 lineDoes
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".
  • quit ends the session; later steps are skipped.
  • --format json adds "events", each {step, action, source, keys, value}; --frames adds one settled frame per step. --styles and --cells work as for dump.
  • 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:

term
$ 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  inherited

It 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").

sh
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 JSON

Bind 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.

sh
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.

sh
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 validate runs, with the same messages, codes, and severities (error and warning), under both themes for a theme="auto" document. A diagnostic in a stylesheet shows on the stylesheet, not on the .tui that loads it: the server reads each <style src> as Load does (.. 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.json next to the .tui is used as if you had passed --data, so B001 to B003 and B008 to B010 appear. Without one they are skipped, as validate skips them without --data, and the same goes for a sample.json that is not valid JSON.
  • Completion. In a .tui: tag names, the attributes of the tag, the keywords of an enumerated attribute, #id targets for to, when, and focus, action names (the built-in ones and the ones the document already uses), key tokens in keys, and the paths of sample.json in {path}, each, bind, and if. In a .tcss: property names and their keywords, $tokens, pseudo-classes, @media features, and the #ids and .classes of the documents that load it. Everything is filtered by the document's version; 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, or focus) goes to its element, a .class to every element that has it, <style src> to its file, and a $token to where :root declares it.
  • Formatting. For a .tui, one edit with what tuimark fmt prints; none when the file is already formatted or is not well-formed XML. A .tcss has 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.

VariableValuesEffect
TUIMARK_THEMEdark, lightoverrides the theme, like --theme
TUIMARK_COLORtruecolor, 256, 16, noneforces the color depth instead of detecting it from TERM, COLORTERM, NO_COLOR, and friends
TUIMARK_SYNC0, 1turns synchronized output off or on instead of asking the terminal
TUIMARK_LOGa file pathwrites an NDJSON log of the session
COLORFGBGset by some terminalsthe 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.

sh
TUIMARK_LOG=session.ndjson ./myapp
jq -c 'select(.ev == "action")' session.ndjson

The 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.

Released under the MIT License.