Skip to content

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
  1. 01
    Check ittuimark version
  2. 02
    Read the briefingtuimark agents
  3. 03
    Render a frametuimark dump app.tui

Check that it is on your PATH:

sh
tuimark version

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
<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>
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
{
  "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 no version: without one, a document gets the whole language. See versions.
  • <style src> loads the stylesheet, relative to the document.
  • The <keymap> binds q to the built-in quit, and enter to an action named open, but only while the list has focus. The labels 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.

term
$ tuimark validate app.tui --data sample.json
ok

When 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
<tui>
  <style>
    #tasks { width: 30px; }
  </style>
  <screen id="main">
    <lsit id="tasks"/>
    <input bind="query"/>
  </screen>
</tui>
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 <lsit>; did you mean <list>?
error V012 broken.tui:7:5 /screen/input <input> is focusable and needs an id

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

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 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 ​

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 ​

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.

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.

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 for everything else a host can do.

Where next ​

Released under the MIT License.