TUIX is a small Go library for building terminal user interfaces
declaratively. You describe the UI in .tuix files (plain XML),
write the application logic in Go, and the library handles layout,
state updates, and input. Rendering and keyboard/mouse handling are
built on tcell.
<App title="Demo">
<State key="name" value="ada"/>
<Column gap="1" padding="1">
<Text value="Hello {name}!" fg="green" bold="true"/>
<Input id="name-in" bind="name" placeholder="type your name"/>
<Button id="ok" label="OK" onActivate="onOk"/>
</Column>
</App>app, err := tuix.LoadFile("ui.tuix")
if err != nil {
log.Fatal(err)
}
app.On("onOk", func(ev event.Event) {
fmt.Println("hello,", app.Get("name"))
})
if err := app.Run(); err != nil {
log.Fatal(err)
}Status: early development (
v0.2.1). It builds, it runs, and it is tested but the API has only been used by its own examples so far and may still change beforev1. Tested on Windows; Linux and macOS should work through tcell but have not been tried yet.
Go 1.24 or newer. One dependency:
go get github.com/Vinetwigs/tuix- Write a
ui.tuixfile with an<App>root (see above). - Load it with
tuix.LoadFile(ortuix.LoadStringfor inline markup). - Seed state with
app.Set, derive values withapp.Computed, attach behavior withapp.On. - Call
app.Run()to start the terminal loop.
Run the bundled demo to see it in action. It needs a real terminal; an IDE output pane will not do, tcell takes over the screen:
go run ./examples/demoKeys: Tab / Shift+Tab move focus, arrows navigate, Enter
confirms, typing edits the focused field, Esc or Ctrl+C quits.
Inputs support mid-text cursor movement (Left/Right,
Home/End, Delete, click to position). The left mouse button
presses buttons and focuses inputs and list rows.
| Directory | What it shows | Run |
|---|---|---|
examples/demo |
Form with bindings, includes, quit button | go run ./examples/demo |
examples/showcase |
Dashboard: stats grid, login form, clickable keypad | go run ./examples/showcase |
examples/todo |
Persistent todo list, autosaved to tasks.json |
go run ./examples/todo |
examples/studio |
.tuix playground: edit snippets, live validation with line numbers, AST outline, password-gated publishing |
go run ./examples/studio |
examples/dash |
Live CI/CD dashboard: progress bars, sparkline and bar chart, run table, threshold slider, toggles | go run ./examples/dash |
The full tag reference lives in docs/tags.md.
The short version:
- Containers:
Column,Row(withgap,padding),Box(border +title),Grid(columns). Sizing viawidth,height(auto,fill, or cells),flexweights, andalign. - Widgets:
Text(value, wordwrap,maxLines),Button(label,onActivate),Input(bind,placeholder,onChange/onEnter,echofor passwords,multiline),List(sourcekey with one row per line,rows,onSelect,emptyplaceholder),Spacer. - Visual elements:
Divider(rule with optionallabel),ProgressBar(bind,max),Checkbox(bind,label),Slider(bind,min/max/step),SparklineandBarChart(numeric series charts),Table(columns,source). - State:
<State key="..." value="..."/>seeds the store;{key}anywhere in text interpolates the current value and re-renders on change.{{and}}escape braces. - Reuse:
<Include src="other.tuix"/>splices another file in; Go code can register fully custom components withtuix.RegisterComponent. - Style:
fg/bg(named colors or#rrggbb) plusbold,dim,italic,underline,blink,reverse.
Markup errors point at the offending line (line N), both for bad
XML and for unknown component types.
// Loading
app, err := tuix.LoadFile("ui.tuix")
app, err := tuix.LoadString(`<App>...</App>`)
// Reactive state
app.Set("name", "grace")
v := app.Get("name") // computed keys included
app.Computed("shout", func(get func(string) string) string {
return strings.ToUpper(get("name"))
})
// Events: named handlers from markup, "*" observes everything
app.On("onOk", func(ev event.Event) { ... })
app.On("*", func(ev event.Event) { ... })
app.Emit(event.Event{Type: event.Custom, Name: "tick"})
// Focus, mouse, lifecycle
app.Focused()
app.FocusNext()
app.FocusPrev()
app.SetFocus(0)
app.ClickAt(x, y, width, height) // headless/testing clicks
app.Notify() // wake Run loop after background Set calls
app.Run() // blocking tcell loop (keyboard + mouse)
app.Quit()event.Event carries Type (KeyPress, Activate, InputChange,
FocusChange, Select, Custom, Quit), the handler Name,
TargetID, an optional Value, and raw Key/Rune/Mod for
keyboard input.
rows := app.RenderCells(80, 24) // one string per rowlayout.Compute assigns every component a cell rectangle. Columns
stack, rows place side by side, boxes reserve a border plus padding,
grids split into equal cells. gap separates siblings, fill and
flex share leftover space, and containers size to their content
where the content is known (Text that wraps needs an explicit
height). Anything that does not fit is clipped, never wrapped by
the layout itself.
tuix.go, app.go public API
ast/ syntax tree, no dependencies
parser/ XML to AST, <Include> resolution
component/ component tree + custom registry
runtime/ reactive store (bindings, computed values)
layout/ rectangles for row/column/box/grid
style/ colors and attributes to tcell.Style
event/ event types + dispatcher
backend/ thin tcell wrapper (drawing, mouse, wakeup)
docs/tags.md complete tag reference (updated with every change)
examples/ demo, showcase, todo, studio
Markup, runtime, and rendering are kept separate on purpose: the parser never imports tcell, layout only sees the component tree, and the backend only draws rectangles and text.
go build ./...
go vet ./...
go test ./... # or: go test -count=1 ./...
gofmt -l . # must print nothingConventions: exported identifiers get doc comments, every markup or
behavior change updates docs/tags.md and adds a test.
GPL-3.0-or-later; see LICENSE.