01
Requires Go 1.21 or later.
$ mkdir myapp && cd myapp
$ go mod init myapp
$ go get github.com/kungfusheep/glyph@latest
The dot import is optional, but used throughout these examples.
It brings all glyph identifiers into scope. Write
VBox() instead of glyph.VBox().
A namespaced import works equally well; the dot form keeps examples shorter.
package main
import . "github.com/kungfusheep/glyph"
02
State lives in plain Go variables. Any type, any structure.
Pass a pointer and the framework reads current values on each update. Keyboard handlers are plain functions.
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)
}
NewInlineApp() renders at the cursor position without taking over the screen.
SetView() compiles the declarative tree once into a flat list of operations.
At runtime, glyph walks that list and dereferences pointers to read current state.
The frame cost is proportional to what's visible, not to what changed.
Text(&count) takes a pointer. glyph dereferences it on every frame.
Handle(key, fn) registers a global key binding.
Plain characters ("q"), special keys ("<Up>", "<Esc>", "<Enter>"),
modifiers ("<C-c>"). The callback runs on keypress and glyph re-renders.
03
Panels & Grow
VBox stacks children vertically, HBox places them side by side.
Both measure and distribute space in a single pass.
HBox(
VBox.Grow(1).Border(BorderRounded).Title("left")(
Text("panel one"),
),
VBox.Grow(2).Border(BorderRounded).Title("right")(
Text("panel two takes 2/3 width"),
),
)
.Grow(n) controls how remaining space is distributed.
Grow(2) gets twice the space of Grow(1).
Without Grow, elements shrink to fit their content.
Borders, Gap & Space
.Border(style) wraps the container in a box.
Built-in styles are BorderRounded, BorderSingle, and BorderDouble.
.Title(s) labels the top edge.
.Gap(n) adds spacing between children.
Space() expands to fill remaining space; use it to push siblings to opposite ends.
HBox.Gap(2)(
Text("ready").FG(Green),
Text("3 tasks").Bold(),
Space(),
Text("q: quit").FG(BrightBlack),
)
Custom Layout
Arrange gives you full control over layout.
Provide a function that receives child sizes and available space, return exact positions.
grid := Arrange(func(children []ChildSize, w, h int) []Rect {
cols := 3
cellW := w / cols
cellH := 3
rects := make([]Rect, len(children))
for i := range children {
rects[i] = Rect{
X: (i % cols) * cellW,
Y: (i / cols) * cellH,
W: cellW, H: cellH,
}
}
return rects
})
grid(
VBox.Border(BorderRounded)(Text("alpha")),
VBox.Border(BorderRounded)(Text("beta")),
VBox.Border(BorderRounded)(Text("gamma")),
VBox.Border(BorderRounded)(Text("delta")),
VBox.Border(BorderRounded)(Text("epsilon")),
VBox.Border(BorderRounded)(Text("zeta")),
)
04
Conditional
If takes a pointer to a bool. glyph dereferences it each frame and picks the matching branch.
online := true
If(&online).Then(
Text("● connected").FG(Green),
).Else(
Text("● offline").FG(Red),
)
.Eq() matches a specific value. Works with any comparable type.
IfOrd adds .Gt(), .Lt(), .Gte(), .Lte() for ordered types.
If(&status).Eq("active").
Then(Text("online").FG(Green)).
Else(Text("offline").FG(Red))
IfOrd(&cpu).Gt(80).
Then(Text("CRIT").FG(Red).Bold()).
Else(IfOrd(&cpu).Gt(50).
Then(Text("WARN").FG(Yellow)).
Else(Text("OK").FG(Green)),
)
Inline Values
If works anywhere a value is expected, not only as a child in a container.
Use it to drive layout properties, dimensions, and styling at render time.
// dynamic height based on state
VBox.Height(
If(&expanded).Then(30).Else(10),
)(
Text("content"),
)
// dynamic width on a progress bar
Progress(&val).Width(
If(&detailed).Then(40).Else(20),
)
Matching
Switch branches on a value with named cases. Default catches unmatched values.
Switch(&mode).
Case("edit", Text("editing")).
Case("preview", Text("previewing")).
Default(Text("idle"))
Match is first-match-wins with operators.
Cases evaluate top-to-bottom; the first true case renders.
Use Eq, Gt, Lt, Gte, Lte, Ne for comparisons and Where for predicates.
Match(&cpu,
Gt(90.0, Text("CRITICAL").FG(Red)),
Gt(70.0, Text("WARNING").FG(Yellow)),
Lte(70.0, Text("OK").FG(Green)),
)
Match(&query,
Eq("", Text("type to search")),
Where(func(q string) bool { return len(q) < 3 },
Text("keep typing..."),
),
Where(func(q string) bool { return len(q) >= 3 },
Text("searching"),
),
)
Iteration
ForEach iterates a slice. The callback receives a pointer to each item.
Conditions work per-item. Style, show, or hide based on each element's state.
ForEach(&items, func(item *Todo) any {
return HBox.Gap(2)(
Checkbox(&item.Done, ""),
If(&item.Done).Then(
Text(&item.Title).Dim(),
).Else(
Text(&item.Title),
),
)
})
05
List takes a pointer to a slice.
.OnSelect() fires when the user presses enter.
The app below lets you pick a file, then exits and prints it.
package main
import (
"fmt"
. "github.com/kungfusheep/glyph"
)
func main() {
app := NewApp()
files := []string{"main.go", "go.mod", "README.md", "config.toml", "Makefile"}
var choice string
app.SetView(
VBox.Border(BorderRounded).Title("open")(
List(&files).OnSelect(func(f *string) {
choice = *f
app.Stop()
}).BindVimNav(),
),
).Run()
fmt.Println("opening", choice)
}
.BindVimNav() adds j/k, <C-d>/<C-u> to page, g/G for start/end.
FilterList wraps the same API with a built-in fuzzy search input.
06
app.Handle(key, fn) registers global key bindings, active regardless of focus.
Plain characters, special names, and modifier combos all work.
app.Handle("q", app.Stop)
app.Handle("<C-c>", app.Stop)
app.Handle("<Esc>", app.Stop)
app.Handle("a", func() { /* add item */ })
app.Handle("d", func() { /* delete item */ })
app.Handle("<C-s>", func() { /* save */ })
List.Handle(key, fn) fires with the selected item already resolved. No index to track.
List(&tasks).Render(func(t *Task) any {
return HBox.Gap(1)(
If(&t.Done).Then(Text("[x]")).Else(Text("[ ]")),
Text(&t.Title),
)
}).Handle("<Space>", func(t *Task) {
t.Done = !t.Done
}).BindVimNav()
.BindVimNav() wires j/k, <C-d>/<C-u>, and g/G in a single call.
For custom keys, or for components like AutoTable that support page-level jumps,
use .BindNav(down, up) and .BindPageNav(down, up).
AutoTable(&procs).Scrollable(20).
BindNav("<C-n>", "<C-p>").
BindPageNav("<C-d>", "<C-u>")
07
Form auto-aligns labels, manages focus between fields, and wires validation.
Each Field pairs a label with an input component.
name := "Pete"
email := "[email protected]"
role := 0
agree := true
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")),
)
Validation
Built-in validators:
VRequired, VEmail, VMinLen, VMaxLen, VMatch, VTrue.
Trigger on VOnBlur (field loses focus) or VOnSubmit (form submitted).
Errors surface inline next to the field.
08
AutoTable takes a pointer to a slice of structs. Columns are inferred from field names.
type Service struct {
Name string
Status string
CPU string
}
services := []Service{
{"api", "running", "12%"},
{"worker", "running", "8%"},
{"cache", "stopped", "0%"},
{"db", "running", "23%"},
}
AutoTable(&services).
Sortable().
Scrollable(10).
BindVimNav()
.Sortable() adds column-click sorting.
.Scrollable(n) sets the visible row count with a scrollbar.
Column formatting is automatic; override with .Format() for custom display.
09
Colours & Attributes
Styles chain directly on elements.
Text("error").FG(Red).Bold()
Text("muted").FG(BrightBlack).Dim()
Text("success").FG(Green)
Text("warning").FG(Yellow).Bold()
.FG(color) and .BG(color) accept the standard ANSI names
(Red, Green, Yellow, Blue,
Magenta, Cyan, White, Black)
and their Bright variants.
For true colour: Hex() and RGB().
Text("branded").FG(Hex(0xc44040))
Text("custom").BG(RGB(20, 20, 40))
.Bold(), .Dim(),
.Italic(), and .Underline() set text attributes.
Any combination is valid; glyph merges them into a single ANSI sequence per cell.
Themes
.CascadeStyle() propagates a base style to all children.
Set it on a container; glyph applies it to every nested widget on each render pass.
Three built-in themes are provided (ThemeDark, ThemeLight, ThemeMonochrome),
each with Base, Muted, Accent, Error, and Border slots.
theme := ThemeDark
VBox.CascadeStyle(&theme.Base).Border(BorderRounded).BorderFG(theme.Border.FG)(
Text("normal text"),
Text("muted").Style(theme.Muted),
Text("accent").Style(theme.Accent),
Text("error!").Style(theme.Error),
)
Custom themes use the same
ThemeEx struct.
myTheme := ThemeEx{
Base: Style{FG: White},
Muted: Style{FG: BrightBlack},
Accent: Style{FG: Cyan, Attr: AttrBold},
Error: Style{FG: Red},
Border: Style{FG: BrightBlack},
}
.BorderFG(color) colours the border independently.
VBox.Border(BorderRounded).BorderFG(Cyan).Title("status")(
Text("all systems go").FG(Green),
)
Custom Rendering
Widget is the escape hatch for fully custom rendering.
Provide a measure function and a render function. glyph calls them during layout and draw.
VBox.Border(BorderRounded).Title("dashboard")(
HBox.Gap(2)(
Text("metrics").Bold(),
Space(),
Text("live").FG(Green),
),
HRule(),
HBox(
VBox.Grow(1).Border(BorderRounded).Title("chart")(
Widget(
func(availW int16) (w, h int16) {
return availW, 4
},
func(buf *Buffer, x, y, w, h int16) {
// draw bar chart directly into the buffer
},
),
),
VBox.Grow(1).Border(BorderRounded).Title("status")(
Leader("uptime", "99.9%"),
Leader("errors", "0"),
Leader("latency", "12ms"),
),
),
)
10
Animation
Any property that accepts a value also accepts
Animate.
glyph interpolates toward the target over time using easing functions.
smooth := Animate.Duration(200 * time.Millisecond).Ease(EaseOutCubic)
VBox.Height(smooth(&targetHeight))(
Text("content"),
)
// combine with conditions
Sparkline(data).Height(
smooth(If(&expanded).Then(26).Else(1)),
)
20+ easing functions built in:
EaseOutCubic, EaseInOutQuad,
EaseOutBounce, EaseInBack, and more. Define a style once, reuse it across properties.
Keep going
Concepts
covers the build/execute cycle, the layout engine, viewport scrolling,
layers for overlapping panels, focus groups, and routing between screens.
API Reference
has full signatures, doc comments, and examples for every type and method.
Press
/ anywhere on the site to search.