Declarative TUI framework for Go: describe the UI in .tuix files, write logic in Go.
See the codeTUIX 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.1.0). 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
ui.tuix file with an <App> root (see above).tuix.LoadFile (or tuix.LoadString for inline markup).app.Set, derive values with app.Computed,
attach behavior with app.On.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/demo
Keys: 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 |
The full tag reference lives in docs/tags.md.
The short version:
Column, Row (with gap, padding), Box
(border + title), Grid (columns). Sizing via width,
height (auto, fill, or cells), flex weights, and align.Text (value, word wrap, maxLines), Button
(label, onActivate), Input (bind, placeholder,
onChange/onEnter, echo for passwords, multiline),
List (source key with one row per line, rows, onSelect,
empty placeholder), Spacer.<State key="..." value="..."/> seeds the store;
{key} anywhere in text interpolates the current value and
re-renders on change. {{ and }} escape braces.<Include src="other.tuix"/> splices another file in;
Go code can register fully custom components with
tuix.RegisterComponent.fg/bg (named colors or #rrggbb) plus bold,
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 row
layout.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 nothing
Conventions: 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.
Declarative TUI framework for Go: describe the UI in .tuix files, write logic in Go.
See the codeTUIX 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.1.0). 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
ui.tuix file with an <App> root (see above).tuix.LoadFile (or tuix.LoadString for inline markup).app.Set, derive values with app.Computed,
attach behavior with app.On.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/demo
Keys: 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 |
The full tag reference lives in docs/tags.md.
The short version:
Column, Row (with gap, padding), Box
(border + title), Grid (columns). Sizing via width,
height (auto, fill, or cells), flex weights, and align.Text (value, word wrap, maxLines), Button
(label, onActivate), Input (bind, placeholder,
onChange/onEnter, echo for passwords, multiline),
List (source key with one row per line, rows, onSelect,
empty placeholder), Spacer.<State key="..." value="..."/> seeds the store;
{key} anywhere in text interpolates the current value and
re-renders on change. {{ and }} escape braces.<Include src="other.tuix"/> splices another file in;
Go code can register fully custom components with
tuix.RegisterComponent.fg/bg (named colors or #rrggbb) plus bold,
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 row
layout.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 nothing
Conventions: 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.