Repository navigation
Releases: abdul-hamid-achik/tuimark
Release list
v0.4.5
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.4.5Docs: https://tuimark.dev
Static props and one default slot for templates (0.4c), and Kit v2 built
on them. A <template> declares props="a b", a <use> passes them as
attributes, and {$a} reads them in the template's body. A <slot/> in a
template marks where the content of a <use> goes; that content is built
where it is written. Documents that use neither, and every document that
writes version, behave as in 0.4.4.
Added
- Props.
props="a b"on a<template>declares static props.- A
<use>passes them as attributes (title="Deploy"), and{$a}
reads them in the template's body: in attribute values, in the text of
atext,buttonorcolumn, and in the other places the composition
guide lists. - A prop is literal text, substituted once per copy. When an attribute's
whole value is a prop the use does not pass, the attribute is left out.
Anywhere else, an unpassed prop is the empty string. - Where a prop cannot stand (an
id, a path,each, a<use>'s
template, inside{…}), or outside a template, the error isV003.
A prop the template does not declare isV002on the<use>.
- The slot.
- A
<slot/>in a template marks where the content of a<use>goes. - The content is built where it is written: the use doesn't scope its
ids, its paths read the store and the aliases in scope there, and its
includes resolve next to its own file. The use'sclassnever reaches
it. - A
<slot/>inside a use's content in a template forwards the outer
use's content. - A template has at most one slot (a second is
V019). - Content toward a template without a slot is
V013
<template id="T"> has no <slot/>. Loose text in a<use>isV013. - A misplaced slot is
V001: outside a template, brought by an
<include>in a use's content, inside a list<item>, or where
content repeats per row. <slot>in a document that writesversionisV001with the
requires hint.
- A
- Tools.
tuimark ir:- a copy's
<slot/>is aslotnode holding its fill; - templates keep
props, and uses in a template body keep their
content; - every placed node has
from.
- a copy's
tuimark inspect:vialists slot entries
(via <slot/> of <use id="U" template="T"> FILE:LINE:COL).tuimark fmt: formats a use's content and<slot/>.tuimark lsp:- completes a use's props,
{$name}in a template, andslot; - hover and definition lead from a prop to the template's
props.
- completes a use's props,
tuimark catalog: listsslot,propsontemplate, and
"open_attrs": "props"onuse.
- Kit v2.
- The kit's templates take literal text as props and, where a component
has a body, content through<slot/>. - New templates:
k-card: a titled panel; its body is the use's content.k-appbar: brand and title props; the right side is the content.k-kv: one key/value row,
<use template="k-kv" label="Owner" with="svc.owner as v"/>.
k-alert,k-emptyandk-confirmtake content too.- Every template takes a
styleprop for its root, which a rule cannot
name unless the template gives it an id (the overlays'U__dialog,
the log'sU__view). k-stattakesminandmax, the sparkline's range.k-helplists its keys in a scroll (U__keys), which takes the focus,
so a long keymap scrolls without a height rule in the app.
- The kit's templates take literal text as props and, where a component
- Docs.
- The composition guide has sections on props, the slot, and when to
use props,with, or the slot. - README's "Language notes" and AGENTS.md cover them too.
- The composition guide has sections on props, the slot, and when to
Changed
- Messages. "Tuimark has no slots" is gone from every message.
Content in an<include>now says to wrap it with a template and a slot.
TheV002of an attribute a<use>does not take now ends with "or
declare it in the template's props" when the name could be a prop. - Help overlay. The kit's help overlay shows "? close help" as its own
line, above the list of keys, and the kit's.k-helpkeymap row has no
label any more.- A
<hints scope="all">in a document that includes the kit without
using the help no longer shows "? close help". - A hint bar under the open help no longer repeats it.
- The overlay itself looks as before.
- A
- Kit v2: literal text is a prop, not data.
- These templates and props:
k-alert:title,icon,text;k-stat:title;k-meter:label;k-empty:icon,title,text;k-field:label,placeholder,help;k-palette:title,placeholder;k-help:title.
- The members they replace are no longer read. A use that still passes
them only throughwithshows no title or label, with no diagnostic.
The kit README's "Upgrading from kit v1" section has a migration table.
- These templates and props:
k-empty. Its call to action is the use's content: your button,
your id, your action. The kit no longer hasempty_createor
U__create.- Palette helpers. The kit's palette helpers (Go and TypeScript) no
longer write a defaulttitleandplaceholderinto the store. - Examples.
examples/containersincludeskit/kit.tuiand uses its templates.
Its detail tab is 45 lines instead of 72, and the whole document 209
instead of 274.examples/kit-galleryshows "v2".
v0.4.4
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.4.4Docs: https://tuimark.dev
Kit v1: the component kit rebuilt on the composition of 0.4.3. A document
includes kit/kit.tui once and uses its templates with <use>, instead of
copying each component's markup. The language and the runtime do not
change: only the kit's files, its helpers, and the documents that load the
kit's stylesheets do.
Added
kit/kit.tui: the kit as one fragment to include. It loads the kit's
stylesheets, holds the keys of its overlays, and has eleven templates
(k-alert,k-stat,k-meter,k-empty,k-field,k-breadcrumb,
k-log,k-toast,k-confirm,k-palette,k-help) used as
<use template="k-…" with="path as x">, with a variant as aclasson the
<use>, labels from data, and ids scoped by the use (ID__part).- The kit's dialog helpers take button labels:
Request.Yes/Request.No
(Go) andyes/no(TypeScript); empty meansConfirmandCancel, and
both labels are written on every open, so an earlier question's labels
never stay. - The
/kitpage shows each template's source next to the document that
uses it; the kit guide shows how to include the kit, pass data with
with, and pick a variant withclass.
Changed
- Every
kit/components/*.tuiincludes../kit.tuiinstead of loading the
two stylesheets; the eleven components that have a template now show it in
use, and the markup of the ten class-only components is otherwise
unchanged. - The templates read from data the text the old component documents wrote
as literals: an alert's title, text, and icon, a stat's title, a meter's
label, a field's label, placeholder and help, the empty state's icon and
call to action, the log's title, the palette's title and placeholder, the
help's title, and the dialog's button labels. Data written for the old
documents shows these blank, and leaves out the optional ones (the
alert's icon, the empty state's button). - The palette helpers write a default
title(Command palette) and
placeholder(type a command…) when the store has none. - The confirm dialog reads
body,yes, andno(it readmessage, with
fixed labels), and the dialog helpers writebody,yes, andno
instead ofmessage..k-dialogtakes the size of its content, up to 60
columns (it was 52 wide), so every dialog with that class changes,
examples/containers' included. - The confirm dialog's yes button is
k-btn primary, painted red in a
dangerdialog (the variant ord.danger); it wask-btn dangerin
every dialog. - The confirm dialog's
yandnkeys have no label, so no<hints>lists
them: the dialog's buttons name the choices, and the keys work while it
is open. - A kit field's input is
FORM__FIELD__input, and the log's scroll is
ID__view. The kit's form helpers route the new ids and focus
FORM__FIELD__inputon a failed submit. - The alert's icon takes its color from the alert's variant, so the
.k-alert-icon.info|ok|warn|dangerrules are gone; the meter's bar also
takes the meter's variant (its.k-meter-bar.ok|warn|dangerflag rules
stay). examples/kit-galleryis built from the kit's templates. Its help overlay
no longer listsy confirmandn cancel, and its hint bar no longer
shows them while the confirm dialog is open.
v0.4.3
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.4.3Docs: https://tuimark.dev
The 0.4b release. It extends the current language, the language of a
<tui> without version, in three directions:
- reuse and split markup:
<include>and<fragment>put a document in
several files, and<template>and<use>stamp a piece of markup with
its own ids and its own data; - form controls:
<checkbox>,<select>, radio groups (single), a
spinner (<progress indeterminate>), and{path}in a button's label; - defaults for people coming from the web: content-sized
colandrow,
anescthat closes a modal, a modal that traps the keymap, a focus look
on buttons.
There is no version="4": writing it is V003, and the document is read as
version="1". Tools that need a number for the current language use 4. A
document that writes version="1", "2", or "3", and the spike <app>
root, keeps exactly the meaning, layout, diagnostic codes, and dumps it had
in 0.4.1. A tag, attribute, or {path} that 0.4.3 adds is V001, V002,
or V003 in such a document, with a message ending in (requires a <tui> without version) (the hint for <template> and its kin used to say Tuimark
had no components); an include's file is never read there.
Changed — versionless documents
A <tui> without version changes layout and keys with 0.4.3. To keep
0.4.1's behavior, write <tui version="3">; nothing else in the document
needs to change. To see what a document gains or loses, copy it with
version="3", run tuimark dump --format json on both at 80 and 120
columns, and diff the results. Measured this way on the 15 examples that
write no version, at five sizes, none changes its base layout, because each
writes the sizes it relies on; with a modal open, some show the focus look,
the trapped keymap's shorter hint bar, or content-sized hints. Documents that
leaned on the old defaults change in these ways.
Layout.
colandroware content-sized (autoon both axes) except as
direct children of ascreen, amodal, or atab, where they stay
1frby1fr. A sidebarcolbeside a bodycolis as wide as its text,
and the body needswidth: 1frto take the rest; panes that are wider than
their row push the last ones out (L009), and awrap: wraptext in a
content-sized pane is measured unwrapped. Thecolandrowchildren of an
eachcontainer take their content size instead of equal shares,justify
acts in containers that1frchildren no longer fill, and a%child of a
content-sized container isL002. A header or statusrowwritten directly
in ascreenis still1fr, so give itheight: 1.boxis unchanged (it
was never1fron its main axis).- The sibling-
frrule applies to viewports only. An unsized sibling of
anfrchild becomes1fronly when it is ascroll, alist, atable,
or hasoverflow: scroll; atext,button,box,tabs, orhintsnext
to anfrpane stays at its content size. A filterrowabove a list in a
tab, ascreen, or amodalis1frand splits the space with the list:
give itheight: 1. frmeetsmin-*andmax-*as CSS does. A child that amaxclamps
(or aminlifts) is frozen at that size, and the otherfrchildren share
what it frees: three1frcolumns in 40 cells, the first withmax-width: 10, are 10, 15, and 15, where a pinned document gives 10, 13, and 14 and
leaves 3 cells empty. Table columns keep their old split.modalandhintsare content-sized. Amodalwith no size is as big
as its content, wide enough for its title, capped by a writtenmax-width
ormax-heightand otherwise by 80% of the screen (a pinned document
gives 80% by 80%). Ahintsis as big as its items along their axis and
drops the items that do not fit, with no diagnostic, so a status row of a
text, aspacer, and<hints/>keeps the text and the hints whole.- An
inputismax(width(placeholder) + 1, 20)columns wide when it
has nowidth, whatever its value, so it no longer grows while you type.
Keys and focus.
esccloses the top modal that has noon:escape, by writingfalse
at itsopenpath (itsbindpath when it has noopen); the next frame
fireson:closeand gives focus back. A modal whoseopenistrue,
false, or!path, or whose path aDerivecomputes, stays open, because
there is nothing to write: use one of these, oron:escape, for a dialog
that must not be dismissed (the game-over modals ofsnakeandtetris).- A modal traps the keymap. While one is open, a keymap row fires only
when its effectivewhenmatches the focus chain up to the top modal, so a
qquit row withoutwhenno longer quits under a confirmation, and a
ctrl+crow withoutwhengives way to the built-in quit.tab,
shift+tab,ctrl+c, and the keys the focused widget takes still work, and
<hints/>shows only the rows that fire. A key that must also work under a
modal is repeated in<keymap when="modal">, a group whose rows all carry
thatwhen. - A
tabsis focusable withoutfocusable="true":tabreaches it,
leftandrightswitch the tab, and focus stays on the strip when the
active tab changes.focusable="false"turns it off. Specs that counttab
presses change; ascreen focus=keeps the old initial focus. - A focused
button,checkbox, orselectis reversed by the built-in
sheet (button:focus, checkbox:focus, select:focus { reverse: true; }).
A document whose buttons have another focus style addsreverse: falseto
it; the kit's.k-btn:focusnow does. ctrl+i,ctrl+j, andctrl+min a keymap areV003, once per
token, because a terminal delivers them astabandenterand they never
matched. Writetaborenter. Pinned documents accept them as before.
Messages and output.
L002andL009name the fix.L002under acolorrowends with
(width: 1fr fills it)(orheight), andL009on a content-sizedcol,
row, orboxsays that the child is sized by its content and does not
shrink, and to give itwidth: 1fr(orflex: 1), or to writeoverflow: hiddento keep the cut.tuimark irprints"version": "0.4"for a versionless document (it
printed"0.3"), validated by the newschema/ir.v0.4.json, andtuimark inspectends with asourceline (--json: asourcemember) naming the
element a node was built from. Pinned documents print neither.
Added
-
<include src>and<fragment>.<include src="parts/forms.tui"/>
places the elements of another file where it stands: directly under
<tui>, in a<screen>, in a container, or in a<keymap>.srcis
relative to the file that writes the include. The file's root is
<fragment>, which takes no attributes and holds any number of elements,
<template>s, a<style>, a<keymap>, or further includes. Ids, styles,
and data are shared and the result is checked as if written where the
include stands; a diagnostic about a placed element ends with, placed by <include> at FILE:LINE:COL. A fragment is named by its path relative to the
document, and the same fragment may be included twice (its ids then repeat:
V004). Failures insert nothing and the rest of the document is still
checked:- an include cycle is
V006, judged by file identity, so a symbolic link
cannot hide one; so are a file outside the file systemLoadFSwas
given, one that cannot be read, and one that is not a regular file of at
most 64 MiB; - a stylesheet, or a file whose root is not
<fragment>, is the newV020.
A fragment is not a document:
dump,play,preview,ir, and
inspectprintV005for it ("check it through the document that includes
it") and exit 2, whilevalidatechecks it on its own (ok (fragment checked on its own), with or without--data) andfmtformats it. - an include cycle is
-
<template>and<use>.<template id="field">holds a piece of
markup that is never drawn by itself, and<use id="email" template="field" with="form.email as f"/>is replaced, when the document is read, by a copy
built where the use stands. A template is a direct child of<tui>or
<screen>, in the document or in a fragment, and may come after its uses.
A<use>is not a node, so it takes no space and holds no content (no
slots).- Scoped ids. The copy's ids are prefixed with the use's id and two
underscores: theinputof<use id="email">isemail__input, and a
use inside a template adds its own prefix (billing__street__input).
Outside the copy (to=,when=,focus=,#idselectors,focus:
steps, an event'ssource) you write the scoped id; inside the template
#xmeans the element of the same copy. A use needs anidwhen its
template holds any (V012), and naming an unscoped id from outside is
B005with the scoped form in the message. with="path as alias"makes the alias stand for the path inside the
copy, so{f.label}readsform.email.labelandbind="f.value"reads
and writesform.email.value; clauses are separated by exactly,. A
copy also reads the aliases in scope where its use stands (a list row's
eachalias). Awithpath that is missing is oneB003at the use and
nothing more from the copy, and a flag read through an alias that stands
for an object (if,hidden,disabled,open,class:NAME) isnull
when the member is missing, so a field shows an error only when the host
wrote one.- Guards and classes.
ifandhiddenon a use are read where the use
stands and are decided per row in a list<item>, aneachcontainer, or
a table. A stati...
- Scoped ids. The copy's ids are prefixed with the use's id and two
v0.4.2
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.4.2Docs: https://tuimark.vercel.app
A patch on 0.4a with no change to the language. Tuimark ships on npm as
@thelacanians/tuimark (the TypeScript SDK and a tuimark command that
downloads the verified release binary), the component kit gains copyable Go
and TypeScript host helpers, examples/ gains a container-engine dashboard
built on the kit, and the runtime builds for js/wasm for the website's
playground.
Added
-
The npm package
@thelacanians/tuimark(sdk/ts): the SDK and the CLI
in one package.import { spawnHost } from "@thelacanians/tuimark"is the
TypeScript client that was@tuimark/host(never published), and its new
tuimarkcommand (npx @thelacanians/tuimark new my-app) runs the Go
binary of the release with the same version, with the same arguments,
stdio, exit code, and signals (and fd 3 and fd 4 oftuimark hostwhen a
parent starts the installed command directly; parents should prefer
spawnHost, or run the binary themselves). The launcher is plain JS for
Node 18 or later and Bun, with no dependencies and nopostinstallscript.
It finds the binary inTUIMARK_BINARY, else the cache ($XDG_CACHE_HOME
or~/.cache,~/Library/Caches,%LOCALAPPDATA%, then
tuimark/<version>/<os>_<arch>/tuimark), else it downloads the release's
tuimark_<version>_<os>_<arch>[.exe], checks its SHA-256 against a table
built into the package from the release'schecksums.txt(a mismatch is
discarded and never run), and caches it; a cached binary is checked against
the same hash on every run.TUIMARK_RELEASE_URLpoints it at a mirror,
checked by the mirror's ownchecksums.txtand cached apart.
spawnHostuses the same lookup whenbinis not given, and
resolveBinaryis exported. -
Raw binaries in the release. GoReleaser now also uploads
tuimark_<version>_<os>_<arch>[.exe](Go's own OS and arch names) for the
six platforms, listed inchecksums.txt, next to the archives. The Homebrew
cask still uses only the archives. -
npm publishing in
release.yml. After GoReleaser, thenpm-packjob
(no OIDC token) builds the package at the tag's version, embeds the six
checksums, and checks the release has the six binaries, and the
npm-publishjob (GitHub environmentnpm, which allows onlyv*tags)
publishes it with npm trusted publishing and provenance. Run by hand from a
release tag ("Use workflow from"), the workflow publishes that tag's
package. -
The Tuimark kit (v0).
kit/is a copy-paste component kit in the
spirit of shadcn: two stylesheets (theme.tcss, the dark and light design
tokens on top of the runtime's ten required ones;kit.tcss, thek-
component classes) and 21 small documents underkit/components/(badge,
button, card, alert, app bar, status bar, key hints, confirm dialog, form
field, empty state, data table, menu, tabs, labeled progress, stat, toast,
command palette, help overlay, split pane, log panel, breadcrumb), each with
a contract comment, markup to copy, and a sample. Classes arek-<component>
andk-<component>-<part>with short modifiers used only next to them, ids
areinstance__part, and actions arecomponent_verb. It adds no tag,
attribute, or property. -
examples/kit-gallery: every component in one app on five tabs, with a
Go host that routes by the prefix of an event's source, amock.json, and
tests that hold the kit's conventions to every document. -
More fuzz targets (internal;
task fuzzruns each briefly, outside
task verifyand CI): the document parser (diagnostic codes, positions,
determinism,fmtkeeps the diagnostics), the binding, key-token and
size grammars, the stylesheet cascade,playstep parsing, and a
whole-runtime render of any document or any stylesheet (grid shape,
snapshot independence, no hang). About an hour of local fuzzing found no
crash. -
Kit host helpers (
kit/go,kit/ts). The behavior markup cannot
express, as files you copy into your project and own, not a
public package:route(an event's instance and part),toasts(a capped
queue that expires),filter(substring or fuzzy, into a store path with
counts),sortpage(column sort cycling with arrows, and pages),form
(per-field rules writingforms.<form>.<field>.errorand
forms.<form>.valid),dialog(Confirm, routed by instance),palette
(a command registry),async(<path>.loading,.error,.data) and
format(bytes, durations, relative time, percentages, thousands).kit/go
is a nested module namedtuimarkkit(no dot, sogo getcannot fetch it)
whose tests drive the real kit components;kit/tsis typed against
@thelacanians/tuimark. Both run in CI and intask verify. For the helpers to be
sound, two kit contracts moved: a form is named by the id prefix of its
inputs (profile__name) with its state atforms.<form>.<field>.{value, error}, and the palette's input fires apalette_queryaction as the query
changes (a host withoutDerive, such as a TypeScript parent, recomputes
foundthere). -
examples/containers: a container-engine dashboard built only from the
kit, over a simulated engine (a seeded state machine on an injected clock; no
Docker). A filterable, sortable, multi-select table with<column priority>,
a detail pane (overview card, CPU and memorysparklinepair, nested tabs for
astick="bottom"log, the environment and the ports), images, volumes and an
events log; stop, restart and remove (a danger dialog) and the prunes through
the kit's confirm dialog, a command palette filtered byDerive, a help
overlay, toasts, a status bar and key hints. It shipssample.jsonand
mock.json(generated snapshots, kept in step by a test), tests for the
view at 40, 80 and 120 columns in both themes with every tab and modal open,
the kit's conventions and thePlayflows, and a-demo COLSxROWSmode that
the docs site turns into an animated SVG.loadfs_test.gonow gives any
example that loads../../kitthe repository root as its file system. -
A js/wasm build target, for the website's playground. The core packages
(the root package andinternal/**) now build and pass their tests for
GOOS=js GOARCH=wasm; the one blocker wassyscall.SIGWINCHin
internal/host/resize_unix.go, now retaggedunix(withresize_other.go
for everything else).cmd/tuimark-wasmis the entry that compiles to
tuimark.wasm(about 1.2 MB with brotli): a private, unversioned JS API
(open,render,step,validate,reload,close) over the same
runtime, whose frames are the CLI's dumps (tests compare them byte for byte
for every example and kit component). It is not a command, is not published,
and promises no compatibility.task wasmbuilds it,task wasm-testruns
the core tests under js with Node, andtask verifychecks its size budget
and a Node smoke test. The CLI andRunare not part of the js target.
Changed
@tuimark/hostis now@thelacanians/tuimark, intuimark new --ts
(whosepackage.jsondepends on^<the running version>),examples/host-ts,
the generatedAGENTS.md, and the guides. The package version is the Tuimark
release it runs (it was1.0.0), andsdk-ts-publish.ymland itssdk-ts-v*
tags are gone.
v0.4.1
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.4.1Docs: https://tuimark.vercel.app
A patch on 0.4a. version on <tui> becomes optional, and examples/
grows by thirteen apps: four tools, four animated apps (Snake among them),
and five more games, with animated demos of nine of them on the docs site.
Changed
versionon<tui>is optional. A<tui>without it is the
current language,version="3": it accepts the whole vocabulary, and
L008/L009and the dump'sclippedapply to it.V014is never
reported any more; its row stays in the catalog, marked retired. A
document that writesversion="1","2", or"3"keeps exactly the
meaning it had, and any other value is stillV003and read as
version="1".tuimark new, the examples, and the docs write no
version, except the SPEC fixtures (examples/inbox,examples/monitor)
and the examples that areversion="1"on purpose. The language server
completes a document withoutversionwith the current vocabulary.
Added
-
Four example apps in
examples/, each a document, a
stylesheet with dark and light palettes, sample data, amock.jsonfor
tuimark run --mock, and a Go host on the 0.4 API with its own tests:logs: a live log viewer. Lines stream in withEvery, the view
follows the end with<scroll stick="bottom">, andDerivekeeps the
filter and the counts.-follow FILEtails a real file.tasks: a kanban board with three lanes, multi-select, a form modal,
and a delete confirmation.-filekeeps the board in a JSON file.gitui: a git client with status and a diff preview, the log with
each commit's detail, and the branches.-repo PATHreads a real
repository, read-only unless-writeis also given.http: an HTTP API client with a collection, a request editor, and
the response's body and headers. It runs against an in-process fake
API unless-base URLis given.
-
examples/README.mdlists every example and what it shows. The docs
site's Examples page has screenshots of the four new apps. -
Four animated example apps, built the same way:
snake: a playable Snake whoseEverytick re-arms faster as the
level rises;-autopilotplays it.pipeline: a CI run with parallel jobs, spinners, progress bars, the
selected job's log streaming into a<scroll stick="bottom">, a flaky
test, retry, rerun, and cancel.music: a music player with no audio, with a library, a queue,
albums, and a spectrum visualizer drawn by a six-row<sparkline>.pomodoro: a Pomodoro timer and a clock in big block digits drawn
from one derived string, with phase colors fromclass:guards.
Each has a
-demo COLSxROWSflag that replays a scripted session on a
fake clock and prints its frames as JSON lines. -
Five more games, built the same way and with the same
-demoflag:chess: legal move generation checked by perft, SAN, UCI, FEN and PGN,
an alpha-beta engine with six levels that thinks in a goroutine, four
modes (against the computer, two players, computer against computer,
review), clocks, undo and redo, hints, promotion, resign and draw
claims, and a board of 64 static clickable cells.tetris: SRS rotation with wall kicks, a 7-bag, hold, a ghost piece,
lock delay, a line-clear flash, T-spins, combos, and an autopilot.minesweeper: beginner, intermediate, expert and custom boards, a safe
first reveal, flood fill, chording, flags and question marks, and a
flag mode for the mouse.2048: a 3×3 to 6×6 board, undo, win and game-over modals, highlights
for merged and new tiles, and an expectimax autopilot.sudoku: a generator whose puzzles have one solution and are graded by
the solving techniques they need, notes, hints, a check, conflicts,
undo and redo, and a clickable number pad.
-
The docs site's Examples page shows those nine as animations: the
generator renders the-demoframes as one CSS-animated SVG per theme
(only the cells that change are stored per frame), and each has a pause
button. Readers who prefer reduced motion start on the last frame.
v0.4.0
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.4.0Docs: https://tuimark.vercel.app
0.4a is the "frontend loop" release. It adds the tools that let you build
a terminal app the way you build a web front end:
- see the document live and edit it while it runs (
tuimark run); - start a project from a template (
tuimark new); - get diagnostics and completion in the editor (
tuimark lspand a VS
Code extension); - drive the view from TypeScript (
@tuimark/host); - keep timers and computed values out of the markup (
Every,Derive).
The vocabulary does not change: version="3" is still the newest, and
every document keeps its meaning and its dumps.
Added
- Go API.
RunContext(ctx, w)andRunWith(ctx, w, RunOptions{OnError})run
the app under a context. A cancellation ends the session as a quit
and restores the terminal.OnErrordecides whether an error from a handler, anEvery, or a
Deriveends the session or is shown and survived.Quit()ends the active session from any goroutine.Every(d, fn)runsfneverydon the loop's goroutine, as one
Batchwith one redraw. Late ticks are coalesced, andstopis safe
from anywhere.Derive(path, fn)keeps a computed value (a filtered list, a count)
in the store. It is recomputed inside every write, so nothing ever
sees a stale value, and it is read-only for every writer: a widget
bound to it does not change.
tuimark run FILEshows a document live in the terminal, with no
Go and no parent program, and reloads it in place when you save.- Host actions are answered by
runitself:--mock FILE.jsongives
each one fixed store writes, so a prototype can react without a
backend. F12(or--strip) shows a strip of the last events, and
--events FILE.ndjsonwrites them as the host protocol'sevent
lines. Asecretinput's value is redacted in both.- A panel lists the document's diagnostics, or why a reload failed.
- A reload keeps the store, the focus, cursors, scroll offsets, inputs,
and the active tab for every node whoseidand tag are unchanged. - Two
ctrl+cwithin one second always end the session.
- Host actions are answered by
tuimark catalogprints the whole vocabulary as JSON: tags,
attributes and their keywords, CSS properties and values,
pseudo-classes, media features, actions, key tokens, and diagnostic
codes, each with theversionthat introduced it. The format is
described byschema/catalog.v1.json.tuimark new NAME [--go | --ts]writes a project that validates
cleanly at 40, 80, and 120 columns under both themes:app.tui,
theme.tcss,sample.json, and anAGENTS.mdbriefing for coding
agents (tuimark agents --project).--goadds amain.gohost and
--tsaparent.tsbuilt on@tuimark/host. The Go module is named
after the directory, and a name Go refuses is rewritten:logor
time(a standard-library root),std,cmd, and a Windows device
name such asauxget-app, sotuimark new log --gobuilds.tuimark lspis a Language Server Protocol server for.tuiand
.tcss.- Diagnostics are exactly
validate's. They are routed to the
stylesheet they belong to, and a siblingsample.jsonis used as the
data. - Completion, hover, go to definition, and formatting with
tuimark fmt. - It reads only regular files, at most 64 MiB each (a
sample.json,
a.tuior a<style src>that is a link to/dev/zeroor
/dev/stdin, or a FIFO, counts as absent). Its diagnostics for one
document never arrive out of order, and a save or a watched file
refreshes those of a stylesheet nobody has open.
- Diagnostics are exactly
- VS Code extension (
editors/vscode): TextMate grammars that mark
anything outside the closed vocabulary, snippets, and a client for
tuimark lsp. Without the binary, highlighting and snippets still
work. The server starts only in a trusted workspace, since a workspace
can settuimark.path, and only from a binary whoseversionprints a
tuimark version. @tuimark/host(sdk/ts), the TypeScript client of the host
protocol, for Bun.spawnHoststartstuimark host, andon,set,
batch,get, andexiteddo the rest. It follows every rule for
parents: signal forwarding,SIGTERMthenSIGKILL, and never
touching the terminal. ASIGTERMorSIGHUPis forwarded after a
1.5 s grace, and only if the child is still running, so a signal sent
to the whole process group does not reach the child twice. Messages of
any size go through fd 3.examples/host-tsnow uses it.TUIMARK_LOGwrites a{"ev":"rounds","queued":N}record when the
lifecycle rounds hit their cap.
Changed
-
Faster frames for big lists. Moving the cursor in a 20 000-row
list now takes about 5.5 ms per key instead of about 120 ms (Apple M5).
A one-pathBatchover a 20 000-row store, with the frame it draws,
takes about 5.5 ms instead of 64 ms. Six things do it:- an unchanged frame is not rebuilt;
- a list's rows are reused while the data they read is unchanged;
- equal rows share one computed style;
- a frame's rows are built in memory kept from the frame before last;
- a
Batchcopies only the parts of the store it writes; - layout measures a list's height once a frame, and checks each box
as it places it instead of in one more pass.
Dumps are byte for byte the same.
task benchprints the numbers. -
Lifecycle events past the 8th round are kept, not dropped. They are
dispatched first at the next settle, whatever causes it: a key, a
resize, a tick, or aPlaystep. -
tuimark validatesays when it did not check bindings. Without
--datathe text output ends withok (bindings not checked: no --data), and the JSON output has adatamember (the path, or
null). -
tuimark hostbounds fd 3 lines. A line of 64 MiB or more ends the
session with{"type":"error","error":"fd 3 line of 64 MiB or more"},
instead of growing without limit.tuimark mcpalready stopped there.
Fixed
- A list or table whose
keyis an object or an array no longer
shares that value between the row and the paths the runtime writes it
to (bind,checked) or the events it fires: aSetthrough one of
them, or a handler changing its event, changed the row too.
v0.3.2
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.3.2Docs: https://tuimark.vercel.app
A patch: fixes and friendlier diagnostics from a full review of 0.3.1.
No vocabulary changes; documents keep their meaning and their dumps,
except that a few invalid ones now say so.
Changed
- Diagnostics suggest a fix. An unknown tag, attribute, CSS property,
or theme within two edits of a known one ends its message with; did you mean <list>?(or"width", naming the version a newer name needs).
A common HTML name gets the Tuimark way to say it instead:<div>→ use
<box>,<col>, or<row>;<span>/<p>→<text>;<checkbox>→
a<list>withchecked=;<include>→ there are no components or
includes. The codes are unchanged. tuimark play --inputexplains a token that fails right after a
text:orpaste:step:--inputsplits steps at spaces, so text with
a space needs--script.App.Dumprefuses a size above 1000 in either dimension, the limit
Playand the CLI already had, instead of trying to allocate the grid
(a huge size panicked).- The viewport key clamp of 0.3.1 applies to every document version.
It fixed anuplost afterendin the same terminal read, so it is
kept forversion="1"andversion="2"too.
Fixed
@mediaerror recovery. A selector with no{inside an@media
block no longer swallows the block's closing}, which made every rule
after the block conditional on the media query.- Selectors that can never match are
V003.#1,.9x, and.-x
name an id or class no element can have (§7's grammar), in stylesheets
and in keymapwhenalike. - A leading
+is not a cell count.gap: +3,padding: +1,
gap="+3", and@media (max-cols: +80)areV003. tuimark fmtkeeps the XML declaration (<?xml version="1.0"?>)
instead of dropping it.- Clearer messages.
V014namesversion="3"; a bare&followed
later by a;says "bare '&'" instead of naming a garbled entity; a
DOCTYPE or processing instruction after the root gets its own message;
text inside an unknown tag no longer adds aV013;box:says the
pseudo-class is empty. tuimark mcpaccepts valid JSON with numbers beyond a double
(1e400, a 400-digit literal): such a line was answered-32700(parse
error), although only a line that is not JSON gets it. Anidis echoed
as written.tuimark fmt --write --checkis a usage error instead of silently
running only the check, andtuimark versionrefuses arguments.tuimark testrejects a manifest entry whosenameis empty, not a
single path segment, or already used (ignoring case), so no entry can
write into another's goldens.
Docs and tests
- The README's language notes say that
wrap: wrapcollapses runs of
spaces and breaks at an NBSP, and that store numbers are doubles
(integers beyond ±2^53 lose precision; pass them as strings). The Go
docs ofBind,Set, andBatchand the host fd comments are
corrected. - New fuzz targets for the stylesheet parser and
fmtidempotence, a
property test of the ANSI frame diff, and tests ofinternal/play's
step grammar. CI cross-compiles and vets for Windows (amd64, arm64).
v0.3.1
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.3.1Docs: https://tuimark.vercel.app
0.3b rounds out version="3"'s layout vocabulary and adds two ways to
drive a document from outside Go: tuimark host, a wire protocol for a
parent process in any language, and tuimark mcp, a Model Context
Protocol server for coding agents. Everything is additive: version="1"
and version="2" documents keep their meaning and their dumps.
Added
- Layout and CSS (
version="3").row-gapandcolumn-gap(0-4)
splitgapinto its two axes — column siblings, grid rows/columns, and
tab/hint strips on one, stacked siblings on the other — whilegap
stays their shorthand in the cascade, so a document that sets onlygap
lays out as before.wrap: truncate-startandwrap: truncate-middle
jointruncate, keeping the end (…efgh) or both ends
(/Syste…/Data) of a cut line instead of the start.<column priority="N">hides a table column, lowest priority first, once the
visible columns no longer fit, and brings it back once they do, in
place of@mediarules for column hiding.<sparkline scale="NAME">
shares one value range across every sparkline of a frame with the same
name, so a group of gauges reads on the same scale. - Several
<keymap>elements are now allowed, in every version: their
rows form one keymap in document order.<keymap when="SEL">
(version="3") gives every row inside it thatwhen, unless a row
writes its own, which replaces it outright. <scroll id="log" stick="bottom">(version="3") keeps a growing
viewport — a log, a chat — showing its end as content arrives, and stops
sticking while you scroll up to read back, resuming once you scroll (or
move-last) back to the end.<modal focus="#id">(version="3") names the node that takes
focus when the modal becomes the top one, and the fallback when the
node focused inside it is lost (it leaves the frame or can no longer
take focus).- Tab label templates. In
version="3", a<tab>'slabeland
shortare templates, resolved every frame like a<text>body
({path}, no expressions); inversion="1"/version="2"a{path}in
either staysV003. tuimark host FILE [--data FILE.json] [--theme dark|light] [--reply-timeout 5s]runs a document in the terminal for a parent
process written in any language: the terminal stays on fd 0/1/2 exactly
as withRun(), the parent writes JSON lines to fd 3 (set,bind,
batch,get,reply) and reads JSON lines from fd 4 (readyfirst,
thenevent/ack/error,exitlast). Each host action waits for the
parent's reply up to--reply-timeout(default5s), and the fd 4
queue is bounded, so a parent that stops reading ends the session
instead of freezing the terminal. POSIX only; a usage error on Windows.
examples/host-tsis a Bun parent that drives it, forwarding
SIGTERM/SIGHUPto the child and never reading the terminal itself.tuimark mcpis a Model Context Protocol server on stdin/stdout
(JSON-RPC 2.0, one message per line, standard library only) with five
read-only tools that run the authoring loop:tuimark_validate,
tuimark_dump,tuimark_play,tuimark_inspect, andtuimark_agents.
Each returns exactly what the matching CLI command prints to stdout,
byte for byte; a result is an error only when the command itself would
exit 1 (a validation failure, exit 2, is a normal result). Register it
with{"command": "tuimark", "args": ["mcp"]}.PlayOptions.FramesmakesPlayalso return onePlayFrameper
applied step, inPlayResult.Frames— the same frames
tuimark play --framesprints.
Changed
Batch's returned error is now every failed part joined with
errors.Join, first each failedSet's own error in call order,
thenfn's own error when it differs. This is a behavior change:
errors.Isstill finds a sentinel error inside it, but comparing the
result with==against a sentinel, or a type switch on it, no longer
matches, since the returned value is always a joinederror, even for
one failure. Callers that comparedBatch's error directly need
errors.Is/errors.As, orUnwrap() []errorto walk every part.examples/monitormoves to the 0.3b vocabulary: its one<keymap>
splits into several<keymap when="…">groups, each row keeping its
place in dispatch order,<hints>, and the IRkeymaparray;#cores
gainsrow-gap: 0so its rows touch whilegap: 1still separates the
columns; and the Processes table's columns hide bypriority—c-io
andc-userfirst, thenc-thr, thenc-mem— in place of the
@mediarules that hid them before. Its goldens and
monitor_resize.ymlare updated to match, andscaleand modal
focus=are deliberately left unused there.- The
V002/V003diagnostics for a 0.3b tag, attribute, or value
(stick,focusonmodal,whenonkeymap,priority,scale,
the two newwrapvalues,row-gap/column-gap, a tab's{path}
label/short) used in aversion="1"orversion="2"document now
end in(requires version="3"), matching the existing hint for
version="2"items used inversion="1".
Fixed
- A focused viewport's keys clamp at its end.
downorendat the
end of a focused<scroll>stored an offset past it, so anupin the
same terminal read was lost and the scroll ended one row off. Its keys
now clamp to the viewport's maximum, asmove-*and the wheel already
did.
v0.3.0
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.3.0Docs: https://tuimark.vercel.app
The host API grows for real embedding and testing, and a new document
version, version="3", reports content that a view cuts without saying
so. Everything is additive: version="1" and version="2" documents
keep their meaning and their dumps.
Added
-
LoadFS(fsys, name)loads a document and its stylesheets from an
fs.FS, so a view embedded withgo:embedloads without being copied to
disk. A<style src>that climbs out of the file system is aV006
diagnostic. -
Get(path)returns a copy of a store value.@focus,@screen, and
@themereturn the app's current focus, screen, and theme. -
Batch(fn)applies severalSets as one change, all or nothing, with
one redraw. No frame, dump, or handler sees half of it. -
Play(opts, steps...)drives the app without a terminal, for tests.
It uses the step grammar oftuimark play, calls the registered
handlers, keeps the state for the next call, and returns the events and
the final dump. One engine now servestuimark play,tuimark test, and
Play, and the CLI output is unchanged. -
<tui version="3">is aversion="2"document plus two layout
warnings:L008: a text loses columns or lines without an ellipsis.L009: a node cuts a child on an axis it does not scroll.
Dumps mark the node with
clipped. A cut the author intends is silenced
by writingwrap: nowraporoverflow: hidden.tuimark irprints IR
"0.3"for these documents (schema/ir.v0.3.json), and
schema/dump.v0.2.jsongainsclipped.
Changed
examples/monitoris aversion="3"document. Its overview grid
now scrolls instead of being cut at 40–58 columns.- The
V003message for an invalidversionnow lists"1","2", or
"3".
Fixed
- Release. The release workflow now publishes the Homebrew cask. For
v0.2.0 the cask upload failed on a template function GoReleaser does not
define, and the cask was published by hand from the release's checksums. examples/monitor. Enter or space no longer confirms a kill. The
confirmation's buttons arefocusable="false": focus rests on the
modal, so onlyy,n, andescact there, and a click on either
button still works. Before, focus went to the first button, "yes (y)",
and enter or space fired it. Views that copied the example should make
the same change.
v0.2.0
brew install --cask abdul-hamid-achik/tap/tuimark
go install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@v0.2.0Docs: https://tuimark.vercel.app
The first public release. It implements version 0.2 of the language in two
parts: runtime, terminal, and tooling improvements that apply to every
document, and a new version="2" vocabulary that documents opt into.
Existing version="1" documents keep their meaning, and their dumps are
unchanged.
Added
- Installation. Release archives for macOS, Linux, and Windows (amd64
and arm64), a Homebrew cask (brew install --cask abdul-hamid-achik/tap/tuimark), andgo install github.com/abdul-hamid-achik/tuimark/cmd/tuimark@latest. version="2"documents. The new vocabulary below is available only
in<tui version="2">. In aversion="1"document each new tag,
attribute, value, property, pseudo-class, media feature, or built-in
action is a diagnostic that ends in(requires version="2").- New widgets.
<table>with<column>(header, per-row cell
templates, column widths in cells,%,fr, or auto, and virtualized
rows so only visible rows are laid out),<tabs>/<tab>(a tab strip
with short labels for narrow terminals, and focus on activation),
<hints>(a key-hint bar built from the keymap, withlabeland
keycap), and<sparkline>. - Layout and style.
layout: grid,scrollbar,bar: eighthsfor
smooth progress bars,class:NAME="path"guards, the:checkedand
:focus-withinpseudo-classes,theme="auto"(asks the terminal for its
background color, then falls back toCOLORFGBG, then to dark), and
@media (theme: dark|light). - Interaction.
eachon containers, multi-select onlistand
table(acheckedarray), built-in actions for moving, checking, and
switching tabs,whenguards evaluated over the focus chain, and mouse
input (SGR reports: click on release and the wheel). tuimark play. Drives a document with simulated keys, text, paste,
resizes, and mouse clicks without a terminal, and prints the resulting
frames as text or JSON.tuimark inspect. Explains one node, by id or by cell: its path,
classes, pseudo-classes, and each style property with the rule that set
it and the rules it overrode.- More dump and CLI options.
dump --styles,--theme dark|lighton
dump,validate,preview, andplay,preview --color, and a
superset check intuimark test --updateso a golden only gains fields. TUIMARK_LOG. Writes an NDJSON log of aRun()session (start,
keys, pastes, mouse reports, actions, and frames) and redacts input
while a secret input has focus.- Environment variables.
TUIMARK_COLOR,TUIMARK_THEME, and
TUIMARK_SYNCoverride color, theme, and synchronized-output detection. - Schemas.
schema/ir.v0.2.jsonandschema/dump.v0.2.json; the JSON
dump of aversion="2"document lists each node'sclasses. - Examples.
examples/unicode(CJK text and emoji) and
examples/monitor, a process-monitor studio built with tables, tabs,
hints, sparklines, the grid layout, and the mouse. tuimark versionnow also prints the commit and build date for
release builds.
Changed
- Unicode. Text is measured, cut, wrapped, painted, and edited by
grapheme cluster with built-in Unicode 15 width tables, so wide
characters and emoji no longer break borders or alignment. The JSON dump
marks wide cells with"wide": true. - Auto-fit scrolling. A
list,scroll, oroverflow: scroll
container without a size on its scroll axis now fits the available space
and scrolls (also inversion="1"documents), with a newL006warning;
dump nodes carry theirscrollstate. - Terminal output.
Run()probes the terminal once at start-up, picks
a color profile (truecolor, 256, 16, or none) and downsamples colors to
it, uses synchronized output when the terminal supports it, handles
bracketed paste as a single edit, and never mistakes terminal replies or
mouse reports for key presses.