tui

package module
v0.44.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 10, 2026 License: BSD-3-Clause Imports: 16 Imported by: 0

README

go-widgets/tui

CI pkg.go.dev coverage go license

Terminal I/O layer around go-widgets/painter's CellPainter. Renders a widget tree as a self-contained ANSI stream sized to the current terminal, so the same widget code that produces pixels for a WUI or GUI back-end also produces text for a terminal.

What it is (and what it isn't)

The package is deliberately minimal — no raw mode, no keyboard event loop, no alt-screen management. It ships a snapshot model: render one frame to a writer and return. Compose it with a caller-managed event loop or a CLI that prints once and exits.

import (
    "os"

    "github.com/go-widgets/painter"
    "github.com/go-widgets/tui"
)

widgets := []painter.Widget{
    &painter.Label{Bounds: painter.Rect{X: 2, Y: 1, W: 30, H: 1}, Text: "GO WIDGETS TUI"},
    &painter.Button{Bounds: painter.Rect{X: 2, Y: 3, W: 12, H: 3}, Label: "OK"},
    &painter.ProgressBar{Bounds: painter.Rect{X: 2, Y: 8, W: 30, H: 3}, Value: 0.72},
}
_ = tui.RenderOnce(os.Stdout, widgets, nil) // nil theme = LightTheme

Interactive tui.App runner (v0.3.x)

For a full interactive experience — raw mode + alt-screen + input loop + resize + cleanup — instantiate a tui.App:

import (
    "os"
    "github.com/go-widgets/toolkit"
    "github.com/go-widgets/tui"
)

func main() {
    app := tui.NewApp()
    app.Root = buildScene()          // toolkit.Widget hierarchy
    app.Keys["q"] = func(a *tui.App) { a.Quit() }
    app.Keys["Ctrl+C"] = func(a *tui.App) { a.Quit() }
    os.Exit(app.Run())
}

App.Run() enters alt-screen, sets raw mode, spawns a stdin reader, dispatches events to Root.OnEvent (with global handlers running first), reacts to SIGWINCH for resize, and always restores the terminal on exit — even on panic, via a deferred TTY.Leave inside a defer/recover chain.

Keys handlers may call a.Consume() to prevent the current event from also reaching Root — needed for mode-switching editors (pressing i to enter edit mode without also inserting i into the buffer).

Modal input capture — set app.InputTarget to a widget and every event a Keys handler does not Consume goes to it instead of Root. This is the focus primitive a command palette or search box needs: swallow typing while the overlay is open, then clear it (set nil) to hand input back. Root still draws every frame; only routing changes.

// Open a search box: capture typing, filter live.
app.Keys["/"] = func(a *tui.App) { a.InputTarget = searchBox; a.Consume() }
app.Keys["Escape"] = func(a *tui.App) { a.InputTarget = nil; a.Consume() }

Both cmd/ demos use it — the editor's command palette and the explorer's incremental file finder route keystrokes into a tui.Entry this way.

Form focus — for a multi-field screen, wire the inputs into a tui.FocusRing. It gives them a shared keyboard focus: Tab advances, Shift+Tab retreats (both wrapping), a click focuses the field it hits, and every other event is forwarded to the focused field. Arrow keys are not consumed, so a focused Dropdown/Scale keeps its own behaviour. Any Focusable (Entry, Button, CheckButton, RadioButton) renders a focus cue and activates on Enter, so the form is fully keyboard-drivable.

form := tui.NewFocusRing(nameEntry, wrapCheck, okButton) // Tab walks these
app.Root = form

Two reference demos ship in cmd/:

  • cmd/tui-explorer — k9s-style file browser (file list + preview pane + arrow-key navigation + a live / incremental finder + File/View menus with a collapsible sidebar).
  • cmd/tui-editor — loom-style modal text editor (View / Edit / Palette modes + a typeable Ctrl+P command palette running save / quit / new / e <path> / find <text> + Ctrl+S save).

Both are exercised by real pty-based e2e tests (//go:build unix && integration) that spawn the binary under a real terminal, send real key bytes, and assert on the rendered frame. These tests catch layout + event-loop bugs that seam-based unit tests miss — see the v0.3.2 / v0.3.3 / v0.3.4 release notes for the concrete regressions this protocol caught.

Cell-mode widget guidance

Most toolkit widgets are designed for pixel rendering — their internal pad constants (AlertPadY = 8, MenuBarH = 22, …) are pixels in PixelPainter mode. In tui.RenderToolkit / tui.App cell mode, those same integers count CELLS, so widgets that lean on large pad constants render at cell-inappropriate sizes.

So for the widgets whose pixel geometry doesn't survive the pixels→cells reinterpretation, tui ships its own cell-native versions (see the catalog below). A handful of simple toolkit widgets already render at ~1 cell per glyph and are safe to use directly in tui.App: Label, TextView. Others (Alert, Card, Stat, HeaderBar, Toast, Banner) are usable but visually inflated, and the pixel-only structural widgets (toolkit.MenuBar, Notebook, HPaned, …) render poorly — use the tui equivalents instead.

Cell-native widget set

Every widget below is a toolkit.Widget that renders through painter.Painter, so the same instance drives a terminal cell grid (TUI) and an RGBA pixel buffer (WUI/GUI) with no code change. All are 100%-covered and tuned so one glyph occupies one cell — no pixel padding leaking into the layout.

Widget What it is
TextEditor read-write code editor: syntax highlight, gutter, undo/redo, search, selection (see below)
Entry single-line text input with placeholder, horizontal scroll, rune-indexed caret
Button clickable action; Default / Prominent / Secondary styles; hover + press states
CheckButton [✓] / [ ] boolean toggle with a label
RadioButton / RadioGroup (•) / ( ) toggle; group for mutual exclusion
Scale draggable slider over Min..Max; click / drag / arrow-key stepping
ProgressBar continuous Fraction fill with an optional centred label
LevelBar discrete Value/Max segments (battery / signal / steps)
ListBox scrollable single-select list with OnSelect
TreeView collapsible hierarchy (chevrons, keyboard nav, scroll) for file trees / outlines
Table data grid: header, auto/fixed columns, zebra rows, selection, scroll
MenuBar top menu strip; ItemXRange for anchoring dropdowns
MenuDropdown anchored, self-sizing dropdown; per-row actions
Toolbar action strip of labelled buttons + separators (below a MenuBar)
Popover bordered modal overlay (title + body), hidden unless Visible
Statusbar footer strip of segments; last segment fills; lazy SetSegment
Notebook tabbed container; label-sized tabs; routes events to the active page
VBox header / body / footer layout with inset, hit-tested overlays + drag-capture
HSplit resizable horizontal split with a draggable grip column
VSplit resizable vertical split (stacked panes) with a draggable grip row
Dialog modal confirm/prompt box with focusable action buttons (Tab/←→, Enter/Esc)
Dropdown value picker (combobox) — collapsed control expands a selectable list
Spinner animated busy indicator (tick-driven), optional label
FocusRing shared keyboard focus over inputs — Tab/Shift+Tab traversal for forms

The cmd/tui-explorer + cmd/tui-editor demos are built from these widgets.

tui.TextEditor

A cell-native, read-write code editor widget with syntax highlighting, a line-number gutter, undo/redo, search, and selection — the buffer behind cmd/tui-editor (read-write) and cmd/tui-explorer's preview (ReadOnly). Being a painter.Painter consumer, the same widget renders to a terminal cell grid (TUI) and an RGBA pixel buffer (WUI/GUI) with no code change.

ed := tui.NewTextEditor()   // gutter on, one empty line
ed.Filename = "main.go"     // extension picks the syntax language
ed.SetText(src)
ed.Focused = true
app.Root = ed               // it is a toolkit.Widget

Fields: Filename (drives the highlighter's language; "" = plain), ReadOnly (viewer mode — navigation still works, edits are ignored), ShowGutter (line numbers), Focused (draws the caret). The viewport scrolls to follow the caret automatically.

Keys (delivered through OnEvent):

Key Action
printable · Enter · Backspace · Delete insert · split line · delete back / forward
move the caret
Home · End line start · line end
PageUp · PageDown move one viewport
Tab · Shift+Tab indent · dedent (caret line or whole selection)
Alt+↑ · Alt+↓ move the current line up · down
Ctrl+Z · Ctrl+Y undo · redo
Ctrl+X · Ctrl+V cut · paste
mouse click · drag position caret · extend selection

Methods: SetText / Text, Find / FindNext, Replace / ReplaceAll, Copy / Cut / Paste, SelectedText / DeleteSelection.

Highlighting lives in the tui/syntax sub-package and covers Go (via the standard library's go/scanner), JavaScript/TypeScript, Python, Ruby, shell, C/C++, Rust, JSON, YAML, HCL, TOML, LaTeX and Markdown — with no external dependency. tui.SyntaxInk maps a token kind to a theme-aware colour.

Sizing

Two variants, take your pick:

  • RenderOnceSized(w, cols, rows, widgets, theme) — explicit dimensions. The reliable form for tests, size-aware callers, and headless renderers.
  • RenderOnce(w, widgets, theme) — queries EnvSize() (COLUMNS / LINES environment variables) and falls back to DefaultCols x DefaultRows (80 x 24) when the environment does not report a size.

The env-vars-only strategy is a deliberate trade-off: it keeps the package stdlib-only — no TIOCGWINSZ ioctl per platform, no cgo, no dependency on golang.org/x/term. Interactive shells that need dynamic sizes should export COLUMNS / LINES from their prompt hook, or pass an explicit size to RenderOnceSized.

Try it

# Default terminal size (or COLUMNS / LINES if set)
go run ./cmd/tui-snapshot

# Force a specific size + dark theme
go run ./cmd/tui-snapshot --cols=100 --rows=25 --theme=dark

The tui-snapshot demo mirrors painter/cmd/tui-demo: same three-widget layout (label, two buttons, progress bar), same theme selection.

For a broad showcase of the cell-native tui.* widget set, run the tui-widgets gallery — one labelled slot per widget, every glyph exactly one cell (this is what realistic cell rendering looks like; the pixel toolkit widgets are for the WUI/GUI backends, not a character grid):

# Gallery of all cell-native tui widgets (Button, Table, TreeView,
# Notebook, Scale, Entry, Popover, VBox, HSplit, …)
go run ./cmd/tui-widgets | less -R

# One widget at a larger scale, or list the names
go run ./cmd/tui-widgets --widget=table --cols=50 --rows=8
go run ./cmd/tui-widgets --list

For an interactive tour of the set, run the widget explorer: a live tui.App with the widget list on the left and a poke-able instance on the right (↑↓ select · Tab focus the widget · Esc back · mouse to click/ drag · q quit) — type into the Entry, drag the Scale, open the Dropdown:

go run ./cmd/tui-widget-explorer

For the interactive reference demos (tui.App powered):

# k9s-style file browser with arrow navigation
go run ./cmd/tui-explorer

# vi-style modal editor
go run ./cmd/tui-editor --file=path/to/file.txt

To run the pty-based end-to-end integration tests:

go test -tags integration ./cmd/tui-explorer/... ./cmd/tui-editor/...

These verify the rendered frame after real key input in a real pty, catching layout + interaction bugs that unit tests miss.

Design axiom

Same as the rest of go-widgets: dependency-free, stdlib-only, 100% coverage (library packages and cmd/). CI enforces the coverage gate on every push.

License

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package tui is the terminal-I/O layer around github.com/go-widgets/painter's CellPainter. It renders a widget tree into an ANSI cell stream sized to the current terminal, so the same widget code that produces pixels for a WUI or GUI back-end also produces text for a terminal — no widget changes required.

The package is deliberately minimal: no raw mode, no keyboard event loop, no alt-screen management. It ships a "snapshot" model — render one frame to a writer and return — that composes cleanly with either a caller-managed event loop or a CLI that prints once and exits. Higher-level facilities (an App runner with input, resize, and cleanup handling) live in a follow-up cycle.

Sizing follows the caller's preference:

  • RenderOnceSized takes explicit (cols, rows) — the reliable form used by tests, size-aware callers, and headless renderers.
  • RenderOnce queries the size from environment variables (EnvSize) and falls back to [DefaultCols]x[DefaultRows] when the environment does not report a size. This keeps the package stdlib-only — no ioctl, no cgo, no dependency on golang.org/x/term.

Both variants write a self-contained ANSI stream to the caller's writer via painter.CellPainter.WriteANSI; the caller is responsible for the surrounding terminal state (raw mode, alt screen, cursor visibility) if any.

Index

Constants

View Source
const (

	// HSplitMinFrac / HSplitMaxFrac bound LeftFrac so neither pane vanishes.
	HSplitMinFrac = 10
	HSplitMaxFrac = 90
)
View Source
const (
	DefaultCols = 80
	DefaultRows = 24
)

DefaultCols and DefaultRows are the terminal dimensions used when neither the environment nor the caller supplies a size — the classic VT100 defaults every terminal emulator honours as a floor.

View Source
const (

	// VSplitMinFrac / VSplitMaxFrac bound TopFrac so neither pane vanishes.
	VSplitMinFrac = 10
	VSplitMaxFrac = 90
)
View Source
const EventTick toolkit.EventKind = 100

EventTick is the toolkit.EventKind the App emits at a caller-configured tick rate. Widgets that animate (e.g. Spinner) match on this kind to advance a frame counter and repaint. The value sits above the toolkit's own iota range so it can never collide with a widget-produced Click / KeyDown / Char event. Defined here (not in the unix-only app.go) so cross-platform widgets can reference it on every backend, including js/wasm.

View Source
const StatusbarSegmentMinW = 10

StatusbarSegmentMinW is the default minimum width of a non-last segment.

Variables

This section is empty.

Functions

func EnvSize

func EnvSize() (cols, rows int, ok bool)

EnvSize reads the terminal size from the COLUMNS and LINES environment variables. It returns (cols, rows, true) when both parse cleanly and hold positive integers; otherwise it returns (0, 0, false) and the caller should substitute defaults.

The env-vars-only strategy is a deliberate trade-off: it keeps the package stdlib-only (no TIOCGWINSZ ioctl per platform, no cgo, no golang.org/x/term dependency). Interactive shells that need dynamic sizes should export COLUMNS / LINES from their prompt hook, or pass an explicit size to RenderOnceSized.

func GutterWidth added in v0.8.0

func GutterWidth(lineCount int) int

GutterWidth returns the cell width of a line-number gutter for a code pane of lineCount lines: the number of digits in the largest line number (a minimum of 2) plus one cell of separation before the code. Shared by tui-explorer + tui-editor so their gutters line up.

func LineNumberInk added in v0.8.0

func LineNumberInk(theme *toolkit.Theme) toolkit.RGBA

LineNumberInk is the muted ink for a line-number gutter: the theme's Border colour, which reads as a dim separator tone against any pane background.

func RenderOnce

func RenderOnce(w io.Writer, widgets []painter.Widget, theme *painter.Theme) error

RenderOnce paints the widget tree into a fresh CellPainter sized to SizeOrDefault, fills the background with theme.Background, calls each widget's Draw, and writes the resulting ANSI stream to w. It is the snapshot form: no raw mode, no event loop; one frame then return.

The theme argument may be nil, in which case painter.LightTheme is used. This keeps CLI callers ergonomic (`tui.RenderOnce(os.Stdout, widgets, nil)`) without forcing them to import painter just to name the default theme.

func RenderOnceSized

func RenderOnceSized(w io.Writer, cols, rows int, widgets []painter.Widget, theme *painter.Theme) error

RenderOnceSized is like RenderOnce but uses the explicit (cols, rows) dimensions instead of querying the environment. Cols and rows must both be positive; a non-positive value falls back to the corresponding default so callers cannot accidentally produce an empty render.

func RenderToolkit added in v0.2.0

func RenderToolkit(w io.Writer, widgets []toolkit.Widget, theme *toolkit.Theme) error

RenderToolkit paints a toolkit.Widget tree into a fresh CellPainter sized to SizeOrDefault, fills the background with theme.Background, calls each widget's Draw, and writes the resulting ANSI stream to w.

It is the toolkit-widget counterpart of RenderOnce (which renders painter.Widget). The two exist side-by-side because toolkit.Theme carries fields (SurfaceAlt, OnBackground, Extra) that painter.Theme does not, so a toolkit widget cannot be rendered through RenderOnce without losing theme surface.

The theme argument may be nil, in which case toolkit.DefaultLight is used.

func RenderToolkitSized added in v0.2.0

func RenderToolkitSized(w io.Writer, cols, rows int, widgets []toolkit.Widget, theme *toolkit.Theme) error

RenderToolkitSized is like RenderToolkit but uses the explicit (cols, rows) dimensions instead of querying the environment. Cols and rows must both be positive; a non-positive value falls back to the corresponding default so callers cannot accidentally produce an empty render.

func SizeOrDefault

func SizeOrDefault() (cols, rows int)

SizeOrDefault returns EnvSize when the environment reports a size, otherwise [DefaultCols]x[DefaultRows]. This is what RenderOnce consumes.

func SyntaxInk added in v0.5.0

func SyntaxInk(k syntax.Kind, theme *toolkit.Theme) toolkit.RGBA

SyntaxInk maps a syntax.Kind to a terminal ink for the given theme, choosing a dark- or light-appropriate One Dark / One Light-style palette from the theme's background luminance. Plain + Punct fall back to the theme foreground. Shared by tui-explorer + tui-editor so their code panes colour identically.

Types

type App added in v0.3.0

type App struct {
	// Root is the widget tree the App draws and dispatches events
	// to. Callers wire the composition once and mutate its fields
	// during OnKey callbacks; the next repaint reflects the state.
	Root toolkit.Widget

	// Theme cascades through the widget tree on every draw. Defaults
	// to [toolkit.DefaultLight] if left nil.
	Theme *toolkit.Theme

	// Keys is a map from a key Code (matching InputParser output —
	// "q", "Ctrl+C", "Up", "Enter", …) to a handler. Handlers can
	// mutate App state (e.g. call [App.Quit]) or the widget tree.
	// Global handlers run BEFORE the event reaches Root.OnEvent — the
	// App always dispatches to Root afterwards.
	Keys map[string]func(*App)

	// InputTarget, when non-nil, receives every event that a Keys handler
	// does not Consume — INSTEAD of Root. It is a modal input capture: a
	// command palette or search box sets it to swallow typing while open,
	// then clears it (sets nil) to hand input back to Root. Root is still
	// drawn every frame; only event routing changes.
	InputTarget toolkit.Widget

	// TickHz sets the auto-tick frequency in Hz. 0 disables ticks. A
	// widget subscribing to EventTick needs TickHz > 0 to animate.
	TickHz int
	// contains filtered or unexported fields
}

App is an interactive TUI runner. Instantiate one, set App.Root to your widget tree, optionally register keybindings + a tick rate, and call App.Run. The App handles alt-screen + raw mode, parses stdin into toolkit.Event values, dispatches them to the widget tree, repaints on demand, and cleans up on exit — including on panic, via a deferred TTY.Leave.

func NewApp added in v0.3.0

func NewApp() *App

NewApp returns an App wired to the real terminal: OpenTTY, os.Stdin, os.Stdout, real Unix signal handling, and time.NewTicker for the tick channel. Tests build an App by hand and swap the seams instead.

func (*App) Consume added in v0.3.1

func (a *App) Consume()

Consume tells the event loop that the current Keys handler fully handled the event and it must NOT propagate to Root.OnEvent. Without Consume, every key that matches a Keys handler also reaches Root — which is fine for global shortcuts (Ctrl+C to quit still lets the Root repaint from its own state), but wrong for mode-switching editors where pressing 'i' to enter edit mode would otherwise ALSO insert 'i' into the underlying TextView.

Idempotent within a single event dispatch. The event loop clears the flag at the top of every event so a Consume from a previous event never affects the next one.

func (*App) IsQuitting added in v0.3.0

func (a *App) IsQuitting() bool

IsQuitting reports whether Quit has been called (whether the loop is on its way to exit). Non-blocking. Useful for consumer tests that verify a key handler triggered a quit without spinning the event loop.

func (*App) Quit added in v0.3.0

func (a *App) Quit()

Quit signals the event loop to exit on the next iteration. Safe to call from a key handler or a goroutine, and idempotent: a second call is a no-op (the underlying quit channel is closed exactly once via sync.Once).

func (*App) Refresh added in v0.3.0

func (a *App) Refresh()

Refresh marks the frame dirty so the next iteration repaints. It also nudges the wake channel so a loop currently blocked in select unblocks — enabling async I/O goroutines to schedule a redraw without racing an unrelated event.

func (*App) Run added in v0.3.0

func (a *App) Run() (exitCode int)

Run blocks, driving the event loop until Quit is called or an interrupt signal arrives. Returns the exit code (0 by default; 1 if TTY setup fails; 2 if a key handler or widget panics). Terminal state is guaranteed to be restored on exit — even on panic — via a deferred TTY.Leave.

The return is named (exitCode) so a panic recovered by the deferred recover func can overwrite it; without the named return the panic path would still yield the pre-panic value.

func (*App) SetOpenTTYFn added in v0.3.0

func (a *App) SetOpenTTYFn(fn func(*os.File) (TTY, error))

SetOpenTTYFn overrides the TTY factory the event loop uses at Run time. Consumer tests inject a fake TTY (or an error) so they can drive Run() without needing a real controlling terminal. Passing a factory that returns an error is a valid way to force Run() to exit with a non-zero code before any raw-mode side effect.

type Button added in v0.26.0

type Button struct {
	toolkit.Base
	Label   string
	OnClick func()
	Style   ButtonStyle // resting appearance; default ButtonDefault
	// contains filtered or unexported fields
}

Button is a cell-native clickable action: a filled block with a centred label. The resting face comes from its Style (Default / Prominent / Secondary); hover and press states override the fill so the user sees feedback before OnClick fires. A parent container drives SetHovered / SetPressed (enter/leave logic stays in one place, not every leaf).

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewButton added in v0.26.0

func NewButton(label string, onClick func()) *Button

NewButton constructs a Button with the given label and click handler (which may be nil -- a no-op button still renders).

func (*Button) Draw added in v0.26.0

func (b *Button) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the face (cycling Surface / SurfaceAlt / Accent by style and interaction state) and the centred label, with an on-accent ink that stays legible over an Accent fill.

func (*Button) OnEvent added in v0.26.0

func (b *Button) OnEvent(ev toolkit.Event)

OnEvent fires OnClick on a click or an Enter key (so a focused button is keyboard-activatable); other events are ignored.

func (*Button) SetFocused added in v0.41.0

func (b *Button) SetFocused(v bool)

SetFocused implements Focusable — a focused button highlights (like hover) and activates on Enter.

func (*Button) SetHovered added in v0.26.0

func (b *Button) SetHovered(v bool)

SetHovered / SetPressed are wired by the parent's mouse dispatcher so the button can render its hover / press visual states.

func (*Button) SetPressed added in v0.26.0

func (b *Button) SetPressed(v bool)

type ButtonStyle added in v0.26.0

type ButtonStyle int

ButtonStyle selects a button's resting fill, giving a layout a visual hierarchy. Hover + press still override the fill on top of the style.

const (
	// ButtonDefault is a Surface-faced button (the plain look).
	ButtonDefault ButtonStyle = iota
	// ButtonProminent is filled with Accent -- the primary action ("OK").
	ButtonProminent
	// ButtonSecondary is filled with SurfaceAlt -- a muted key between the two.
	ButtonSecondary
)

type Cell added in v0.3.6

type Cell struct {
	Rune rune
	Fg   Color
	Bg   Color
}

Cell is one grid position after ANSI-decoding a terminal frame: the rune actually written, plus the foreground and background colors that were active when it was written. All zero-value colors mean "default" (nothing set). Cell exists so consumer tests can assert on the visual output at cell-precision — the previous text-strip protocol dropped color information and let bugs like "highlight rendered as `█` characters" ship undetected.

type CheckButton added in v0.29.0

type CheckButton struct {
	toolkit.Base
	Label    string
	Checked  bool
	OnToggle func(checked bool)
	// contains filtered or unexported fields
}

CheckButton is a cell-native boolean toggle: a "[✓]" / "[ ]" box followed by a Label. A click anywhere on the row flips Checked and fires OnToggle. The box paints in Accent when checked so the state reads at a glance.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewCheckButton added in v0.29.0

func NewCheckButton(label string, checked bool) *CheckButton

NewCheckButton constructs a CheckButton with the given label and initial checked state.

func (*CheckButton) Draw added in v0.29.0

func (c *CheckButton) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the box (Accent when checked, Border otherwise) and the label.

func (*CheckButton) OnEvent added in v0.29.0

func (c *CheckButton) OnEvent(ev toolkit.Event)

OnEvent flips Checked and fires OnToggle on a click or an Enter key (so a focused checkbox is keyboard-toggleable); other events are ignored.

func (*CheckButton) SetFocused added in v0.41.0

func (c *CheckButton) SetFocused(v bool)

SetFocused implements Focusable — a focused checkbox draws its box in Accent and toggles on Enter.

type Color added in v0.3.6

type Color struct {
	R, G, B uint8
	Set     bool // false = default/reset, true = explicitly set
}

Color carries the 24-bit RGB values decoded from a CSI SGR "38;2" (foreground) or "48;2" (background) sequence. Zero value means "default" — SGR 0 or no color set.

type Dialog added in v0.37.0

type Dialog struct {
	toolkit.Base
	Title    string
	Message  []string
	Buttons  []string
	Active   int // focused button index
	Visible  bool
	OnAction func(idx int, label string)
}

Dialog is a cell-native modal: a content-sized box centred within its bounds, with a Title, Message lines, and a row of action Buttons. It manages focus across its own buttons — Left/Right and Tab/Shift+Tab move the focused button, Enter activates it, Escape cancels, and a click activates the clicked button. OnAction fires with the chosen button's index and label (index -1 on Escape / cancel).

Being a modal, the host both adds it to a container's overlays (so it is drawn and bounded) AND sets it as App.InputTarget while Visible (so it captures the keyboard) — the same pattern the command palette uses.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewDialog added in v0.37.0

func NewDialog(title string, message []string, buttons ...string) *Dialog

NewDialog builds a Dialog with the given title, message lines, and button labels, focusing the first button.

func (*Dialog) Draw added in v0.37.0

func (d *Dialog) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the box, title, message and button strip (the Active button in Accent), when Visible.

func (*Dialog) HitTest added in v0.37.0

func (d *Dialog) HitTest(px, py int) bool

HitTest — a hidden dialog claims no clicks (so routing falls through to the widgets beneath it).

func (*Dialog) OnEvent added in v0.37.0

func (d *Dialog) OnEvent(ev toolkit.Event)

OnEvent handles button traversal (Left/Right, Tab/Shift+Tab), activation (Enter / click) and cancel (Escape).

type Dropdown struct {
	toolkit.Base
	Options  []string
	Selected int
	Open     bool
	OnChange func(idx int, value string)
	// contains filtered or unexported fields
}

Dropdown is a cell-native value picker (combobox): a one-row control showing the currently-selected Option and a ▼ indicator; when Open it lists the options below (within the widget's bounds) with the active one highlighted. Enter / Down / a click opens it; Up/Down move the highlight; Enter or a click selects (firing OnChange only on a real change) and closes; Escape closes without changing. Unlike MenuDropdown (a menu of one-shot actions), a Dropdown persists a chosen value — the core form select control.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewDropdown added in v0.39.0

func NewDropdown(options []string, selected int) *Dropdown

NewDropdown builds a Dropdown over options with the given initial selection (clamped into range).

func (d *Dropdown) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the control row and, when Open, the option list below it.

func (d *Dropdown) OnEvent(ev toolkit.Event)

OnEvent toggles/navigates the list. Collapsed: Enter/Down/click opens. Open: Up/Down move the highlight, Enter/click selects, Escape closes.

type Entry added in v0.25.0

type Entry struct {
	toolkit.Base
	Text        string
	Placeholder string
	Cursor      int // rune index in [0, len(runes)]
	Focused     bool
	OnChange    func(text string)
	OnSubmit    func(text string)
	// contains filtered or unexported fields
}

Entry is a cell-native single-line text input: a search box, a command prompt, a form field. It edits Text via EventKeyDown (Backspace, Delete, ArrowLeft/Right, Home, End, Enter) and EventChar (printable runes), shows a muted Placeholder while empty and unfocused, and scrolls horizontally so the cursor stays visible in a narrow field. Cursor is a rune index, so multi-byte UTF-8 characters move it by one visible column.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewEntry added in v0.25.0

func NewEntry(initial string) *Entry

NewEntry builds an Entry with initial text and the cursor parked at the end.

func (*Entry) Draw added in v0.25.0

func (e *Entry) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the field background, the text (or Placeholder), and — when Focused — a reverse-video block cursor.

func (*Entry) OnEvent added in v0.25.0

func (e *Entry) OnEvent(ev toolkit.Event)

OnEvent handles focus + caret placement on click, keyboard navigation and editing, and character insertion.

func (*Entry) SetFocused added in v0.41.0

func (e *Entry) SetFocused(focused bool)

SetFocused implements Focusable — it drives whether the caret is drawn.

type Expander added in v0.44.0

type Expander struct {
	toolkit.Base
	Title    string
	Body     toolkit.Widget
	Expanded bool
	OnToggle func(expanded bool)
	// contains filtered or unexported fields
}

Expander is a cell-native collapsible section: a header row with a ▾/▸ chevron and a Title, and a Body widget shown in the rows below only while Expanded. Clicking the header (or Enter while focused) toggles it; clicks in the open body forward to Body. It is Focusable (chevron renders Accent when focused), so it fits a FocusRing.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewExpander added in v0.44.0

func NewExpander(title string, body toolkit.Widget) *Expander

NewExpander builds a collapsed Expander with the given title and body.

func (*Expander) Draw added in v0.44.0

func (e *Expander) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the header (chevron + title) and, when Expanded, the Body.

func (*Expander) OnEvent added in v0.44.0

func (e *Expander) OnEvent(ev toolkit.Event)

OnEvent toggles on a header click / Enter, and forwards clicks in the open body to Body (translated below the header row).

func (*Expander) SetBounds added in v0.44.0

func (e *Expander) SetBounds(r toolkit.Rect)

SetBounds reserves the header row and lays the Body out below it.

func (*Expander) SetFocused added in v0.44.0

func (e *Expander) SetFocused(v bool)

SetFocused implements Focusable.

type FocusRing added in v0.41.0

type FocusRing struct {
	toolkit.Base
	Items []Focusable
	// contains filtered or unexported fields
}

FocusRing gives a set of Focusables a shared keyboard focus: Tab advances and Shift+Tab retreats (both wrapping); a click focuses the widget it lands on; every other event is forwarded to the focused member. Arrow keys are NOT consumed by the ring — they reach the focused widget, so a focused Dropdown or Scale keeps its own arrow behaviour. Layout is the caller's job (position each member's bounds); the ring manages focus + routing and draws its members.

A form wires its inputs into a FocusRing and hands it to the App as Root (or as an InputTarget); Tab then walks the fields exactly as a GUI form does.

func NewFocusRing added in v0.41.0

func NewFocusRing(items ...Focusable) *FocusRing

NewFocusRing builds a ring over items, focusing the first.

func (*FocusRing) Draw added in v0.41.0

func (r *FocusRing) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints every member (positioned by the caller).

func (*FocusRing) Focus added in v0.41.0

func (r *FocusRing) Focus(i int)

Focus moves focus to member i (clamped), updating SetFocused on the old and new members.

func (*FocusRing) Focused added in v0.41.0

func (r *FocusRing) Focused() int

Focused returns the focused member's index, or -1 when the ring is empty.

func (*FocusRing) Next added in v0.41.0

func (r *FocusRing) Next()

Next / Prev move focus to the following / previous member, wrapping.

func (*FocusRing) OnEvent added in v0.41.0

func (r *FocusRing) OnEvent(ev toolkit.Event)

OnEvent moves focus on Tab/Shift+Tab, focuses + forwards a click to the member it hits, and forwards every other event to the focused member.

func (*FocusRing) Prev added in v0.41.0

func (r *FocusRing) Prev()

type Focusable added in v0.41.0

type Focusable interface {
	toolkit.Widget
	SetFocused(focused bool)
}

Focusable is a widget that can hold keyboard focus within a FocusRing. The ring calls SetFocused as focus moves; the widget renders a focus cue while focused and acts on the keys the ring forwards to it (a Button/CheckButton/ RadioButton fires on Enter, an Entry edits its text).

type HSplit added in v0.23.0

type HSplit struct {
	toolkit.Base
	Left, Right toolkit.Widget
	LeftFrac    int
	// contains filtered or unexported fields
}

HSplit is a cell-native resizable horizontal split: Left takes LeftFrac percent of the width, a 1-cell grip column separates it from Right, and Right takes the remainder. The grip glyph doubles as a drag handle -- a click on it starts a drag session; subsequent EventMouseDrag events (routed here by a parent VBox's drag-capture, or any container that keeps forwarding a captured drag) update LeftFrac; EventMouseUp ends the session. LeftFrac is clamped to [HSplitMinFrac, HSplitMaxFrac] so neither pane can collapse to zero cells.

Events are local (0-based within the split); bounds are absolute for Draw. Renders through painter.Painter, so the same split drives the cell (TUI) and RGBA (WUI/GUI) backends.

func (*HSplit) Draw added in v0.23.0

func (h *HSplit) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints both panes, then the grip column on top (Accent while dragging, Border otherwise) so the separator stays visible over either pane's bg.

func (*HSplit) OnEvent added in v0.23.0

func (h *HSplit) OnEvent(ev toolkit.Event)

OnEvent handles resize drags and routes clicks/keys to the panes. A click on the grip starts a drag session; a click left/right of it goes to that pane (right pane translated to its local origin); non-mouse events go to Left.

func (*HSplit) SetBounds added in v0.23.0

func (h *HSplit) SetBounds(r toolkit.Rect)

SetBounds lays out the left and right panes, reserving one column for the grip.

type InputParser added in v0.3.0

type InputParser struct {
	// contains filtered or unexported fields
}

InputParser reads bytes from a terminal stdin stream and emits toolkit.Event values. A single call to InputParser.Feed may produce zero, one, or many events: typing "abc" yields three toolkit.EventChar events, pasting a control sequence like "\x1b[A" yields one arrow-up event but consumes three bytes.

The toolkit exposes toolkit.EventKeyDown for named-key presses (Enter, Tab, arrows, Escape, …) and toolkit.EventChar for printable characters — the parser routes accordingly.

The pending buffer holds the incomplete tail of the last Feed call: a lone ESC (0x1B) whose next byte has not arrived, an ESC [ … partial CSI sequence still waiting for its final byte (0x40..0x7E), or a partial UTF-8 rune (1–3 bytes of a multi-byte codepoint split across two Reads). It is consumed automatically on the next Feed call, or drained by InputParser.Flush on an input-idle deadline.

func NewInputParser added in v0.3.0

func NewInputParser() *InputParser

NewInputParser returns a fresh parser with an empty buffer.

func (*InputParser) Feed added in v0.3.0

func (p *InputParser) Feed(b []byte) []toolkit.Event

Feed hands b bytes to the parser and returns any events that completed as a result. Bytes belonging to an incomplete sequence (a lone ESC, a partial CSI, a partial UTF-8 rune) are buffered internally and consumed on the next Feed call.

The classic Escape-vs-CSI ambiguity is resolved as follows: a lone ESC at the end of the buffer is held pending; on the next Feed a leading '[' starts a CSI sequence, any other byte causes the held ESC to be emitted as "Escape" and the following byte is then processed as a fresh input. Callers waiting on an idle deadline flush the held ESC via InputParser.Flush.

func (*InputParser) Flush added in v0.3.0

func (p *InputParser) Flush() []toolkit.Event

Flush emits any pending ESC (or partial CSI) as an "Escape" event and clears the buffer. Callers invoke it on an input-idle deadline to resolve the Escape-vs-CSI ambiguity in favour of a plain Escape. A partial UTF-8 rune is discarded silently — its tail bytes would be indistinguishable from an unrelated new keystroke by the time an idle timeout fires.

type LevelBar added in v0.32.0

type LevelBar struct {
	toolkit.Base
	Value, Max int
}

LevelBar is the discrete cousin of ProgressBar: Max equal segments separated by a 1-cell gap, the first Value in Accent and the rest in SurfaceAlt. Useful for battery / signal-strength / step indicators at terminal scale.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewLevelBar added in v0.32.0

func NewLevelBar(max int) *LevelBar

NewLevelBar builds a LevelBar with the given Max (floored at 1) and Value 0.

func (*LevelBar) Draw added in v0.32.0

func (l *LevelBar) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints Max segments with a 1-cell gap; the first Value segments use Accent, the rest SurfaceAlt.

type ListBox added in v0.18.0

type ListBox struct {
	toolkit.Base
	Items    []string
	Selected int
	// OnSelect fires after Selected changes (keyboard or click). Optional; a
	// nil callback is a no-op. It receives the new index.
	OnSelect func(int)
	// contains filtered or unexported fields
}

ListBox is a cell-native single-selection list: one item per row, an accent-highlighted selection, arrow / Home / End / PageUp / PageDown navigation, click-to-select, and a viewport that scrolls to keep the selection visible. It is a toolkit.Widget, so like every widget here it renders through painter.Painter -- a terminal cell grid (TUI) or an RGBA pixel buffer (WUI/GUI) with the same code.

func NewListBox added in v0.18.0

func NewListBox(items []string) *ListBox

NewListBox returns a ListBox over items with the first row selected.

func (*ListBox) Draw added in v0.18.0

func (l *ListBox) Draw(p painter.Painter, theme *toolkit.Theme)

Draw paints the pane background, then the visible rows with the selected row highlighted in the theme accent.

func (*ListBox) OnEvent added in v0.18.0

func (l *ListBox) OnEvent(ev toolkit.Event)

OnEvent handles list navigation + click selection.

type MenuBar struct {
	toolkit.Base
	Items []MenuItem
}

MenuBar is a cell-native horizontal menu: item labels laid out left to right on a single row, separated by menuBarSep cells, with a click on a label's cell range firing its OnClick. It is a toolkit.Widget, rendering through painter.Painter (cell grid for TUI, RGBA buffer for WUI/GUI).

func NewMenuBar added in v0.19.0

func NewMenuBar(items ...MenuItem) *MenuBar

NewMenuBar returns a MenuBar over items.

func (m *MenuBar) Draw(p painter.Painter, theme *toolkit.Theme)

Draw paints the bar background and the item labels.

func (m *MenuBar) ItemXRange(i int) (int, int)

ItemXRange returns the [start, end) local-X cell range of the i-th item, or (-1, -1) if i is out of range. Callers use it both to hit-test and to anchor a dropdown directly under an item.

func (m *MenuBar) OnEvent(ev toolkit.Event)

OnEvent fires the OnClick of whichever item's label range contains a click.

type MenuDropdown struct {
	toolkit.Base
	Title       string
	Body        []string
	ItemActions []func() // parallel to Body; nil / short = informational row
	Visible     bool
	AnchorX     int
	AnchorY     int
}

MenuDropdown is an anchored, self-sizing dropdown menu -- the panel that opens under a MenuBar item. It positions itself at (AnchorX, AnchorY) and sizes to its content, ignoring any bounds a container hands it. A click on a Body row runs the matching ItemActions entry (nil / short slice = an informational row: the click still dismisses the menu but runs nothing) and hides the menu.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func (d *MenuDropdown) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the anchored box (when Visible): Title on the top row, Body rows below.

func (d *MenuDropdown) HitTest(px, py int) bool

HitTest — hidden dropdowns claim no clicks.

func (d *MenuDropdown) OnEvent(ev toolkit.Event)

OnEvent runs the clicked Body row's action (if any) and dismisses the menu.

func (d *MenuDropdown) SetBounds(_ toolkit.Rect)

SetBounds ignores the requested rect and self-positions at the anchor with the natural size (a container's layout pass calls this; the dropdown opts out of the normal flow).

type MenuItem struct {
	Label   string
	OnClick func()
}

MenuItem is one clickable entry in a MenuBar. OnClick is optional -- a nil callback is a no-op (useful for a decorative or coming-soon slot).

type Notebook added in v0.31.0

type Notebook struct {
	toolkit.Base
	Tabs         []NotebookTab
	Active       int
	OnTabChanged func(idx int)
}

Notebook is a cell-native tabbed container: a 1-row tab strip on top, the active page's body below. Tabs size to their labels. A click on a tab swaps Active and fires OnTabChanged; every other event routes to the active page (translated below the strip).

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewNotebook added in v0.31.0

func NewNotebook() *Notebook

NewNotebook returns an empty Notebook (no tabs, Active = 0).

func (*Notebook) AddTab added in v0.31.0

func (n *Notebook) AddTab(label string, page toolkit.Widget)

AddTab appends a tab with the given label and page widget.

func (*Notebook) Draw added in v0.31.0

func (n *Notebook) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the tab strip (active tab in Accent) and the active page's body.

func (*Notebook) OnEvent added in v0.31.0

func (n *Notebook) OnEvent(ev toolkit.Event)

OnEvent selects a tab on a strip click, else routes the event to the active page (translated below the strip).

type NotebookTab added in v0.31.0

type NotebookTab struct {
	Label string
	Page  toolkit.Widget
}

NotebookTab is one entry in a Notebook: a Label painted on the tab and the Page widget shown in the body when that tab is active.

type Popover added in v0.20.0

type Popover struct {
	toolkit.Base
	Title   string
	Body    []string
	Visible bool
}

Popover is a cell-native modal overlay: a bordered box with a Title on the top row and Body lines below, shown only while Visible. It sizes its drawn height to the content (3 border/title rows + one per Body line, capped to its bounds). HitTest returns false while hidden, so an invisible Popover never claims a click from the widgets beneath it.

A toolkit.Widget: renders through painter.Painter (cell grid for TUI, RGBA buffer for WUI/GUI).

func (*Popover) Draw added in v0.20.0

func (p *Popover) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the box (when Visible) tightened to the content height.

func (*Popover) HitTest added in v0.20.0

func (p *Popover) HitTest(px, py int) bool

HitTest — an invisible Popover must not claim clicks (its bounds still cover the area), else routing would swallow events meant for the content below.

type ProgressBar added in v0.27.0

type ProgressBar struct {
	toolkit.Base
	Fraction float64
	Label    string
}

ProgressBar is a cell-native horizontal fill: a SurfaceAlt track with the first Fraction of its cells filled in Accent, and an optional Label centred over the bar. Fraction is clamped to [0,1]. Use it for download / load / completion indicators at terminal scale.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewProgressBar added in v0.27.0

func NewProgressBar() *ProgressBar

NewProgressBar builds an empty (Fraction=0) ProgressBar.

func (*ProgressBar) Draw added in v0.27.0

func (p *ProgressBar) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the track, the Accent fill proportional to Fraction, and the centred Label.

func (*ProgressBar) SetFraction added in v0.27.0

func (p *ProgressBar) SetFraction(f float64)

SetFraction clamps f to [0,1] and assigns it (0 = empty, 1 = full).

type RadioButton added in v0.33.0

type RadioButton struct {
	toolkit.Base
	Label    string
	Checked  bool
	OnToggle func(checked bool)
	// contains filtered or unexported fields
}

RadioButton is a cell-native circular toggle paired with a label: "(•)" checked / "( )" unchecked, then the Label. Grouped via RadioGroup for mutual exclusion; a standalone RadioButton toggles like a CheckButton.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewRadioButton added in v0.33.0

func NewRadioButton(label string) *RadioButton

NewRadioButton constructs a standalone RadioButton. Add it to a RadioGroup for mutual-exclusion behaviour.

func (*RadioButton) Draw added in v0.33.0

func (r *RadioButton) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the mark (Accent when checked, Border otherwise) and the label.

func (*RadioButton) OnEvent added in v0.33.0

func (r *RadioButton) OnEvent(ev toolkit.Event)

OnEvent, on a click or Enter (so a focused radio is keyboard-activatable), routes through the group (siblings clear) if grouped, else toggles locally.

func (*RadioButton) SetFocused added in v0.41.0

func (r *RadioButton) SetFocused(v bool)

SetFocused implements Focusable — a focused radio draws its mark in Accent and activates on Enter.

type RadioGroup added in v0.33.0

type RadioGroup struct {
	Members []*RadioButton
	Active  int
}

RadioGroup makes a set of RadioButtons mutually exclusive. Active is the index of the checked member, or -1 when none has been clicked yet.

func NewRadioGroup added in v0.33.0

func NewRadioGroup() *RadioGroup

NewRadioGroup builds an empty group with Active = -1.

func (*RadioGroup) Add added in v0.33.0

func (g *RadioGroup) Add(r *RadioButton)

Add appends r to the group and records its membership so a click on any member can clear the others.

type Scale added in v0.28.0

type Scale struct {
	toolkit.Base
	Min, Max float64
	Value    float64
	Step     float64 // arrow-key increment; <= 0 means a tenth of the range
	OnChange func(v float64)
}

Scale is a cell-native horizontal slider over a continuous Min..Max range: a SurfaceAlt track, an Accent fill up to the thumb, and a thumb cell at the value's position. A click or drag jumps the thumb to that column; ArrowLeft/Right step by Step (or a tenth of the range when Step <= 0). Because it treats a click and a captured drag identically, a Scale nested in a drag-capturing container (VBox / HSplit) is smoothly draggable.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewScale added in v0.28.0

func NewScale(min, max, initial float64) *Scale

NewScale builds a Scale spanning [min, max] with the given initial value. Min == Max is allowed but renders a non-interactive track.

func (*Scale) Draw added in v0.28.0

func (s *Scale) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the track, the Accent fill up to the thumb, and the thumb cell.

func (*Scale) OnEvent added in v0.28.0

func (s *Scale) OnEvent(ev toolkit.Event)

OnEvent handles click/drag positioning and arrow-key stepping.

func (*Scale) SetValue added in v0.28.0

func (s *Scale) SetValue(v float64)

SetValue clamps v to [Min, Max] before assigning.

type SpinButton added in v0.43.0

type SpinButton struct {
	toolkit.Base
	Min, Max int
	Value    int
	Step     int
	OnChange func(v int)
	// contains filtered or unexported fields
}

SpinButton is a cell-native integer field with steppers: a ◂ decrement cap on the left, the value, and a ▸ increment cap on the right, all within [Min, Max]. Clicking a cap (or, while focused, Up/Right/'+' and Down/Left/'-') steps the value by Step and fires OnChange on a real change. It is Focusable, so it drops into a FocusRing as a form control (the caps render in Accent when focused).

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewSpinButton added in v0.43.0

func NewSpinButton(min, max, initial, step int) *SpinButton

NewSpinButton builds a SpinButton over [min, max] with the given initial value (clamped) and step (floored at 1 so a stepper always moves).

func (*SpinButton) Draw added in v0.43.0

func (s *SpinButton) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the ◂ / ▸ caps and the centred value.

func (*SpinButton) OnEvent added in v0.43.0

func (s *SpinButton) OnEvent(ev toolkit.Event)

OnEvent steps on a cap click or (when focused) an arrow / +/- key.

func (*SpinButton) SetFocused added in v0.43.0

func (s *SpinButton) SetFocused(v bool)

SetFocused implements Focusable — a focused spinner renders its caps in Accent and responds to arrow / +/- keys.

func (*SpinButton) SetValue added in v0.43.0

func (s *SpinButton) SetValue(v int)

SetValue clamps v to [Min, Max] and assigns it (no OnChange — direct setter).

type Spinner added in v0.38.0

type Spinner struct {
	toolkit.Base
	Frames []string // animation frames (one glyph each); defaults to braille dots
	Label  string
	Active bool
	// contains filtered or unexported fields
}

Spinner is a cell-native busy indicator: a single animated glyph (Accent), optionally followed by a Label. It advances one Frame per EventTick while Active, so a host that wants animation runs the App with TickHz > 0. Set Active = false to freeze it (e.g. when the work completes).

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewSpinner added in v0.38.0

func NewSpinner(label string) *Spinner

NewSpinner builds an active Spinner with the default frames and the given label (which may be empty).

func (*Spinner) Draw added in v0.38.0

func (s *Spinner) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the current frame (Accent) and the label (OnSurface).

func (*Spinner) OnEvent added in v0.38.0

func (s *Spinner) OnEvent(ev toolkit.Event)

OnEvent advances the animation one frame per tick while Active.

type Statusbar added in v0.24.0

type Statusbar struct {
	toolkit.Base
	Segments []string
	// SegmentMinW is the minimum width (in cells) any non-last segment takes;
	// the last segment always fills the rest of the bar. Defaults to
	// StatusbarSegmentMinW when <= 0.
	SegmentMinW int
}

Statusbar is a cell-native footer strip: N text segments laid out left-to-right (e.g. "Line 12, Col 4" + "UTF-8" + "Go" in an editor), each with a 1-cell pad, a thin vertical divider between them, and the LAST segment stretched to fill the remaining width so an empty bar still looks deliberate. It is the natural pairing for a MenuBar above and a document area between — together they assemble the stock window frame at terminal scale.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewStatusbar added in v0.24.0

func NewStatusbar(segs []string) *Statusbar

NewStatusbar builds a Statusbar with the given segments and the default minimum segment width.

func (*Statusbar) Draw added in v0.24.0

func (s *Statusbar) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the strip background plus every segment, with a divider column between adjacent segments.

func (*Statusbar) SetSegment added in v0.24.0

func (s *Statusbar) SetSegment(i int, text string)

SetSegment replaces the i-th segment in place. An index past the end grows the bar (filling intermediate slots with "") so callers can add segments lazily; a negative index is ignored.

type TTY added in v0.3.0

type TTY interface {
	// Enter puts the terminal in raw mode, saves the current
	// termios state, switches to the alt-screen, and hides the
	// cursor. Idempotent — a second call while entered is a no-op.
	Enter() error

	// Leave reverses Enter: restores the alt-screen, shows the
	// cursor, and restores the saved termios state. Always safe to
	// call, including from a defer or a panic recovery.
	Leave() error

	// Size returns the current terminal dimensions in cells.
	Size() (cols, rows int, err error)
}

TTY is the abstraction the App runner uses to talk to a real terminal. Implementations set raw mode, hide the cursor, switch to the alt-screen, and reverse everything on Close. The mock implementation used in tests satisfies the same interface without touching a real terminal.

func OpenTTY added in v0.3.0

func OpenTTY(f *os.File) (TTY, error)

OpenTTY opens the terminal associated with the given file (which must be a tty — os.Stdout in normal use). Returns an error on non-Unix platforms or if the file is not a tty.

type Table added in v0.35.0

type Table struct {
	toolkit.Base
	Columns  []TableColumn
	Rows     [][]string
	Selected int // -1 = no selection
	OnSelect func(row int)
	// contains filtered or unexported fields
}

Table is a cell-native data grid: a header row on top, then one body row per Rows entry with zebra striping and an accent-highlighted selection. Auto columns reflow to the widget width. Arrow / Home / End / PageUp / PageDown navigate and a click selects a body row; the viewport scrolls to keep the selection visible.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewTable added in v0.35.0

func NewTable(cols []TableColumn, rows [][]string) *Table

NewTable builds a Table with the given columns and rows, no row selected.

func (*Table) Draw added in v0.35.0

func (t *Table) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the header, the visible body rows, and the column separators.

func (*Table) OnEvent added in v0.35.0

func (t *Table) OnEvent(ev toolkit.Event)

OnEvent handles row navigation and click selection (the header row is inert).

type TableColumn added in v0.35.0

type TableColumn struct {
	Title string
	Width int // cells; 0 = auto (equal share of the remainder)
}

TableColumn is one column: a header Title and an optional fixed Width in cells. A Width of 0 marks the column "auto" — it claims an equal share of the cells left after the fixed columns.

type TermGrid added in v0.3.6

type TermGrid struct {
	Rows  int
	Cols  int
	Cells []Cell
}

TermGrid is a 2D cell buffer produced by DecodeANSI. Rows and Cols are the grid dimensions; Cells is row-major (Cells[y*Cols+x]).

func DecodeANSI added in v0.3.6

func DecodeANSI(stream []byte, cols, rows int) *TermGrid

DecodeANSI parses stream as a sequence of CSI SGR + printable runes + cursor-positioning + line breaks, and lays every printable rune into a Cell at the current cursor position with the current foreground / background color. Only the subset of CSI a painter.CellPainter emits is honored:

  • CSI H → cursor to (1, 1)
  • CSI r ; c H → cursor to (c, r), 1-indexed
  • CSI 0 m → reset colors
  • CSI 38 ; 2 ; R ; G ; B m → foreground truecolor
  • CSI 48 ; 2 ; R ; G ; B m → background truecolor
  • CSI ?1049 h / l → alt-screen enter/leave (recognised, no-op)
  • CSI ?25 h / l → cursor show/hide (recognised, no-op)

Unknown CSI sequences consume their bytes and are ignored so a caller can freely pass a frame that also contains cursor styling or bracketed paste. \n and \r reset the cursor to column 0 and advance the row (LF) or stay on the same row (CR).

func (*TermGrid) At added in v0.3.6

func (g *TermGrid) At(x, y int) Cell

At returns the cell at (x, y). Out-of-bounds coordinates return the zero Cell — callers that care about bounds should check dims.

func (*TermGrid) Row added in v0.3.6

func (g *TermGrid) Row(y int) []Cell

Row returns the y-th row as a Cell slice. Panics on out-of-bounds y — mirrors slice indexing conventions.

func (*TermGrid) RowText added in v0.3.6

func (g *TermGrid) RowText(y int) string

RowText joins the Rune of every cell in row y into a string. Non- printable / space runes stay as ' '. Useful for coarse "does row Y contain this text" assertions without giving up the color info the underlying grid still carries.

type TextEditor added in v0.9.0

type TextEditor struct {
	toolkit.Base
	Lines      []string
	CursorLine int
	CursorCol  int
	Focused    bool
	Filename   string
	ReadOnly   bool
	ShowGutter bool
	// contains filtered or unexported fields
}

TextEditor is a cell-native, read-write multi-line text editor widget with syntax highlighting and an optional line-number gutter. It is a toolkit.Widget: give it a bounds, set Focused, and forward events to OnEvent.

  • One cell per glyph, one row per line (no soft wrap).
  • Filename's extension drives the syntax language (see the syntax package); "" leaves the text plain. Highlighting is recomputed on every change so it tracks live edits.
  • Set ReadOnly to use it as a code viewer: insert / delete / newline events are ignored, but the caret still navigates (arrows, click).
  • ShowGutter draws a right-aligned line-number gutter (NewTextEditor enables it).

func NewTextEditor added in v0.9.0

func NewTextEditor() *TextEditor

NewTextEditor returns an empty editor with the line-number gutter enabled.

func (*TextEditor) Copy added in v0.13.0

func (t *TextEditor) Copy() string

Copy stores the selection in the widget clipboard and returns it.

func (*TextEditor) Cut added in v0.13.0

func (t *TextEditor) Cut() string

Cut copies the selection to the clipboard and removes it, returning the text.

func (*TextEditor) DeleteSelection added in v0.13.0

func (t *TextEditor) DeleteSelection() bool

DeleteSelection removes the selected text (a single undo step). Returns whether anything was deleted.

func (*TextEditor) Draw added in v0.9.0

func (t *TextEditor) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the pane background, the (optional) line-number gutter, the syntax-highlighted text, and the caret.

func (*TextEditor) Find added in v0.11.0

func (t *TextEditor) Find(query string) bool

Find moves the caret to the next occurrence of query at or after the current caret, searching forward and wrapping to the top; it returns whether a match was found. An empty query is a no-op. The query is remembered for FindNext.

func (*TextEditor) FindNext added in v0.11.0

func (t *TextEditor) FindNext() bool

FindNext repeats the last Find starting just past the caret, so successive calls walk consecutive matches (wrapping). No-op with no prior Find.

func (*TextEditor) OnEvent added in v0.9.0

func (t *TextEditor) OnEvent(ev toolkit.Event)

OnEvent applies one input event: character insert, Backspace, Delete, Enter, Ctrl+Z/Y (undo/redo), Ctrl+X/V (cut/paste) -- all no-ops when ReadOnly -- the arrow / Home / End / PageUp / PageDown navigation keys, or a click / drag (caret + selection). Re-highlights after any change.

func (*TextEditor) Paste added in v0.13.0

func (t *TextEditor) Paste()

Paste inserts the clipboard at the caret, replacing an active selection. A single undo step. No-op when ReadOnly or the clipboard is empty.

func (*TextEditor) Replace added in v0.12.0

func (t *TextEditor) Replace(query, repl string) bool

Replace finds the next occurrence of query (forward from the caret, wrapping) and replaces it with repl, leaving the caret just after the replacement so a repeated call walks to the next one. Returns whether a replacement happened. No-op (false) when ReadOnly or query is empty. Undoable as one step.

func (*TextEditor) ReplaceAll added in v0.12.0

func (t *TextEditor) ReplaceAll(query, repl string) int

ReplaceAll replaces every occurrence of query with repl across the whole buffer and returns the number replaced. The caret column is clamped to its (possibly shorter) line. No-op (0) when ReadOnly or query is empty; the whole operation is a single undo step.

func (*TextEditor) SelectedText added in v0.13.0

func (t *TextEditor) SelectedText() string

SelectedText returns the text covered by the active selection, or "" if none.

func (*TextEditor) SetText added in v0.9.0

func (t *TextEditor) SetText(s string)

SetText replaces the buffer (split on "\n"), resets the caret to the top, and re-highlights.

func (*TextEditor) Text added in v0.9.0

func (t *TextEditor) Text() string

Text returns the buffer joined by newlines (no trailing newline).

type Toolbar added in v0.34.0

type Toolbar struct {
	toolkit.Base
	Items []ToolbarItem
}

Toolbar is a cell-native horizontal strip of labelled buttons and optional separators — the action strip that sits below a MenuBar and composes with Notebook + Statusbar into a stock window frame. Buttons size to their labels; a click runs an enabled item's OnClick.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewToolbar added in v0.34.0

func NewToolbar(items []ToolbarItem) *Toolbar

NewToolbar builds a Toolbar with the given items.

func (*Toolbar) Draw added in v0.34.0

func (t *Toolbar) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the strip background, each button (muted when Disabled), and the separators.

func (*Toolbar) OnEvent added in v0.34.0

func (t *Toolbar) OnEvent(ev toolkit.Event)

OnEvent runs the clicked item's OnClick when it is an enabled button.

type ToolbarItem added in v0.34.0

type ToolbarItem struct {
	Label     string
	OnClick   func()
	Disabled  bool
	Separator bool
}

ToolbarItem is one cell in a Toolbar: a labelled button with an OnClick and a Disabled flag, or — when Separator is true — a 1-cell vertical divider (the other fields are ignored).

type TreeNode added in v0.30.0

type TreeNode struct {
	Label    string
	Expanded bool
	Children []*TreeNode
	Data     any
}

TreeNode is one entry in a TreeView. Children nest arbitrarily deep; Expanded controls whether they render. Data is anything the host wants to hang off the node (a path, an id, the model object) — the widget never reads it.

type TreeView added in v0.30.0

type TreeView struct {
	toolkit.Base
	Root       *TreeNode
	Selected   *TreeNode
	OnActivate func(node *TreeNode)
	// contains filtered or unexported fields
}

TreeView renders a hierarchical TreeNode set as indented cell rows: a ▼/▶ chevron on nodes with children, then the label. A click on the chevron toggles Expanded; a click elsewhere selects the row and fires OnActivate. The arrow keys navigate (Up/Down move the selection, Left collapses, Right expands) and Enter activates the selection. Long trees scroll to keep the selection visible.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func NewTreeView added in v0.30.0

func NewTreeView(root *TreeNode) *TreeView

NewTreeView builds a TreeView rooted at root (nil for an empty view).

func (*TreeView) Draw added in v0.30.0

func (t *TreeView) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints the visible rows (chevron + indented label), highlighting the selection.

func (*TreeView) OnEvent added in v0.30.0

func (t *TreeView) OnEvent(ev toolkit.Event)

OnEvent handles click (chevron toggle / row select) and keyboard navigation.

type VBox added in v0.22.0

type VBox struct {
	toolkit.Base
	Header   toolkit.Widget
	Body     toolkit.Widget
	Footer   toolkit.Widget
	HeaderH  int
	FooterH  int
	Overlays []toolkit.Widget
	// contains filtered or unexported fields
}

VBox is a header / body / footer vertical layout: Header keeps a fixed HeaderH rows at the top, Footer a fixed FooterH rows at the bottom, and Body fills the space between. Overlays float on top, inset from the body area, and are hit-tested (so an invisible Popover / anchored MenuDropdown claims clicks only when it wants them). It captures a drag once a click lands on a child so subsequent EventMouseDrag / EventMouseUp keep flowing to that child with the same coordinate translation.

A toolkit.Widget rendering through painter.Painter (cell grid / RGBA buffer).

func (*VBox) Draw added in v0.22.0

func (p *VBox) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints body first, then header + footer chrome, then overlays on top.

func (*VBox) OnEvent added in v0.22.0

func (p *VBox) OnEvent(ev toolkit.Event)

OnEvent routes an event: an active drag stays with its captured child; a click goes to the topmost overlay that hit-tests, else the header / footer / body band it lands in (translated to child-local coordinates); any other event goes to the body.

func (*VBox) SetBounds added in v0.22.0

func (p *VBox) SetBounds(r toolkit.Rect)

SetBounds lays out the header, body, footer and overlays within r.

type VSplit added in v0.40.0

type VSplit struct {
	toolkit.Base
	Top, Bottom toolkit.Widget
	TopFrac     int
	// contains filtered or unexported fields
}

VSplit is the vertical counterpart of HSplit: Top takes TopFrac percent of the height, a 1-row grip separates it from Bottom, and Bottom takes the rest. The grip row doubles as a drag handle -- a click on it starts a drag session; subsequent EventMouseDrag events (forwarded by a parent's capture) update TopFrac; EventMouseUp ends it. TopFrac is clamped to [VSplitMinFrac, VSplitMaxFrac] so neither pane collapses. Useful for editor-over-output or list-over-detail layouts.

Events are local (0-based within the split); bounds are absolute for Draw. Renders through painter.Painter (cell grid / RGBA buffer).

func (*VSplit) Draw added in v0.40.0

func (v *VSplit) Draw(pnt painter.Painter, theme *toolkit.Theme)

Draw paints both panes, then the grip row on top (Accent while dragging, Border otherwise) so the separator stays visible over either pane's bg.

func (*VSplit) OnEvent added in v0.40.0

func (v *VSplit) OnEvent(ev toolkit.Event)

OnEvent handles resize drags and routes clicks/keys to the panes. A click on the grip starts a drag session; a click above/below it goes to that pane (bottom pane translated to its local origin); non-mouse events go to Top.

func (*VSplit) SetBounds added in v0.40.0

func (v *VSplit) SetBounds(r toolkit.Rect)

SetBounds lays out the top and bottom panes, reserving one row for the grip.

Directories

Path Synopsis
cmd
tui-editor command
tui-editor is the reference loom-style interactive demo for tui.App: a modal (vi-inspired) text editor built on top of the existing toolkit widgets — TextView for the buffer, Statusbar for the mode + cursor + filename indicator, MenuBar for the chrome, and a Popover-wrapped SearchEntry for the command palette.
tui-editor is the reference loom-style interactive demo for tui.App: a modal (vi-inspired) text editor built on top of the existing toolkit widgets — TextView for the buffer, Statusbar for the mode + cursor + filename indicator, MenuBar for the chrome, and a Popover-wrapped SearchEntry for the command palette.
tui-explorer command
tui-explorer is the reference interactive demo for tui.App: a small k9s-style file-browser mockup composed entirely from the exported cell-native widgets in the tui package.
tui-explorer is the reference interactive demo for tui.App: a small k9s-style file-browser mockup composed entirely from the exported cell-native widgets in the tui package.
tui-snapshot command
tui-snapshot renders a sample widget tree to stdout as an ANSI stream.
tui-snapshot renders a sample widget tree to stdout as an ANSI stream.
tui-widget-explorer command
tui-widget-explorer is an interactive gallery of the go-widgets/tui cell-native widget set: a scrollable list of widget names on the left, a live instance of the selected widget on the right.
tui-widget-explorer is an interactive gallery of the go-widgets/tui cell-native widget set: a scrollable list of widget names on the left, a live instance of the selected widget on the right.
tui-widgets command
tui-widgets renders the go-widgets/tui CELL-NATIVE widget set (every kind with a public constructor) into a scrollable cell-grid frame written to stdout as an ANSI stream.
tui-widgets renders the go-widgets/tui CELL-NATIVE widget set (every kind with a public constructor) into a scrollable cell-grid frame written to stdout as an ANSI stream.
Package syntax is a tiny, dependency-free syntax highlighter for the TUI previews.
Package syntax is a tiny, dependency-free syntax highlighter for the TUI previews.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL