Files

207 lines
6.4 KiB
Markdown

# 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.
```go
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
```go
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
```go
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
```go
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
```go
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
```go
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
```go
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
```go
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.