# Tuimark: the complete documentation
> Tuimark is a view language for terminals: UI as .tui markup, .tcss styles and JSON data, interpreted by a small Go runtime that lays out a cell grid and dumps every frame as JSON for coding agents. Drive it from Go, TypeScript, or any language.
Source: https://tuimark.dev. The index of these pages is https://tuimark.dev/llms.txt.
---
# Getting started
URL: https://tuimark.dev/guide/getting-started
> Install the tuimark CLI, write a first document, check it with validate, dump, preview and play, and try it live with tuimark run.
Install the CLI, write a three-file document, and look at it the way an agent does: as a validated, dumped frame. Then run it for real from a few lines of Go.
## Install [#install]
Check that it is on your `PATH`:
```sh
tuimark version
```
## Your first document [#your-first-document]
A Tuimark view is text. Make a directory with three files: the structure (`app.tui`), the style (`theme.tcss`), and some data to render it with (`sample.json`).
```tui tab="app.tui"
{project}{t.title}
```
```tcss tab="theme.tcss"
#title { bold: true; color: $accent; }
#tasks { border: rounded; border-color: $border; }
#tasks:focus { border-color: $focus; }
#keys { dock: bottom; height: 1; }
.hint-key { color: $accent; }
```
```json tab="sample.json"
{
"project": "launch checklist",
"selected": "t1",
"tasks": [
{ "id": "t1", "title": "write the release notes" },
{ "id": "t2", "title": "tag v0.2.0" },
{ "id": "t3", "title": "publish the docs" }
]
}
```
Reading it top to bottom:
- `` is the root. It needs no `version`: without one, a document gets the whole language. See [versions](/guide/language#versions).
- `
```
```term
$ 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 ; did you mean ?
error V012 broken.tui:7:5 /screen/input is focusable and needs an id
```
The codes are listed in the [diagnostics reference](/reference/diagnostics). The exit status is 2, so a script or an agent can tell right away.
## Look at it [#look-at-it]
`dump` renders one frame at a size you choose and prints it: the grid of cells, then every laid-out node with its size and position, then the diagnostics.
```term
$ tuimark dump app.tui --data sample.json --cols 48 --rows 10
=== 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) selected
- text 46x1 @(1,2)
- item 46x1 @(1,3)
- text 46x1 @(1,3)
- item 46x1 @(1,4)
- 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
```
This is the whole point of Tuimark: the frame is data. `--format json` prints the same thing as JSON for a program to read, and the [dump format](/guide/tools#dump) is stable and versioned.
The text grid has no colors. To see them, use `preview`, which paints the frame with ANSI colors when its output is a terminal, and `--watch` repaints it whenever you save:
```sh
tuimark preview app.tui --data sample.json --cols 80 --rows 12 --watch
```
## Drive it [#drive-it]
`play` replays input against the document with no terminal at all, and prints the frame it ends on plus every action that fired. Here the list moves down twice, and `enter` fires `open`:
```term
$ 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
```
Each event line reads `STEP ACTION SOURCE KEYS VALUE`: at step 3, `open` fired from the list `tasks`, and `keys` names the row the cursor was on (`t` is the `each` alias). The `selected` marker moved to the third item, and `moved` fired on each step that moved the cursor.
`play` never runs host code: host actions are only recorded. That is what makes it safe to run anywhere, and exactly what you need to check a keymap before any Go exists.
## Try it live, without Go [#try-it-live-without-go]
`tuimark run` shows the document in your terminal and lets you use it: keys, focus, and the mouse all behave as they will in your app, and it reloads whenever you save.
```sh
tuimark run app.tui --data sample.json --strip
```
With no host program, every action the document fires is listed in a strip at the bottom of the screen (`F12` shows or hides it). A `--mock` file gives each action fixed store writes, enough for a clickable prototype: an `add` that clears the input, a `cancel` that closes a modal. See [Running a document](/guide/run).
To start a new project instead of writing the files by hand, `tuimark new NAME` writes a document, a stylesheet, sample data, and an `AGENTS.md` for coding agents, all of which validate cleanly. See [`new`](/guide/tools#new).
## Run it [#run-it]
A host program loads the document, gives it data, and handles the actions it names. Nothing else: layout, focus, keys, colors, and the terminal stay in the runtime.
```go
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/abdul-hamid-achik/tuimark"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
ui, err := tuimark.Load("app.tui")
if err != nil {
return err
}
raw, err := os.ReadFile("sample.json")
if err != nil {
return err
}
var data map[string]any
if err := json.Unmarshal(raw, &data); err != nil {
return err
}
if err := ui.Bind("", data); err != nil { // the whole store
return err
}
ui.On("open", func(ev tuimark.Event) error {
return ui.Set("project", fmt.Sprintf("opened %v", ev.Keys["t"]))
})
return ui.Run(os.Stdout)
}
```
```sh
go mod init hello
go get github.com/abdul-hamid-achik/tuimark@latest
go run .
```
`q` quits: `quit` is a built-in action, so it needs no handler. `moved` has no handler either, so it does nothing. See the [Go API](/guide/go-api) for everything else a host can do.
## Where next [#where-next]
- [Use Tuimark with AI agents](/guide/agents): the loop above, as an agent runs it.
- [The .tui language](/guide/language), [TCSS](/guide/tcss), and [Layout](/guide/layout): what you can write.
- [Widgets](/guide/widgets): lists, tables, tabs, inputs, modals, and the rest.
- [The kit](/guide/kit): ready-made components to copy into your project.
- [Examples](/examples): complete apps from the repository.
---
# Use Tuimark with AI agents
URL: https://tuimark.dev/guide/agents
> How a coding agent such as Claude Code or Cursor builds terminal UIs with Tuimark - the AGENTS.md briefing from tuimark agents, the tuimark mcp server, the validate, dump and play loop, and the rules that keep it on track.
Tuimark was designed for a coding agent to write. An agent is good at editing text and reading structured output, and cannot look at a terminal. So in Tuimark the UI is text, and every frame can be printed as JSON: the agent edits files, renders them, reads back exactly what is on the screen, and repeats.
Setting an agent up takes two steps: give it the briefing, and give it the tools, either as shell commands or as an MCP server.
## The briefing: AGENTS.md [#the-briefing-agentsmd]
`tuimark agents` prints a complete briefing for an agent: what to edit and what not to, the loop to follow, the tags and the attributes each takes, every CSS property and its values, the key tokens, every diagnostic code, and the behavior worth knowing. It is generated from the runtime's own catalogs, so it always matches the binary that printed it.
Put it where your agent looks for instructions:
```sh
tuimark agents --project > AGENTS.md
```
- **A new project** gets it for free: [`tuimark new`](/guide/tools#new) writes the same briefing as the project's `AGENTS.md`, next to a working `app.tui`, `theme.tcss`, and `sample.json`.
- **Claude Code** reads `CLAUDE.md`. Import the briefing from it with a line that says `@AGENTS.md`, so both files stay one.
- **Cursor, Codex, and most other agents** read `AGENTS.md` at the root of the repository directly.
Regenerate it when you upgrade Tuimark. Without `--project`, `tuimark agents` prints the briefing as the Tuimark repository itself uses it, with paths into its source; use `--project` everywhere else. The [reference](/reference) pages on this site are generated from the same output. Agents and editors that need the vocabulary as data, not prose, read [`tuimark catalog`](/guide/tools#catalog).
## Connect the MCP server [#connect-the-mcp-server]
An agent with a shell can run the loop below as commands. An agent that reaches tools through the Model Context Protocol gets the same loop from `tuimark mcp`: five read-only tools, `tuimark_validate`, `tuimark_dump`, `tuimark_play`, `tuimark_inspect`, and `tuimark_agents`, each returning exactly what the matching command prints. It needs only the `tuimark` binary on your `PATH`.
Add it for yourself:
```sh
claude mcp add tuimark -- tuimark mcp
```
or for everyone who works on the repository, which writes it to `.mcp.json` at the root:
```sh
claude mcp add --scope project tuimark -- tuimark mcp
```
Add it to `.cursor/mcp.json` in the project (or `~/.cursor/mcp.json` for every project):
```json
{
"mcpServers": {
"tuimark": {
"command": "tuimark",
"args": ["mcp"]
}
}
}
```
Any MCP client that starts stdio servers takes the same two values: the command `tuimark` and the single argument `mcp`. Most of them read the `mcpServers` JSON shown for Cursor; see your client's documentation for where it goes.
[MCP server](/guide/mcp) lists every tool's arguments and how to read a result.
## Docs for agents [#docs-for-agents]
This site is written for agents too. Every page has a Markdown version at the same URL plus `.md` (this page is [`/guide/agents.md`](/guide/agents.md)), which a request with `Accept: text/markdown` also gets. [`/llms.txt`](/llms.txt) indexes them, and [`/llms-full.txt`](/llms-full.txt) is the whole site in one file, for an agent that should read everything first.
## The loop [#the-loop]
The whole method fits in four commands:
```sh
tuimark validate app.tui --data sample.json
tuimark dump app.tui --data sample.json --cols 80 --rows 24 --format json
tuimark dump app.tui --data sample.json --cols 120 --rows 24 --format json
tuimark play app.tui --data sample.json --input "tab down enter" --styles --format json
```
1. **Edit** the `.tui`, the `.tcss`, and the sample data.
2. **Validate.** Fix every error; read the warnings. Each diagnostic has a code, a position, and usually says what to write instead.
3. **Dump** at 80 and 120 columns (and 40 if it has to fit a narrow pane). Read `grid` for what the screen shows and `nodes` for where each element landed, and check that `errors` is empty.
4. **Play** the interactions: read `events` to confirm the right actions fired with the right `keys` and `value`, and `styles` to confirm that focus and selection actually change how cells look.
Stop when the dumps match the intent at every width and nothing is reported. Then a person, or a host program, can run it for real.
An agent connected to [the MCP server](#connect-the-mcp-server) runs the same loop as tool calls: `tuimark_validate`, `tuimark_dump`, and `tuimark_play` take the same arguments as the flags above and return what the commands print.
### Reading a dump [#reading-a-dump]
| Question | Where to look |
|---|---|
| Does it render at all? | `ok` is `true` and `errors` is empty |
| Is the text right? | `grid`, one string per row |
| Is this panel the right size, in the right place? | the node's `x`, `y`, `w`, `h` in `nodes` |
| Is the list scrolled to the selection? | the list's `scroll` and the `selected` item |
| Does focus start in the right place? | `focus` |
| Did a conditional class apply? | the node's `classes` |
| Is the focused row highlighted? | `play … --styles`: the row's spans have `"a": ["reverse"]` |
| Why is this cell this color? | `tuimark inspect app.tui --at X,Y` |
## Rules that keep an agent on track [#rules-that-keep-an-agent-on-track]
The briefing states them, and they are worth repeating in your own instructions:
- **Edit only view files**: `*.tui`, `*.tcss`, and sample data. The view never needs Go.
- **Never invent a tag or a property.** The vocabulary is closed; an invented one is a diagnostic, not a feature. Check the [tags](/reference/tags) and [properties](/reference/css) instead.
- **No logic in markup.** If the view needs a computed value, it goes in the data, and the host computes it.
- **No terminal UI framework code.** No hand-written layout, widths, padding, or truncation in the host; Tuimark does all of that from the document.
- **Name actions; do not implement them in markup.** `on:select="open"` is a name, and the host decides what it means.
## A prompt that works [#a-prompt-that-works]
> Build the view for a deploy dashboard in `ui/app.tui` and `ui/theme.tcss`, with realistic data in `ui/sample.json`: a pipeline list on the left, a log panel on the right, a status bar, and a confirm modal for deploying. Read `AGENTS.md` first. Loop with `tuimark validate` and `tuimark dump --format json` at 80 and 120 columns until there are no diagnostics, and check the keymap with `tuimark play`. Do not write any Go. When you are done, list the action names the view fires.
## Handing off to the host [#handing-off-to-the-host]
When the view is done, the host needs to know what to handle. `tuimark validate app.tui --json` includes the action catalog: every name, where it is used, and which are built in. Save the names the host implements to `actions.json`, and from then on:
```sh
tuimark validate app.tui --catalog actions.json
```
reports any action the view fires that the host does not know (`B004`), which catches a renamed action on either side. In Go, `ui.Catalog()` gives the same list; a [test](/guide/testing#go-tests) can require a handler for every name.
## Keeping it honest [#keeping-it-honest]
- Commit goldens for the sizes you checked ([testing](/guide/testing#golden-dumps)): the next agent that edits the view gets a failing test and a diff, not a surprise.
- Keep `sample.json` realistic, with the long names, empty lists, and wide characters real data will have.
---
# The .tui language
URL: https://tuimark.dev/guide/language
> How a Tuimark document is structured, the closed set of tags, common attributes, text, screens and modals, and document versions.
A `.tui` file is a strict subset of XML that describes what is on the screen: containers, text, and widgets, with their data bindings and the names of the actions they fire. It holds no logic. How things look lives in [TCSS](/guide/tcss), the data in JSON, and what actions do in the host program.
## A document [#a-document]
```tui
……
```
- **``** is the root. It needs no `version` (see [versions](#versions)). `theme` is `dark` (the default), `light`, or `auto`. `mouse` turns mouse input on; see [the mouse](/guide/interaction#the-mouse).
- **`
1230%1fr2fr
```
```grid
┌──────────┐┌────────────────┐┌────────┐┌──────────────────┐
│12 ││30% ││1fr ││2fr │
└──────────┘└────────────────┘└────────┘└──────────────────┘
```
At 60 columns, `#a` takes 12 and `#b` takes 30% of 60, 18. The 30 cells left are shared one part to `#c` and two parts to `#d`. The last `fr` item gets any remainder, so no cell is lost to rounding.
## The box model [#the-box-model]
`width` and `height` are the **outer** size, border included. Inside it:
```
outer the allocated rect: x, y, w, h
border one cell on each side, when there is one
padding inside the border
content what is left for children or text
```
`padding` and `margin` take one to four cell values, in the CSS order (top, right, bottom, left). `gap` (0–4) puts empty cells between a container's children. A size that would leave a negative content box is clamped to zero, and anything that does not fit is clipped, never wrapped onto the next line.
In `version="3"`, `row-gap` and `column-gap` (0–4) split `gap` into its two axes: `column-gap` between siblings side by side (a `layout: row` container, grid and table columns, a tab strip, a `hints` laid out along a row), `row-gap` between siblings stacked (a `layout: column` container, grid rows, a `hints` with `layout: column`). `gap` stays their shorthand in the cascade — it sets both, with its own specificity and order — so a document that writes only `gap` lays out exactly as before.
## Direction and alignment [#direction-and-alignment]
`col` stacks its children vertically, `row` horizontally, and `box` is a column unless you set `layout: row`. Any container takes `layout: column | row` (and, in version 2, `grid`).
| Property | Axis | Values |
|---|---|---|
| `justify` | main | `start`, `center`, `end`, `space-between` |
| `align` | cross | `stretch` (the default), `start`, `center`, `end` |
| `content-align` | text inside its box | `start`, `center`, `end` |
With the default `align: stretch`, children fill the cross axis. That is usually what you want, but it is also why a row of buttons in a tall modal grows tall: give such a row `height: 1`.
## Default sizes [#default-sizes]
| Tag | Width | Height |
|---|---|---|
| `screen` | 100% | 100% |
| `col`, `row` | `1fr` | `1fr` |
| `box` in a `row` | `auto` | `1fr` |
| `box` in a `col` | `1fr` | `auto` |
| `text`, `input`, `button`, `progress`, `rule` | `auto` | `auto` |
| `list`, `scroll`, `table` in a `col` | `1fr` | `auto`, fit to the space left |
| `sparkline` | one column per value | 1 |
| `modal` | 80% of the screen | 80% of the screen |
If any sibling on an axis has an `fr` size, siblings with no size on that axis become `1fr` too.
## How space is shared [#how-space-is-shared]
Along the main axis, a container hands out its content size in this order:
1. fixed sizes (`N`);
2. percentages, of the parent's full content size (not of what is left);
3. `auto` children, at their content size;
4. viewports sized `auto`, at their content size but capped by what is left ([auto-fit](#auto-fit));
5. what remains, split between the `fr` children by weight.
`min-width`, `max-width`, `min-height`, and `max-height` clamp a child after its percentage and before the `fr` split. There is no flex-wrap: a row that does not fit is clipped.
## Docks [#docks]
`dock: top | bottom | left | right` pulls a child out of the flow and pins it to an edge of its parent. Top and bottom strips are cut first, then left and right, and the other children share what is left.
```tui
header · dock: topstatus · dock: bottomdock: lefteverything else
```
```grid
header · dock: top
┌────────────┐┌────────────────────────────────┐
│dock: left ││everything else │
│ ││ │
│ ││ │
│ ││ │
└────────────┘└────────────────────────────────┘
status · dock: bottom
```
Give a docked child a size across its edge: a `height` for `top` and `bottom`, a `width` for `left` and `right`. An `fr` size on the same axis as a dock is `V010`, and more than one bottom-docked status line is warned about (`L005`).
## Scrolling [#scrolling]
A **viewport** clips its content and scrolls it: a `scroll`, a `list`, a `table`, or any container with `overflow: scroll`.
- `` (the default) scrolls vertically, `axis="x"` horizontally, `axis="both"` both ways. `list`, `table`, and `overflow: scroll` scroll on `y`.
- A `scroll` takes the arrow and paging keys when it has focus; give it `focusable="true"` and an `id`. A list or table follows its cursor instead.
- In version 2, `scrollbar: auto` paints a thumb over the right border of a bordered viewport at least 3 rows tall whose content does not fit. It never takes layout space.
- In `version="3"`, `` keeps a growing viewport showing its end, like a log or a chat: while the offset is already at the bottom it keeps sticking as content arrives, but scrolling up to read back stops it from jumping, until you scroll (or `move-last`) back to the end. It needs an `id` and a `y` axis, and cannot go inside a list `` or a container `each` template.
The dump reports each viewport's `scroll` state (offset and extent), and a list reports every item, including the ones scrolled out of view.
## Auto-fit [#auto-fit]
A viewport sized `auto` on its scroll axis takes its content size, capped by the space its siblings leave. A short list stays short; a long one fills what is left and scrolls, and whatever comes after it stays on screen.
```tui
deploy log{l.n} {l.text}{count} lines
```
```grid
deploy log
┌──────────────────────────────┐
│9 rotate secrets │
│10 warm caches │
│11 notify #deploys │
│12 done │
└──────────────────────────────┘
12 lines
```
Twelve lines of log do not fit in 8 rows, so the list takes the 6 rows the title and the status line leave, and scrolls to keep its cursor (the last line) in view. Viewports are served in document order: the first may leave nothing for the next. To share space between two viewports, give both an `fr` size.
`L006` warns when a viewport can never show any of its content: zero rows while it has some, or clipped away by an ancestor that does not scroll. The fix is a size (`N`, `%`, or `fr`) or more room in the parent.
## Grids [#grids]
In version 2, `layout: grid` places a container's children in equal columns, row by row, which is what a dashboard of panels needs:
```tui
23%61%73%1.202.1 MB/s52 C
```
`grid-columns: K` is the most columns (1 to 12). With `grid-min-width: M`, the count adapts to the width: as many columns of at least `M` cells as fit, up to `K`, so the grid is responsive without any `@media`. The same document at 64 and 36 columns:
```grid
╭─ cpu ────────────╮ ╭─ memory ─────────╮ ╭─ disk ─────────────╮
│23% │ │61% │ │73% │
╰──────────────────╯ ╰──────────────────╯ ╰────────────────────╯
╭─ load ───────────╮ ╭─ net ────────────╮ ╭─ temp ─────────────╮
│1.20 │ │2.1 MB/s │ │52 C │
╰──────────────────╯ ╰──────────────────╯ ╰────────────────────╯
```
```grid
╭─ cpu ─────────╮ ╭─ memory ───────╮
│23% │ │61% │
╰───────────────╯ ╰────────────────╯
╭─ disk ────────╮ ╭─ load ─────────╮
│73% │ │1.20 │
╰───────────────╯ ╰────────────────╯
╭─ net ─────────╮ ╭─ temp ─────────╮
│2.1 MB/s │ │52 C │
╰───────────────╯ ╰────────────────╯
```
`gap` separates both columns and rows. A row is as tall as its tallest child, and `align` places shorter ones. A child's `width`, `min-width`, `max-width`, `flex`, and any `fr` or `%` height are ignored in a grid (`L007` warns). A grid never shrinks to fit: put it in a `` (or make it `overflow: scroll`) to scroll its rows when they do not fit.
## Responsive layouts [#responsive-layouts]
`@media` switches rules by terminal size. This is the tour document from the [home page](/): at 56 columns its panes stack.
```tcss
#body { gap: 1; }
#tickets { width: 45%; border: rounded; border-color: $border; }
#tickets:focus { border-color: $focus; }
#detail { width: 1fr; border: rounded; border-color: $border; }
#keys { height: 1; }
.hint-key { color: $accent; bold: true; }
@media (max-cols: 60) {
#body { layout: column; gap: 0; }
#tickets { width: 100%; height: 5; }
#detail { width: 100%; height: 1fr; }
}
```
```grid
╭─ tickets ────────────────────────────────────────────╮
│t-1 login loop on staging │
│t-2 cannot deploy europe-west │
│t-3 stale feature flag cache │
╰──────────────────────────────────────────────────────╯
╭─ t-2 ────────────────────────────────────────────────╮
│Deploys to europe-west fail at the health check: the │
│new probe path returns 404. │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────╯
q quit enter open
```
`display: none` removes a node from the layout entirely (the monitor example hides table columns that way on narrow terminals); `visibility: hidden` keeps its space and paints nothing.
## Checking a layout [#checking-a-layout]
Dump at the widths you care about and read the nodes:
```sh
tuimark dump app.tui --data sample.json --cols 80 --rows 24
tuimark dump app.tui --data sample.json --cols 120 --rows 24 --format json
```
Layout problems are diagnostics with an `L` code. The ones you will meet:
| Code | Meaning |
|---|---|
| `L001` | an `fr` size or `flex` on a child along its viewport's scroll axis, where there is no leftover to share |
| `L002` | a `%` size inside a parent whose size on that axis is `auto` |
| `L003` | fixed and minimum sizes that add up to more than the parent (a warning; the rest is clipped) |
| `L004` | a modal that is not the last child of its screen |
| `L006` | a viewport that can never show its content |
| `L007` | a size on a grid cell, which the grid ignores |
The full list is in the [diagnostics reference](/reference/diagnostics#layout).
---
# Bindings and data
URL: https://tuimark.dev/guide/bindings
> How Tuimark markup reads a JSON store - {path} interpolation, bind, each, if, truthiness and class:NAME guards - and why it has no expressions.
A Tuimark app has one data store: a JSON value, usually an object. The host fills it with `Bind` and changes it with `Set`; the CLI loads it from a file with `--data sample.json`. Markup reads it through **paths** and never computes anything: every value it shows is already in the store.
## Paths [#paths]
A path is a dotted chain of names: `title`, `ticket.body`, `cpu.hist`. Each name is a letter or `_` followed by letters, digits, or `_` (no hyphens, no spaces, no indexes). Inside an `each` template, the alias comes first: in `each="tickets as t"`, `t.title` is the current ticket's title.
Arrays are reached only through `each`; `tickets.0.title` is not a path markup can use.
## Interpolation [#interpolation]
`{path}` inserts a value into text. It works in four places: the body of a ``, a `title`, a `placeholder`, and the body of a `` (the cell template of a [table](/guide/widgets#table)).
```tui
{done} of {total} done ({done})…
```
The value is formatted as text:
| Value | Shows as |
|---|---|
| a string | itself, with control characters and ANSI escapes removed |
| an integer-valued number | `3`, `1024` |
| another number | the shortest form that reads back the same: `2.5`, `1e+20` |
| `true`, `false` | `true`, `false` |
| `null` | nothing |
| an array or object | compact JSON, object members sorted: `[1,"a"]`, `{"a":2,"b":1}` |
A path that does not resolve shows nothing and reports `B003`, a warning (`--strict` makes it an error). Anything between braces that is not a plain path, such as `{ folder }` or `{a + b}`, is `V003`. A `{` with no closing `}` is just text.
## Widgets that write back [#widgets-that-write-back]
`bind` connects a widget's own state to a path, in both directions:
| Widget | `bind` holds | Written when |
|---|---|---|
| `input` | its text | the user types |
| `list`, `table` | the key of the cursor row | the cursor moves |
| `tabs` | the id of the active tab | a tab is activated |
| `progress` | a number from 0 to 100 | never (read only) |
| `sparkline` | an array of numbers | never (read only) |
| `modal` | truthy while open | never (read only) |
So the host never has to track what is typed or selected: it is in the store, and the `on:change` or `on:select` event says when it moved. Setting the path from the host moves the widget.
## Repeating with each [#repeating-with-each]
`each="path as alias"` repeats a template once per element of an array, in order. On a `list` (and in version 2 a `table`), the template is the `` and each element is a row:
```tui
{t.id} {t.title}
```
- `key="path"` names each row. The cursor, `bind`, checked rows, and the `keys` of every event use it, so a row keeps its identity when the array is re-sorted or filtered. Without `key`, rows are numbered by index and you get `B006`.
- The path must be an array: a missing or non-array value is `B001`.
- Nothing inside a list row can take focus. The list is the focusable unit and moves its own cursor; act on a row through `on:select` or a keymap row with `when="#tickets:focus"`, whose event names the row.
In version 2, `each` also works on `col`, `row`, and `box`. The container is not repeated; its children are the template, inflated once per element:
```tui
{tag}
```
Such a template holds nothing that takes focus or keeps state (no `input`, `button`, `list`, `modal`, `table`, `tabs`, `tab`, no `focusable`, `on:click`, or `on:focus`) and no `id`, since it would repeat. An alias that shadows an enclosing one is `V011`. An empty array leaves the container empty, so it matches `:empty`.
## Conditions [#conditions]
`if="path"` keeps a node only while the value is truthy; `if="!path"` only while it is not:
```tui
{selected_ticket.title}no selection
```
| Value | Truthy? |
|---|---|
| `null`, `false`, `0`, `""`, `[]` | no |
| everything else, `{}` included | yes |
A path that does not resolve reports `B002` and counts as `null`: `if="missing"` removes its node and `if="!missing"` keeps it.
`hidden` and `disabled` take the same `path` and `!path`, or a literal `true` or `false`. A hidden node is not laid out; a disabled one stays visible but cannot take focus, and keys, actions, and clicks skip it. A modal's `open` and the document's `mouse` flag work the same way.
## Conditional classes [#conditional-classes]
In version 2, `class:NAME="path"` adds the class `NAME` while the path is truthy (or `class:NAME="!path"` while it is not). It is how data changes the look of a node without any expression in markup:
```tui
{status_label}
```
```tcss
.live { color: $ok; }
.stale { color: $danger; }
```
The guard is evaluated on every frame, in the node's scope, so inside a template it reads the alias: `` colors the rows of protected processes. `NAME` is lowercase: a letter or `_`, then letters, digits, `_`, or `-` (`V015` otherwise). A missing path is `B002` and adds nothing.
The JSON dump of a version 2 document lists each node's `classes`, the static ones first, so you can check that a guard fired without reading colors.
## No expressions [#no-expressions]
Markup cannot compute: no arithmetic, comparisons, string formatting, or filtering. When a view needs a derived value (a count, a label like `cpu 23%`, a flag like `cpu_hot`, the filtered rows), the host computes it and puts it in the store. This keeps every `.tui` file readable at a glance, keeps the runtime small, and gives an agent exactly one place to look when a value is wrong: the data.
```json
{
"count": 3,
"cpu": { "label": "cpu 23%", "pct": 23, "hot": false },
"tickets": [{ "id": "t-12", "title": "login loop on staging" }]
}
```
Keep a `sample.json` next to each document with realistic data, including the awkward cases (long strings, empty arrays, wide characters). The CLI renders with it, `validate --data` checks every path against it, and golden tests pin it.
---
# Keymap, actions and focus
URL: https://tuimark.dev/guide/interaction
> How keys, clicks and focus become named actions in Tuimark - the keymap, when selectors over the focus chain, built-in actions, key dispatch, event payloads and the mouse.
Markup never runs code. When something happens (a key, a click, a cursor move, a submitted input), the runtime fires an **action**: a name, with a small payload that says where it came from. The host program registers handlers for the names it cares about. A few names are built in.
## Actions are names [#actions-are-names]
Widgets name actions in `on:*` attributes:
```tui
…
```
| Attribute | On | Fires when |
|---|---|---|
| `on:select` | `list`, `table`, `tabs` | the cursor moves to another row, or another tab becomes active |
| `on:change` | `input`; `list`, `table` with `checked` | the text changes; the set of checked rows changes |
| `on:submit` | `input` | `enter` is pressed in it |
| `on:click` | any node, `button` above all | `enter` or `space` on it while focused, or a mouse click |
| `on:focus` | any focusable node | it receives focus |
| `on:open`, `on:close` | `modal` | it opens or closes |
| `on:escape` | `modal` | `esc` is pressed while it is the top modal |
An action name is a plain identifier (`open`, `ask_deploy`). A name with no handler does nothing, so a view can be written and tested before its host exists; pass `--catalog actions.json` to `validate` to have unknown names reported (`B004`).
## The keymap [#the-keymap]
Global keys live in one ``. Each `` row maps one or more keys to an action:
```tui
```
- `keys` is a comma-separated list of [key tokens](/reference/keys): `q`, `enter`, `ctrl+a`, `shift+tab`, `/`. Nothing is implicit: if you want `j` and `k` to move a list, bind them.
- `when` is a selector that must match for the row to apply. It is how a key means different things in different places.
- Rows are tried in document order, and the first match wins, so put specific rows before general ones.
- `label` (and `keycap`, the text shown instead of the key) make the row a key hint that [``](/guide/widgets#hints) displays (version 2).
A document may hold **several `` elements**, in every version: their rows form one keymap, in document order (the first keymap's rows, then the second's). It is a way to group rows without repeating a `when` on each one: in `version="3"`, `` gives every row inside it that `when`, unless a row writes its own, which replaces it outright rather than combining with it (`when=""` on a row, or on the `` itself, means no `when`).
```tui
```
### when and the focus chain [#when-and-the-focus-chain]
In a `version="2"` document, `when` matches if its selector matches the focused node **or any ancestor** up to the screen; with nothing focused, it matches against the top open modal and then the screen. So:
- `when="#procs:focus"`: only while the table `procs` itself has focus;
- `when="#processes"`: while focus is anywhere inside the tab `processes`;
- `when="#kill"`: while focus is inside the modal `kill`;
- `when="#app"`: while focus is in the main view and no modal has it, when `#app` is the screen's first child and the modals are its siblings.
In a `version="1"` document, `when` matches the focused node only.
## Built-in actions [#built-in-actions]
Two built-ins work everywhere:
| Action | Does |
|---|---|
| `quit` | stops the app. With no `quit` handler registered, `Run` returns; with one, it returns when the handler returns `ErrQuit`. |
| `focus` | moves focus to the node `to="#id"` names |
Version 2 adds built-ins for the common widget moves, so no host code is needed for them. They are allowed only in keymap rows, and act on the node `to` names, or else on the focused node:
| Action | On a list or table | On tabs | On a scrolling viewport |
|---|---|---|---|
| `move-next`, `move-prev` | cursor down or up one row | next or previous tab, wrapping | scroll one row |
| `move-first`, `move-last` | first or last row | first or last tab | to the top or the bottom |
| `move-page-down`, `move-page-up` | a page of rows | not applicable | a page |
| `check-toggle` | check or uncheck the cursor row | | |
| `check-all`, `check-none` | check every row, or none | | |
| `switch-to` | | activate the tab `to` names | |
`switch-to` also switches to another screen when `to` names one.
A built-in row only matches when its target can take it: `move-next` needs a list with rows, `check-all` needs a list with `checked`, and so on. When it cannot, the key falls through to the next row, so a global `` never steals `ctrl+a` from an input. A row that matches takes its key even if nothing changes (`move-next` on the last row). The events these actions cause, such as `on:select` or `on:change`, fire as if the user had moved the cursor.
```tui
```
## Focus [#focus]
One node has focus at a time, or none.
- `input`, `list`, `button`, and `modal` (and in version 2 `table`) can take focus. Any other node can with `focusable="true"`; a `tabs` needs it to take `left` and `right`.
- A focusable node needs an `id` (`V012`).
- `tab` and `shift+tab` cycle through focusable nodes in document order, skipping hidden and disabled ones.
- `` picks the first focused node; otherwise it is the first focusable one.
- An open modal traps focus: `tab` cycles inside it, and a request to focus a node outside it does nothing. When it closes, focus returns to where it was.
- The host moves focus with `Set("@focus", "#id")`, and a keymap row with `action="focus" to="#id"`.
## How a key is handled [#how-a-key-is-handled]
The same procedure serves the live terminal, `tuimark play`, and ``. For each key:
1. **`esc` and a modal.** If the key is `esc` and the top open modal has `on:escape`, that fires and the key is used up.
2. **The focused widget.** A focused, enabled widget takes its own keys:
- an `input` takes printable keys, `space`, `backspace`, `ctrl+h`, `ctrl+u`, `left`, `right`, `home`, `end`, `ctrl+a`, `ctrl+e`, and `enter` when it has `on:submit`;
- a `list` or `table` with rows takes `up`, `down`, `home`, `end`, `pgup`, and `pgdn`;
- a focusable `tabs` takes `left` and `right`;
- another scrolling viewport takes the arrows and the paging keys;
- any other node with `on:click` takes `enter` and `space`.
3. **The keymap.** The first row whose keys, `when`, and (for a built-in) target all match fires.
4. **The defaults.** `tab` and `shift+tab` move focus, and `ctrl+c` fires `quit`.
Step 2 is why typing `q` into a search box types a `q` instead of quitting, and why `` hides the `q quit` hint while the box has focus.
In version 2, each key sees the frame the previous key left, even when several arrive in one read (a fast typist, a paste): a `when` or a class guard that the first key changed applies to the second.
## Events [#events]
A handler receives an `Event`:
| Field | Holds |
|---|---|
| `Action` | the action name |
| `Source` | the `id` of the node that fired it |
| `Keys` | the `each` aliases of the row involved, by key: `{"t": "t-12"}` |
| `Value` | the input's text; the new `checked` array; a tab's id; otherwise usually `null` |
A keymap row that fires a host action reports the **focused** node as its source, not the node its `when` matched, and the cursor row of a focused list or table as its `keys`. So `` tells the handler exactly which ticket to delete.
## The mouse [#the-mouse]
In version 2, `` turns the mouse on while the flag is truthy. It takes `true`, `false`, a path, or `!path`, and is off by default, because a terminal in mouse mode no longer lets people select text. A setting can switch it with `Set`.
With the mouse on, only the left button and the wheel act:
- **A click** acts when the button is released over the same node it was pressed on. Walking up from the cell: a disabled or hidden node stops it; a tab label activates its tab; a list or table row focuses the widget and moves the cursor there; a table column header with `on:click` fires it (sortable columns); a node with `on:click` takes focus if it can and fires it; any other focusable node takes focus.
- **The wheel** moves a list or table cursor by one row, a tab strip by one tab, or another viewport by one row.
- While a modal is open, only events inside it count.
`tuimark inspect --at X,Y` names the node a click at that cell starts from, and `play` replays clicks with `click:X,Y`, `wheel-up:X,Y`, and `wheel-down:X,Y`.
## Checking interactions without a terminal [#checking-interactions-without-a-terminal]
`tuimark play` runs the same key handling with no terminal and records every action that fired. Check a keymap before the host exists:
```sh
tuimark play app.tui --data sample.json --input "tab down down enter" --format json
tuimark play app.tui --data sample.json --input "text:deploy enter"
tuimark play app.tui --data sample.json --input "click:12,3 wheel-down:12,5"
```
See [play](/guide/tools#play) for every kind of step.
---
# Widgets
URL: https://tuimark.dev/guide/widgets
> Tuimark's built-in widgets - list, input, button, progress, sparkline, table, tabs, hints and modal - with rendered examples, their events and the keys they take.
Tuimark has a small, fixed set of widgets. Each one below is shown with a document you can copy and the frame it renders; the pictures come from `tuimark play` on exactly these files. All of them share one `data.json`:
data.json
```json
{
"query": "",
"cursor": "internal/app.go",
"marked": ["go.mod"],
"files": [
{ "path": "cmd/app/main.go" },
{ "path": "internal/app.go" },
{ "path": "go.mod" },
{ "path": "README.md" }
],
"build": {
"pct": 62,
"hist": [12, 18, 25, 22, 40, 52, 47, 63, 71, 66, 80, 92, 88, 70, 64, 75, 90, 97, 85, 78, 60, 55, 49, 62, 68, 74, 81, 77, 72, 69]
},
"cpu_cursor": 9921,
"procs": [
{ "pid": 412, "name": "WindowServer", "cpu": "87.3", "mem": "1.4G", "hot": true },
{ "pid": 8810, "name": "Code Helper", "cpu": "41.8", "mem": "904M", "hot": true },
{ "pid": 9921, "name": "node", "cpu": "18.2", "mem": "388M", "hot": false },
{ "pid": 15023, "name": "go", "cpu": "9.7", "mem": "221M", "hot": false }
],
"view": "processes"
}
```
## list [#list]
A focusable list with a cursor. Its rows come from `each` over an array, or from static `` children.
```tui
{f.path}
```
- `bind` holds the cursor row's key; `on:select` fires when the cursor moves. A focused list takes `up`, `down`, `home`, `end`, `pgup`, and `pgdn`.
- The cursor row is `:selected`, reversed by the built-in sheet (`list > item:selected { reverse: true; }`).
- **Multi-select** (version 2): `checked="path"` holds the keys of the checked rows, an array the runtime reads and writes; `mark="✓"` reserves room for the mark at the start of every row; `on:change` fires with the new array. Checked rows match `:checked`. Bind the keys yourself with the `check-toggle`, `check-all`, and `check-none` built-ins.
- Rows do not take focus: a row cannot hold an `input`, `button`, `list`, or `modal`. Act on the cursor row with `on:select`, or with a keymap row scoped to the list, whose event names the row.
- Without a size, a list fits its rows into the space left and scrolls ([auto-fit](/guide/layout#auto-fit)); `scrollbar: auto` shows where you are.
## input [#input]
A single-line text field. `bind` holds its text.
```tui
```
- `placeholder` shows, dimmed, while it is empty. `secret="true"` paints one `•` per character, and keeps the text out of `TUIMARK_LOG`.
- `on:change` fires with the new text as the event's value; `on:submit` fires on `enter`.
- A focused input takes printable keys, `space`, `backspace`, `left`, `right`, `home`, `end`, `ctrl+a`, `ctrl+e`, `ctrl+h`, and `ctrl+u` before the keymap sees them, so typing never triggers shortcuts. A paste arrives as one edit and one `on:change`.
- An input is one row of text. With a border it is three rows tall, as above; a `height: 1` would leave the border no room.
## button [#button]
```tui
```
The label comes from `label` or the body, painted as `[ Deploy ]`. A focused button fires `on:click` on `enter` or `space`, and on a mouse click. Show focus with `button:focus { reverse: true; }`.
## progress and sparkline [#progress-and-sparkline]
A `progress` is a bar from 0 to 100, from `bind` or a literal `value`. A `sparkline` (version 2) draws the last values of a numeric array as bars, one column per value and eight levels per row.
```tui
blockeighthsvalue
```
- `bar: eighths` (version 2) fills eighths of a cell for a smoother bar; `bar: block`, the default, fills whole cells.
- A sparkline is naturally one column per value and one row tall; give it a `height` for more levels. Values are right-aligned in the width it gets, `null` is a gap, and `min` and `max` fix the range (otherwise the shown values set it).
- The bar color is the widget's `color`.
- **`scale="NAME"`** (`version="3"`) shares a range across every sparkline of one frame that carries the same name: they all take `lo`/`hi` from the smallest and largest of their shown values together, so a group of related gauges reads on one scale. A sparkline's own `min`/`max` still wins for that one; an inactive tab's sparkline is not in the group.
## table [#table]
Rows of an array, in columns, with a cursor (version 2). Each `` has a header `title` and a body that is the cell template.
```tui
{p.pid}{p.name}{p.cpu}{p.mem}
```
- It keeps a cursor like a list: `bind` holds the cursor row's key, `on:select` fires with the row in `keys`, and a focused table takes the arrow and paging keys. `checked`, `mark`, and the `check-*` built-ins work as on a list.
- **Column widths**: a number of cells; a `%` of the table; an `fr` share of what is left, clamped by `min-width` and `max-width`; or, by default, the widest of the title and every cell.
- **Styling**: a rule on a column styles its header cell. Body cells carry the column's `class` and its `class:NAME` guards, evaluated per row, so `.num { content-align: end; }` right-aligns a whole column and `#procs > item > text.hot` colors only the hot cells. One attribute-only `` styles whole rows.
- Only the rows in view are laid out, painted, and dumped, so a table of thousands of rows costs what its visible rows cost.
- Cells never wrap. `placeholder` shows on the first row when the array is empty.
- `display: none` on a column hides it. Under `@media`, that is how a table drops columns on a narrow terminal. The same document at 30 columns:
```grid
╭────────────────────────────╮
│ PID NAME CPU%│
│ 412 WindowServer 87.3│
│ 8810 Code Helper 41.8│
│ 9921 node 18.2│
│ 15023 go 9.7│
╰────────────────────────────╯
```
A `column` with `on:click` fires when its header is clicked, the natural place for sorting.
- **`priority="N"`** (`version="3"`) hides a column automatically when the visible columns no longer fit: the column with the smallest `N` hides first (a non-negative integer; the last one in document order breaks a tie), and it comes back once the table is wide enough again. A hidden column is exactly a `display: none` one — no header cell, no body cells — but it still counts toward the table's intrinsic width. Prefer `priority` to `@media` rules for column hiding; a column without `priority` never hides this way.
## tabs [#tabs]
A strip of tab labels over the content of the active tab (version 2).
```tui
panelsthe process tablesettings
```
- With `bind`, the active tab is the one whose id is the bound string, and activating a tab writes its id there; `on:select` fires with the id as the value. Without `bind`, the runtime remembers the tab.
- Tabs switch with the `switch-to`, `move-next`, and `move-prev` built-ins, with `left` and `right` on a `focusable="true"` tabs, with a click on a label, and when focus moves into an inactive tab.
- Only the active tab is laid out; widgets in the others keep their state (a list's cursor, an input's text).
- When a tab becomes active while focus was in the old tab, on the strip, or nowhere, focus moves to the new tab's `focus="#id"` target, or its first focusable node.
- **Labels always fit.** Every `label` when there is room, else every `short`, else only the active tab as `‹ label ›`. The same strip at 24 and 16 columns:
```grid
1 ovr ▸2 proc 3 cfg
the process table
```
```grid
▸‹ 2 processes ›
the process tabl
```
Labels are generated `text` nodes with the class `tab-label`, `:selected` on the active one; style them with `.tab-label`.
In `version="3"`, `label` and `short` are templates, resolved every frame exactly like a `` body (`{path}`, no expressions), so a tab can show a live count: ``. A missing path resolves to empty, and any line break in the resolved value becomes a space, since a label is one row. In `version="1"`/`version="2"`, `{path}` in either attribute stays `V003`.
## hints [#hints]
Key hints generated from the keymap (version 2). Every `` row with a `label` is a hint; `keycap` replaces the key text shown.
```tui
{f.path}
```
By default (`scope="active"`), a hint shows only while one of its keys would actually fire, through the same key handling the app uses. Press `/` and focus moves to the input, which takes `q`, `/`, `space`, and `ctrl+a` for itself, so only the hint that still applies is left:
```grid
/ to filter
╭──────────────────────────────────────────╮
│cmd/app/main.go │
│internal/app.go │
│go.mod │
│README.md │
╰──────────────────────────────────────────╯
esc done
```
`scope="all"` lists every hint, for a help screen. Items are laid out along the row (or down it with `layout: column`), and the ones that do not fit are dropped from the end. Style them with `.hint-key` and `.hint-label`.
## modal [#modal]
A layer over the screen, open while `open` is truthy.
```tui
the files you markedThis cannot be undone.
```
- A modal is centered, 80% by 80% of the screen unless sized, with a single border by default and its `title` on it.
- It traps focus while open, gives it back when it closes, and fires `on:open`, `on:close`, and, on `esc`, `on:escape`.
- A modal must be the last child of its screen (`L004`). Several can be open; the last one in the document is on top.
- Give the button row `height: 1`. Rows stretch across the modal by default, and a focused `reverse` button would paint the whole stretched rect.
- **`focus="#id"`** (`version="3"`) names the node that takes focus when the modal becomes the top one, instead of the first entry of its focus cycle; the target must be inside the modal. It also doubles as the fallback when the node focused inside the modal is lost (it leaves the frame or can no longer take focus): focus goes to it when it still can take focus, else to the first entry of the cycle as usual.
## scroll, rule, spacer [#scroll-rule-spacer]
```tui
{log}leftright
```
- `scroll` clips and scrolls its content; with `focusable="true"` it takes the arrow and paging keys. See [scrolling](/guide/layout#scrolling).
- `rule` is a one-cell line, horizontal by default (`axis="y"` for vertical).
- `spacer` takes the free space in a row or column, pushing its siblings apart.
---
# The kit
URL: https://tuimark.dev/guide/kit
> A copy-paste component kit for Tuimark - design tokens, k- component classes and 21 small documents you copy into your project and own, with a gallery app that shows them all.
The kit is a set of ready-made components for Tuimark: buttons, cards, alerts, a data table, a command palette, dialogs, toasts, and more. It follows the shadcn way: you do not install it, you **copy what you need into your project and own it**. There is nothing to import and nothing to keep in sync; if a card should look different, you edit your copy.
The kit adds no tag, attribute, or CSS property to the language. A component is ordinary `.tui` markup plus class names, ids, and action names that follow a few conventions, styled by two stylesheets. It lives in the repository's [`kit/`](https://github.com/abdul-hamid-achik/tuimark/tree/main/kit) directory.
| File | Holds |
|---|---|
| `kit/theme.tcss` | the design tokens, dark and light, and the base typography |
| `kit/kit.tcss` | the component classes |
| `kit/components/NAME.tui` | one small, valid document per component, with the markup to copy and a `NAME.sample.json` beside it when it binds data |
| `examples/kit-gallery` | every component in one app, on five tabs, with a Go host |
## Using it [#using-it]
Copy `theme.tcss` and `kit.tcss` into your project and load them first, then your own sheet **last**. There is no `@import` in TCSS, so each document lists the sheets it uses:
```tui
api-gateway 2.14.0
```
The sheets are cascaded together, and where two rules tie the later one wins. That is how you rebrand the kit: every color is a token in `:root`, so your own sheet only re-declares the ones you want to change.
```tcss
/* app.tcss, loaded after theme.tcss and kit.tcss */
:root { --accent: #e8590c; --focus: #e8590c; }
@media (theme: light) {
:root { --accent: #c2410c; --focus: #c2410c; }
}
```
Every document in the kit validates on its own, so you can try a component before copying it:
```sh
tuimark validate kit/components/table.tui --data kit/components/table.sample.json
tuimark run kit/components/table.tui --data kit/components/table.sample.json
```
Each component file starts with a **contract**, an XML comment that says what it needs from your store and your host:
```text
k-NAME · Contract: · Actions: · Keys: · Requires:
```
## Tokens [#tokens]
All color lives in `:root` of `theme.tcss`, read as `$name`. The first ten names are the runtime's own required tokens (the same names its built-in themes define); the kit adds the rest.
| Token | Use |
|---|---|
| `bg` `fg` `muted` `border` `panel` | the screen, body text, secondary text, idle borders, cards and modals |
| `accent` `focus` | the brand color, and the border or fill of the focused thing |
| `ok` `warn` `danger` `info` | status colors |
| `subtle` `raised` `sel` | tertiary text, raised or inset fills, the cursor row of an unfocused list |
| `on-accent` `on-ok` `on-warn` `on-danger` | text drawn on a filled color |
| `code` | inline code |
Selection and focus never rely on a background color alone. A cursor row is also bold, a focused list or table gets a double border and a solid accent bar, and a focused button is inverted and underlined, so all of it still reads under 16 colors.
## Conventions [#conventions]
**Classes.** Every kit class starts with `k-`: `k-card` for a component and `k-card-title` for a part of it. (TCSS has no descendant combinator, only `>`, so a part cannot be styled as "text inside a card"; each part carries its own class.) A **modifier** is a second, short class on the same node, such as `primary`, `ok`, `warn`, `danger`, and it is only ever used next to its component class: `k-btn danger`, `k-badge ok`, `k-alert warn`. Pick a modifier from your data with `class:NAME="path"` guards.
**Ids** are `instance__part`, with two underscores: `confirm__dialog`, `confirm__yes`, `confirm__no`, `profile__email`. A node that is the whole component keeps the instance name (`svc` for a table), and so do the singletons of your app (`app`, `status`, `nav`).
**Actions** are `component_verb`, a plain identifier: `dialog_confirm`, `palette_run`, `toast_show`, `form_submit`, `table_select`. Generic names such as `cancel` or `save` are avoided. An action does not say which instance fired it; a host routes by the prefix of the event's `source` before the last `__`, so `confirm__yes` belongs to the instance `confirm`, and one handler serves every dialog.
**Overlays** (dialogs, the palette, the help) are modals that close on `esc`, write their own size, and declare their keys in a keymap scoped to their own class, listed before any global keymap:
```tui
```
**Sizes are explicit.** A component that fills a space says so in its class, and a `tabs` writes `focusable`, so nothing depends on the default size of a bare `row`, `col`, or modal.
## Components [#components]
| Component | Class | In one line |
|---|---|---|
| badge | `k-badge` | a short status label, with a modifier for its color |
| button | `k-btn` | `primary`, `secondary`, `danger` and `ghost`, inverted when focused |
| card | `k-card` | a bordered panel with a title that takes the focus color while it holds focus |
| alert | `k-alert` | a callout with an icon, in four status colors |
| app bar | `k-appbar` | a one-row bar docked to the top |
| status bar | `k-statusbar` | a one-row bar docked to the bottom, with filled segments |
| key hints | `k-hints` | the keymap's labeled rows as a row of hints |
| confirm dialog | `k-dialog` | a modal that puts the cursor on the safe button |
| form field | `k-field` | a label, an input, help text and an error line, with invalid styling |
| empty state | `k-empty` | an icon, a title, a line of help and a call to action, centered |
| data table | `k-table` | a table with a header, right-aligned numbers and columns that hide by priority |
| menu | `k-menu` | a navigation list with icons, shortcuts and optional check marks |
| tabs | `k-tabs` | a tab strip bound to a path |
| labeled progress | `k-meter` | a label, a value and a bar in eighths |
| stat | `k-stat` | a KPI card with a change and a sparkline |
| toast | `k-toast` | a one-row notice that never moves the page |
| command palette | `k-palette` | a modal with an input over a filtered list |
| help overlay | `k-help` | a modal that lists every labeled key |
| split pane | `k-split` | a list beside a detail card, stacked on a narrow terminal |
| log panel | `k-log` | a scrolling log that follows its end |
| breadcrumb | `k-breadcrumb` | the current path, down to the last crumb on a narrow terminal |
Markup has no expressions, so a few components lean on the host: the palette's filtered list and the split pane's detail are computed with `Derive`, and a toast is cleared by an `Every` tick. The contract comment of each file says which, and the [helpers](#helpers) below do the common ones for you.
## Helpers [#helpers]
A component is markup, and markup has no expressions. Expiring a toast, filtering the palette, validating a form and routing a dialog's answer have to happen in the host. The kit ships that code as **helpers**: small files in Go and in TypeScript that write the store paths the components read. Like the components, you copy them and own them. They are not a package: `kit/go` is a nested module named `tuimarkkit`, with no dot in its name so that `go get` cannot fetch it, and `kit/ts` has no published name.
To use one, copy its file into your project. In Go, change `package kit` to your package; a helper imports only `github.com/abdul-hamid-achik/tuimark`. In TypeScript, copy the `.ts` file next to your parent; it imports only types from `@thelacanians/tuimark`. A file that needs another says so in its header: `dialog` and `form` need `route`, and `palette` needs `filter`. Each helper has a test beside it. The Go tests of `toasts`, `form`, `dialog`, `palette` and `sortpage` load the real component documents and drive them; the others, and the TypeScript tests, run against the store alone.
| Helper | What it does | Writes |
|---|---|---|
| `route` | `Instance(source)` and `Part(source)`: the two halves of an `instance__part` id | nothing |
| `toasts` | `Push(kind, text, ttl)` into a capped queue that shows one toast at a time and expires them | `toast.show`, `.text`, `.is_ok`, `.is_warn`, `.is_danger` |
| `filter` | substring or fuzzy match over an array of objects, by named fields | `{items, any, count, total}` at a path you choose |
| `sortpage` | cycle the sort of a column (with an arrow in its title) and paginate (`1–50 of 340`) | the sorted rows of the page, and a meta object |
| `form` | per-field rules (required, pattern, length, your own function) on change and on submit | `forms.