Files
terminal/tui
..
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00
2026-07-12 18:41:05 -04:00

tui

Immediate-mode widget toolkit on top of the terminal cell buffer. No retained widget tree, no framework loop: the application owns a []terminal.Cell buffer and all state; tui provides regions, layout math, render functions, and plain-struct state helpers. Every frame is a full logical redraw — the terminal diff layer keeps actual output minimal.

Core concept: Region

A Region is a bounds-checked rectangular view over a cell slice. All drawing goes through regions; coordinates are region-relative. Sub-regions nest and clip to parent bounds, so widgets cannot draw outside their allotted area.

w, h := term.Size()
cells := make([]terminal.Cell, w*h)
root := tui.NewRegion(cells, w, 0, 0, w, h)

panel := root.Sub(2, 1, 40, 10) // clipped view
inner := panel.Inset(1)         // shrink 1 cell on all sides

Out-of-bounds writes are silently dropped — no bounds management needed in widget code.

Quick start

term := terminal.New()
term.Init()
defer term.Fini()

w, h := term.Size()
cells := make([]terminal.Cell, w*h)

list := tui.NewScrollState(len(items), h-2)
list.Selection = 0

for {
    root := tui.NewRegion(cells, w, 0, 0, w, h)
    root.Fill(terminal.Gunmetal)

    root.Box(tui.LineRounded, terminal.SteelBlue)
    content := root.Inset(1)
    content.List(buildItems(items), list.Selection, list.Offset, tui.ListOpts{
        CursorBg: terminal.DarkSlate,
    })
    content.ScrollBar(content.W-1, list.Offset, list.Visible, list.Total,
        terminal.IronGray)

    term.Flush(cells, w, h)

    ev := term.PollEvent()
    switch ev.Type {
    case terminal.EventKey:
        switch ev.Key {
        case terminal.KeyUp:
            list.SelectPrev()
        case terminal.KeyDown:
            list.SelectNext()
        case terminal.KeyEscape:
            return
        }
    case terminal.EventResize:
        w, h = ev.Width, ev.Height
        cells = make([]terminal.Cell, w*h)
        list.SetVisible(h - 2)
    }
}

Layout

cols := tui.SplitH(root, 0.3, 0.7)          // ratio split, normalized
rows := tui.SplitV(cols[1], 0.5, 0.5)
side, main := tui.SplitHFixed(root, 24)     // fixed left width
top, rest := tui.SplitVFixed(root, 3)       // fixed top height
dlg := tui.Center(root, 50, 12)             // centered sub-region

The last ratio segment absorbs rounding remainder — no gaps.

Text and style

r.Text(x, y, "label", fg, bg, terminal.AttrNone)
r.TextCenter(y, "title", fg, bg, terminal.AttrBold)
r.TextRight(y, "hint", fg, bg, terminal.AttrDim)
lines := r.TextBlock(x, y, longText, fg, bg, attr) // word-wrapped, returns line count
r.TextStyled(x, y, s, tui.Style{Fg: fg, Bg: bg, Attr: attr})

String utilities operate on rune counts: RuneLen, Truncate / TruncateLeft / TruncateMiddle (ellipsis variants), PadLeft / PadRight / PadCenter, WrapText.

Style{Fg, Bg, Attr} bundles cell appearance; most widget option structs accept it.

Widgets

Widgets are stateless render functions (mostly Region methods). Application state lives in plain structs passed by pointer. Available renderers:

boxes and lines (Box, BoxFilled, HLine, VLine — single, double, rounded, heavy line types), List, Table, Tree, TabBar, KeyValue / KeyValueWrap, Progress / ProgressV / Gauge / Spinner, ProgressOverlay, Sparkline / SparklineV, Input / TextField, Editor, Modal / Overlay / ConfirmDialog, ScrollBar / ScrollIndicator, masonry layout.

Representative patterns below; remaining widgets follow the same opts-struct + state-struct shape — read the source for full options.

Scrollable list with scrollbar

items := make([]tui.ListItem, 0, len(files))
for _, f := range files {
    items = append(items, tui.ListItem{
        Icon: '▸', IconFg: terminal.Amber,
        Text: f.Name, TextStyle: tui.Style{Fg: terminal.LightGray},
    })
}
r.List(items, state.Selection, state.Offset, tui.ListOpts{CursorBg: terminal.DarkSlate})
r.ScrollBar(r.W-1, state.Offset, state.Visible, state.Total, terminal.IronGray)

Modal dialog

dlg := tui.Center(root, 50, 12)
content := dlg.Modal(tui.ModalOpts{
    Title:    "Settings",
    Border:   tui.LineDouble,
    BorderFg: terminal.SteelBlue,
    TitleFg:  terminal.White,
    Bg:       terminal.DarkSlate,
})
content.TextBlock(0, 0, body, fg, terminal.DarkSlate, terminal.AttrNone)

Modal fills, borders, titles, and returns the content region. Overlay adds fullscreen/floating/shadow variants; ConfirmDialog adds yes/no buttons with focus state.

Progress overlay

prog := tui.NewProgressState(tui.DefaultProgressOpts("Indexing", "Scanning...",
    tui.ProgressDeterminate))

// per frame:
prog.Tick()
prog.SetProgress(done / total)
if prog.Visible {
    root.ProgressOverlay(prog.Opts)
}

Five progress types (spinner, determinate, indeterminate, pulse, dots), eight spinner styles, eight bar styles, seven frame styles — combinable via opts.

Multi-line editor

ed := tui.NewEditorState(initialText)

// input:
if ev.Type == terminal.EventKey {
    ed.HandleKey(ev.Key, ev.Rune, ev.Modifiers) // full emacs-style bindings built in
}

// render:
r.Editor(ed, tui.EditorOpts{LineNumbers: true, Border: tui.LineSingle, Focused: true})
text := ed.Value()

TextFieldState + TextField provide the single-line equivalent (placeholder, prefix, password mask, max length).

State helpers

Pure logic, no rendering — usable independently:

  • ScrollState — item-index scrolling with selection (SelectNext/Prev, EnsureVisible, PageUp/Down, AtTop/AtBottom)
  • ViewportScroll — row-based content scrolling with viewport clipping (ClipToViewport maps content rows to visible rows)
  • TreeState + TreeExpansion + TreeBuilder — cursor/scroll, expand/collapse keyed state, hierarchical → flat visible-node list
  • EditorState, TextFieldState — text content, cursor, scroll, key handling
  • MasonryState — multi-column layout calculation over a viewport
  • Free functions: AdjustScroll, ClampScroll, ClampCursor, ScrollPercent, PageDelta

Notes

  • Width calculations count runes, not terminal columns; East Asian wide characters and combining marks are not width-aware.
  • Zero-value color.RGB in style fields generally means "inherit" (widget default or row background) — check specific widget docs.
  • Mouse hit testing: TabBar returns []TabBounds; other widgets require application-side geometry from the regions used.