glyph — Go TUI Framework

3 min read Original article ↗

package main import . "github.com/kungfusheep/glyph" func main() { count := 0 app := NewInlineApp() app.SetView( VBox( Text(&count), Text("↑/↓ to count, enter to quit"), ), ). Handle("<Up>", func() { count++ }). Handle("<Down>", func() { count-- }). Handle("<Enter>", app.Stop). ClearOnExit(true). Run() println("You selected", count) }

A glyph app is a tree of plain functions. VBox stacks its children, Text renders a value. Pass a pointer; glyph reads the current value every update. Keyboard handlers take plain function callbacks. Run starts the event loop and blocks until the app exits.

Follow the full guide →

Once

Build

When you call SetView(), the declarative tree is compiled into a flat array of operations. All reflection, type switches, and allocation happen here. The composable API surface, VBox.Gap(2).Border(...), is purely a build-time convenience.

Each update

Execute

Rendering walks the compiled ops and dereferences pointers to read current state. Pointer reads into a cell buffer, then a diff-based flush to the terminal. The execute path is zero-alloc by design.

Components are themable and stylable, but they work out of the box with zero configuration.

File browser

Split pane with list navigation and live preview. OnSelect loads the file, TextView renders it.

HBox( VBox.Grow(1).Border(BorderRounded)( List(&files).BindVimNav().OnSelect(func(f *string) { data, _ := os.ReadFile(*f) preview = string(data) }), ), VBox.Grow(2).Border(BorderRounded)( TextView(&preview).Grow(1), ), )

Process monitor

Progress bars for system metrics, sortable table for processes. AutoTable infers columns from struct fields.

VBox.Gap(1)( HBox.Gap(4)( Text("CPU"), Progress(&cpuPct).Width(30), Text("Mem"), Progress(&memPct).Width(30), ), AutoTable(&procs).Sortable().Scrollable(20).BindVimNav(), )

Deploy log

Spinner, status text, and progress in a single row. Log streams output live. Result appears conditionally when done.

VBox.Border(BorderRounded).Title("deploy")( HBox.Gap(2)( Spinner(&frame).FG(Cyan), Text(&status).Bold(), Progress(&pct).Width(20), ), Log(output).Grow(1).MaxLines(500), If(&done).Then(Text(&result).Bold().FG(Green)), )

Fuzzy finder

FilterList handles the search input and matching. Custom Render for each row, border and title for framing.

FilterList(&packages, func(p *Pkg) string { return p.Name }). Placeholder("search packages..."). Render(func(p *Pkg) any { return HBox.Gap(2)( Text(&p.Name).Bold(), Text(&p.Desc).FG(BrightBlack), ) }).MaxVisible(15).Border(BorderRounded).Title("packages")

Live dashboard

Two sparkline panels side by side with a scrolling event log below. Update the slices; glyph reads the new values on the next update.

VBox( HBox.Gap(1)( VBox.Grow(1).Border(BorderRounded).Title("requests/s")( Sparkline(&reqData).FG(Green), Text(&reqRate).FG(BrightBlack), ), VBox.Grow(1).Border(BorderRounded).Title("p99 latency")( Sparkline(&latData).FG(Yellow), Text(&p99).FG(BrightBlack), ), ), Log(events).Grow(1).MaxLines(200), )

Registration form

Form auto-aligns labels, manages focus, and wires validation. Errors surface on blur or submit, as configured.

Form.LabelBold().OnSubmit(register)( Field("Name", Input(&name).Validate(VRequired, VOnBlur)), Field("Email", Input(&email).Validate(VEmail, VOnBlur)), Field("Role", Radio(&role, "Admin", "User", "Guest")), Field("Terms", Checkbox(&agree, "I accept").Validate(VTrue, VOnSubmit)), )

The values guiding glyph.

Performance is a feature.

Views are a story, not an investigation.

Time-to-understand is a design goal.

The common case should already be solved.

Make the right thing the obvious thing.

Complexity belongs to the framework, not the builder.

$ go get github.com/kungfusheep/glyph@latest copied