Getting started
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 the CLI with Go
One static binary, no CGO. It lands in $(go env GOPATH)/bin; make sure that is on your PATH.
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@latest- 01Check it
tuimark version - 02Read the briefing
tuimark agents - 03Render a frame
tuimark dump app.tui
Check that it is on your PATH:
tuimark versionYour 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>
<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>#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; }{
"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:
<tui>is the root. It needs noversion: without one, a document gets the whole language. See versions.<style src>loads the stylesheet, relative to the document.- The
<keymap>bindsqto the built-inquit, andenterto an action namedopen, but only while the list has focus. Thelabels make them key hints. - The
<screen>holds a title, a list, and a<hints>bar.{project}and{t.title}read the JSON data;each="tasks as t"repeats the<item>once per task;bind="selected"ties the list's cursor to a value in the data. on:select="moved"names an action. Markup never runs code: it only names what happened, and a host program decides what that means.
Check it
validate reads the document and its stylesheet, lays it out at 40, 80, and 120 columns, and reports every problem it finds. --data lets it check the bindings too.
$ tuimark validate app.tui --data sample.json
okWhen something is wrong, each problem is one line: severity, a stable code, the position, and a message. This document has a typo in a tag, a unit that does not exist in a terminal, and an input with no id:
<tui>
<style>
#tasks { width: 30px; }
</style>
<screen id="main">
<lsit id="tasks"/>
<input bind="query"/>
</screen>
</tui>$ 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 idThe codes are listed in the diagnostics reference. The exit status is 2, so a script or an agent can tell right away.
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.
$ 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 ===
okThis 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 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:
tuimark preview app.tui --data sample.json --cols 80 --rows 12 --watchDrive 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:
$ 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"} nullEach 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
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.
tuimark run app.tui --data sample.json --stripWith 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.
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.
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.
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)
}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 for everything else a host can do.
Where next
- Working with agents: the loop above, as an agent runs it.
- The .tui language, TCSS, and Layout: what you can write.
- Widgets: lists, tables, tabs, inputs, modals, and the rest.
- The kit: ready-made components to copy into your project.
- Examples: complete apps from the repository.