# Overview > Install tui with go get (Go 1.25+), write a first Model with Init, Update and View, and see every package, its stability level and the supported platforms. Web: https://tui.nizaami.com/overview/ Source: https://github.com/ows4444/tui/blob/code/README.md A terminal UI framework for Go built on the Elm Architecture (Model / Update / View). It uses only the standard library: `go.mod` has no `require` directive, and a test in `internal/archtest` fails if any package imports outside the standard library and this module. An application implements `tui.Model`, hands it to `tui.NewProgram` with the options it wants, and calls `Run`. The Program reads keys, mouse events and resizes, calls `Update` on each Msg, runs the Cmds `Update` returns, and redraws `View` with a cell diff so only changed cells are written. Guides, a cookbook and screens of every example are at **[tui.nizaami.com](https://tui.nizaami.com)**; the API reference is on [pkg.go.dev](https://pkg.go.dev/github.com/ows4444/tui). ## Install Requires Go 1.25 or later. ```console $ go get github.com/ows4444/tui ``` There is no tagged release yet, so this resolves to a pseudo-version of the latest commit. ## Usage A Model is any value with `Init`, `Update` and `View`. This is `ExampleNewProgram` from [example_test.go](https://github.com/ows4444/tui/blob/code/example_test.go); it reads keys from a string and discards the output, so it runs without a terminal: ```go type counter struct{ n int } func (c counter) Init() tui.Cmd { return nil } func (c counter) Update(msg tui.Msg) (tui.Model, tui.Cmd) { if k, ok := msg.(tui.Key); ok && k.Type == tui.KeyRunes { switch k.Text { case "+": c.n++ case "q": return c, tui.Quit() } } return c, nil } func (c counter) View() string { return fmt.Sprintf("count: %d", c.n) } func ExampleNewProgram() { p := tui.NewProgram(counter{}, tui.WithInput(strings.NewReader("++q")), tui.WithOutput(io.Discard), ) final, err := p.Run() if err != nil { fmt.Println(err) return } fmt.Println(final.View()) // Output: count: 2 } ``` Against a real terminal, leave out `WithInput` and `WithOutput`: they default to `os.Stdin` and `os.Stdout`. [examples/counter](https://github.com/ows4444/tui/blob/code/examples/counter/main.go) does exactly that: ```console $ go run ./examples/counter ``` Every directory under [examples/](https://github.com/ows4444/tui/tree/code/examples) is a runnable program, for instance `./examples/dashboard`, `./examples/form`, `./examples/signup`, `./examples/agentshell` and `./examples/probe`. ## Packages API reference is the godoc of each package. The layering and stability levels below come from the package comment in [doc.go](https://github.com/ows4444/tui/blob/code/doc.go); a package may import only the layers above it, and `go test ./internal/archtest` fails on an import that points up. Stability levels: - **Core**: the primitives, codecs and `tuitest`. - **Stable-ish**: the render helpers and most components. - **Experimental**: packages whose own package comment says `Stability: experimental.` `go run ./internal/tools/doccheck` fails if that list and the list in `doc.go` disagree. `doc.go` does not assign a level to the root package `tui` or to `cellbuf`. ### Runtime | Package | Purpose | Stability | |---|---|---| | `tui` | The Program and its event loop; owns terminal I/O through `term` and `internal/termio`. Re-exports 14 `input` types as aliases (`Key`, `MouseEvent`, `PasteEvent`, ...), each as stable as the type it names | not stated | | `cellbuf` | A retained grid of terminal cells, for widgets that draw straight into cells | not stated | ### Primitives | Package | Purpose | Stability | |---|---|---| | `ansi` | Styles, colour, escape encoding, width, graphemes, wrapping | Core | | `layout` | A Node tree with flex, grid and overlay layout | Core | | `theme` | Colours, glyph sets and states as data | Core | | `term` | Raw mode and terminal size | Core | | `motion` | Springs, transitions and the reduced-motion preference | Core | ### Codecs and ports | Package | Purpose | Stability | |---|---|---| | `input` | Decodes terminal bytes into keys, mouse, paste and focus events | Core | | `keymap` | A registry of key bindings that help widgets read | Core | | `hittest` | Named click regions: which region a mouse event landed on | Core | ### Renderers and test harness | Package | Purpose | Stability | |---|---|---| | `widgets` | Stateless render helpers: Badge, Box, CodeBlock, ChatMessage, DiffView, ProgressBar, ... | Stable-ish | | `widgets/chart` | Sparkline, BarChart, LineChart, HeatMap, Gauge | Stable-ish | | `markdown` | A CommonMark subset rendered to styled, width-aware text | Stable-ish | | `focus` | A focus Ring over components, moved with Tab and Shift+Tab | Stable-ish | | `tuitest` | A headless harness that runs a model against a virtual screen | Core | ### Components One package per stateful widget, each a value-typed `Model` configured through exported fields. Every widget has a `LayoutNode()` adapter, and every widget implements `Linearize` for accessible mode; tests in `internal/archtest` and `internal/tools/doccheck` fail when one is missing. | Package | Purpose | Stability | |---|---|---| | `textinput` | Single-line text input | Stable-ish | | `textarea` | Multi-line text input | Stable-ish | | `passwordinput` | `textinput` that masks every character | Stable-ish | | `maskedinput` | `textinput` that masks with a configurable rune | Stable-ish | | `emailinput` | `textinput` that rejects whitespace | Stable-ish | | `numberinput` | `textinput` restricted to digits and a leading `-` | Stable-ish | | `taginput` | A list of short tags entered through a text input | Stable-ish | | `autocomplete` | Text input with a filtered suggestion dropdown | Stable-ish | | `form` | A column of labelled, validated single-line fields | Stable-ish | | `confirm` | Yes/no prompt | Stable-ish | | `picker` | Single-choice list | Stable-ish | | `multiselect` | Multi-choice list | Stable-ish | | `menu` | Nested-navigation list built on `picker` | Stable-ish | | `menubar` | Horizontal bar of titled dropdown menus | Stable-ish | | `contextmenu` | Popup menu opened at an anchor point | Stable-ish | | `datatable` | `widgets.Table` plus row navigation | Stable-ish | | `treeview` | Hierarchical expandable tree | Stable-ish | | `filepicker` | Filesystem browse-and-select | Stable-ish | | `datepicker` | Keyboard-navigable calendar on `time.Time` | Stable-ish | | `colorpicker` | Palette swatches plus hex input | Stable-ish | | `virtuallist` | Scrollable window onto a large uniform-height list | Stable-ish | | `viewport` | Scrollable window onto content taller than it | Stable-ish | | `logview` | Append-only scrolling log | Stable-ish | | `scrollbar` | Track and thumb showing the visible part of some content | Stable-ish | | `splitpane` | Two panes with a draggable divider | Stable-ish | | `tabs` | Horizontal tab bar | Stable-ish | | `accordion` | List of collapsible sections | Stable-ish | | `wizard` | Step navigation for multi-step flows | Stable-ish | | `dialog` | Modal box composited over the screen | Stable-ish | | `drawer` | Overlay anchored to an edge of the screen | Stable-ish | | `popover` | Overlay anchored near a point | Stable-ish | | `toast` | Transient auto-dismissing notification | Stable-ish | | `helpscreen` | Full-screen key-binding help overlay | Stable-ish | | `spinner` | Animated loading indicator | Stable-ish | | `loadingbar` | Indeterminate progress animation | Stable-ish | | `skeleton` | Loading placeholder block | Stable-ish | | `clockview` | Wall clock, stopwatch or countdown timer | Stable-ish | | `appshell` | Header, input, scrollable content and key-hints footer composed from existing widgets | Experimental | | `streamtext` | Text revealed a few characters at a time | Experimental | | `toolapproval` | Gate-before-execution prompt for an agent tool call | Experimental | | `notificationcenter` | Panel showing every queued notification at once | Experimental | | `commandpalette` | Text input with a fuzzy-filtered list of Commands | Experimental | | `errorretry` | An error with retry and dismiss keys | Experimental | | `faces` | 50 animated Braille characters and a widget that plays them | Experimental | | `imageview` | PNG through the kitty graphics protocol or Sixel, with a text placeholder otherwise | Experimental | | `clipboard` | A "copy to clipboard" button that writes OSC 52 | Experimental | Packages under `internal/` are not public API. ## Platforms and limitations - **Platforms.** Linux, macOS (darwin), Windows, FreeBSD, OpenBSD, NetBSD and DragonFly BSD. Any other GOOS, including solaris and illumos, is unsupported: the build fails on purpose with an import named `tui_unsupported_platform_this_GOOS_is_not_supported_see_README` ([platform_unsupported.go](https://github.com/ows4444/tui/blob/code/platform_unsupported.go)). CI runs the tests on Linux, macOS, Windows and FreeBSD; OpenBSD, NetBSD and DragonFly BSD are only cross-compiled (`TestBuildsOnSupportedPlatforms`). `WithSuspendOnCtrlZ` does nothing on Windows, where Ctrl+Z stays an ordinary key. - **Colour.** Without `WithColorProfile`, `NewProgram` detects the depth from the output and the environment (`ansi.DetectColorProfileFor`), in this order: `NO_COLOR` (non-empty) turns colour off; `CLICOLOR_FORCE` (non-empty, not `0`) colours output that is not a terminal; otherwise non-terminal output or `CLICOLOR=0` turns colour off; `TERM=dumb` turns colour off; `COLORTERM=truecolor` or `24bit`, `WT_SESSION`, and some `TERM_PROGRAM` values give 24-bit colour (`Apple_Terminal` gives 256); then the `TERM` name decides (an unrecognised name gets 16 colours; an empty `TERM` gets none, except 16 on Windows). A truecolor terminal that sets none of these (some SSH sessions, tmux with `TERM=screen`) is detected at a lower depth; pass `WithColorProfile(ansi.TrueColor)` to override. Passing `WithColorProfile` also overrides `NO_COLOR`. - **Unicode.** Width, truncation and trimming treat an extended grapheme cluster (a ZWJ emoji sequence, a flag, a letter with combining marks) as one unit, following UAX #29. Set `TUI_NO_CLUSTERS=1` for a terminal that draws the parts separately. The width, grapheme and bidi tables are from Unicode 17.0.0. Right-to-left text is reordered for display only with `WithBidi(true)`; it is off by default. - **Accessibility.** `WithAccessible(true)` switches to append-only, unstyled output and renders the root model's `Linearize` instead of `View` when it has one. `WithAccessibleAuto` turns it on for `ACCESSIBLE=1` or `TERM=dumb`, and `TERM=dumb` alone also turns it on unless `WithAltScreen` or `WithAccessible` was given. VoiceOver, NVDA and Orca are unverified: no screen-reader run is recorded for this repository. ## Terminal probe results [examples/probe](https://github.com/ows4444/tui/blob/code/examples/probe/main.go) reports what a terminal supports (colour depth, focus reporting, OSC 11 background detection) and leaves a `probe:` line in the scrollback. No probe run is recorded for any terminal yet, so every row is unverified. The middle column is what `ansi/profile.go` detects from the environment alone, not a measured result. | Terminal | Detected colour depth | Probe result | |---|---|---| | Windows Terminal (`WT_SESSION`) | 24-bit | unverified | | iTerm2 (`TERM_PROGRAM=iTerm.app`) | 24-bit | unverified | | WezTerm (`TERM_PROGRAM=WezTerm`) | 24-bit | unverified | | VS Code (`TERM_PROGRAM=vscode`) | 24-bit | unverified | | Ghostty (`TERM_PROGRAM=ghostty`) | 24-bit | unverified | | Hyper (`TERM_PROGRAM=Hyper`) | 24-bit | unverified | | kitty (`TERM=xterm-kitty`) | 24-bit | unverified | | Alacritty (`TERM=alacritty`) | 24-bit | unverified | | Apple Terminal (`TERM_PROGRAM=Apple_Terminal`) | 256 colours | unverified | ## Documentation - [docs/](https://tui.nizaami.com/overview/): topic guides, architecture and testing. - [CONTRIBUTING.md](https://tui.nizaami.com/contributing/): conventions and the checks CI runs. - [CHANGELOG.md](https://tui.nizaami.com/changelog/): user-visible changes. ## License MIT; see [LICENSE](https://github.com/ows4444/tui/blob/code/LICENSE). --- # Cookbook > Short recipes. Each is copied from an Example function, which go test runs and checks against its // Output: comment, so the code compiles and the output is real. Web: https://tui.nizaami.com/cookbook/ Source: https://github.com/ows4444/tui/blob/code/docs/cookbook.md Short recipes. Each is copied from an `Example` function, which `go test` runs and checks against its `// Output:` comment, so the code compiles and the output is real. The source is named under each recipe. In `layout/example_test.go`, `show` is a helper that prints each line with trailing spaces trimmed. Recipes on topic pages: [follow the terminal's light or dark background](https://tui.nizaami.com/theming/), [click regions from a layout](https://tui.nizaami.com/input/#mouse-and-hit-testing), [focus across mixed widgets](https://tui.nizaami.com/input/#focus-between-widgets), [a widget inside a sized layout](https://tui.nizaami.com/layout/#putting-a-widget-in-a-layout). ## Header and footer that stay put around a long body Wrap the body in `layout.Fill`. A plain `Grow: 1` child that measures taller than the screen pushes the footer out of view instead (`Example_growTrap`). ```go lines := make([]string, 100) for i := range lines { lines[i] = fmt.Sprintf("log %02d", i) } ui := layout.Column(0, layout.FlexChild{Node: layout.Block("== header ==")}, layout.Fill(layout.Block(strings.Join(lines, "\n"))), layout.FlexChild{Node: layout.Block("== footer ==")}, ) show(layout.Draw(ui, layout.Constraints{MinW: 12, MaxW: 12, MinH: 5, MaxH: 5})) // Output: // == header == // log 00 // log 01 // log 02 // == footer == ``` Source: `ExampleFill`, `layout/example_test.go`. ## Columns that line up across rows A zero `Track` sizes the column to its widest cell, `Grow` takes the leftover width, and `Size` fixes it. ```go g := layout.GridNode( []layout.Track{{}, {Grow: 1}, {Size: 3}}, 1, layout.Block("id"), layout.Block("name"), layout.Block("ok"), layout.Block("7"), layout.Block("widget"), layout.Block("no"), ) // MaxH must be set: a zero Max means "at most 0". show(layout.Draw(g, layout.Constraints{MinW: 20, MaxW: 20, MaxH: layout.Unbounded})) // Output: // id name ok // // 7 widget no ``` Source: `ExampleGridNode`, `layout/example_test.go`. For different column and row gaps, see `ExampleGridNodeGaps`. ## A different screen for narrow terminals ```go ui := layout.Responsive([]layout.Break{{MaxW: 20}}, layout.Block("menu"), layout.Block("home | search | settings")) fmt.Println(layout.Draw(ui, layout.Loose(layout.Size{W: 20, H: 3}))) fmt.Println(layout.Draw(ui, layout.Loose(layout.Size{W: 40, H: 3}))) // Output: // menu // home | search | settings ``` Source: `ExampleResponsive`, `layout/example_test.go`. The first node whose `Break` admits the available width and height is used. A node with no `Break` is the fallback for larger sizes. ## Tab order that follows the screen Number the fields in the order your model keeps them, label them with `layout.Named`, and let `focus.LayoutOrder` sort them by position. ```go cell := func(name string) layout.FlexChild { return layout.FlexChild{Node: layout.Named(name, layout.Block(name)), Basis: 1} } form := layout.Row(2, layout.FlexChild{Node: layout.Column(0, cell("name"), cell("email")), Basis: 8}, layout.FlexChild{Node: layout.Column(0, cell("city"), cell("submit")), Basis: 8}, ) names := []string{"name", "email", "city", "submit"} size := layout.Size{W: 20, H: 2} ring := focus.New(len(names)).WithOrder(focus.LayoutOrder(form, size, names...)) for range names { fmt.Print(names[ring.Current()], " ") ring = ring.Next() } fmt.Println() // Output: name city email submit ``` Source: `ExampleLayoutOrder`, `focus/order_test.go`. ## Scroll text in a fixed window ```go m := viewport.New(10, 3) m.SetContent(strings.Join([]string{"one", "two", "three", "four", "five"}, "\n")) fmt.Println(m.View()) m.LineDown(2) fmt.Println("--") fmt.Println(m.View()) // Output: // one // two // three // -- // three // four // five ``` Source: `Example`, `viewport/example_test.go`. ## A selectable table ```go m := datatable.New([]string{"name", "qty"}, [][]string{{"apple", "3"}, {"pear", "5"}, {"plum", "8"}}) m.Height = 3 m, _ = m.Update(tui.Key{Type: tui.KeyDown}) for _, line := range strings.Split(ansi.StripANSI(m.View()), "\n") { fmt.Println(strings.TrimRight(line, " ")) } fmt.Println("cursor row:", m.Cursor()) // Output: // name qty // ────────── // apple 3 // > pear 5 // plum 8 // cursor row: 1 ``` Source: `Example`, `datatable/example_test.go`. `examples/table` is the full program, confirming a row with Enter via `datatable.SelectedMsg`. ## A dialog and its dismissal message `dialog.New` returns an open dialog. Enter closes it and returns a Cmd whose message tells your `Update` it was dismissed. ```go d := dialog.New("Saved", "Your changes were saved.") fmt.Println(d.Open()) d, cmd := d.Update(tui.Key{Type: tui.KeyEnter}) fmt.Println(d.Open()) fmt.Printf("%T\n", cmd()) d.Show() fmt.Println(d.Open()) // Output: // true // false // dialog.DismissedMsg // true ``` Source: `Example`, `dialog/example_test.go`. To draw it over your screen, see [overlays](https://tui.nizaami.com/layout/#overlays-on-a-drawn-screen). ## Resize a split pane from the keyboard ```go m := splitpane.New(layout.Block("files"), layout.Block("preview")) m.SetTotal(21) m.Min1, m.Min2 = 4, 4 m, _ = m.Update(tui.Key{Type: tui.KeyRight, Mod: input.ModCtrl}) first, second := m.Sizes() fmt.Println(first, second) // Output: 11 9 ``` Source: `Example`, `splitpane/example_test.go`. By default, Ctrl+Right or Ctrl+Down grows the first pane, Ctrl+Left or Ctrl+Up shrinks it, and Ctrl+E centres the divider. ## Detect conflicting key bindings ```go var r keymap.Registry r.Add(keymap.NewBinding("quit", "q")) conflicts, _ := r.Add(keymap.NewBinding("close", "q")) fmt.Println(len(conflicts)) for _, h := range r.Hints("") { fmt.Println(h.Key, h.Desc) } // Output: // 1 // q quit // q close ``` Source: `Example`, `keymap/example_test.go`. The second binding is still registered. `Add` reports the conflict and returns a non-nil error. --- # Running a Program > tui.NewProgram wraps a Model in a Program, and Program.Run takes over the terminal, runs the event loop until the model quits, and restores the terminal before it returns. Web: https://tui.nizaami.com/program/ Source: https://github.com/ows4444/tui/blob/code/docs/program.md `tui.NewProgram` wraps a Model in a Program, and `Program.Run` takes over the terminal, runs the event loop until the model quits, and restores the terminal before it returns. Without options a Program reads `os.Stdin`, writes `os.Stdout`, draws on the alternate screen, and has bracketed paste on. ```go p := tui.NewProgram(counter{}, tui.WithInput(strings.NewReader("++q")), tui.WithOutput(io.Discard), ) final, err := p.Run() if err != nil { fmt.Println(err) return } fmt.Println(final.View()) // Output: count: 2 ``` (From `ExampleNewProgram` in `example_test.go`. With a `strings.Reader` as input, Run needs no terminal, and reaching EOF quits the Program.) A Program runs once. A second `Run` returns `ErrProgramReused` without touching the terminal; build a new Program instead. ## The Model A Model has three methods: - `Init() Cmd` runs once, before the first frame. - `Update(Msg) (Model, Cmd)` handles one message and returns the next model. - `View() string` returns the screen as text. ANSI styling is fine; cursor movement is not, because the renderer positions the cursor. At startup the Program calls, in order: `SetTheme` (only with `WithTheme` and a `Themeable` model), `Init`, then `Update` with a `ResizeMsg` holding the starting size, then draws the first frame. After that, each message goes to `Update` and the Program redraws if the View changed. See [rendering.md](https://tui.nizaami.com/rendering/) for what a redraw does. The loop doesn't handle Ctrl+C. Raw mode turns off the terminal's signal keys, so Ctrl+C reaches `Update` as a `Key` with `Type == tui.KeyCtrlC`, and the model has to return `tui.Quit()` itself. Key decoding is covered in [input.md](https://tui.nizaami.com/input/). A model can implement optional interfaces that the Program looks for: `CursorPlacer` or `CursorProvider` for the hardware cursor, `Linearizer` for accessible output ([accessibility.md](https://tui.nizaami.com/accessibility/)), `Themeable` for `WithTheme`, and `CellDrawer` for direct cell drawing. A model that wraps another model can implement `Unwrapper`, and the Program then looks for the cursor interfaces, `Linearizer` and `Themeable` on the wrapped model too. Themeable also needs the wrapper to implement `Rewrapper`. `CellDrawer` is used only on the root model itself. ## Messages the Program sends | Msg | When | | --- | --- | | `ResizeMsg` | Once at startup, on every resize, and again if a capability probe changes how the terminal measures text | | `Key`, `PasteEvent` | Key presses; a bracketed paste arrives as one `PasteEvent` unless `WithBracketedPaste(false)` | | `MouseEvent` | Only with `WithMouse` or after `EnableMouse` | | `FocusEvent` | Only with `WithFocusReporting(true)`; a repeat of the last state is dropped | | `KeyRepeatMsg`, `KeyReleaseMsg` | Only with `WithKeyboard(... \| KeyboardReportEvents)` | | `ChordMsg` | Only with `WithChords` | | `InputErrorMsg` | Once, if reading input fails. Input stops but the Program keeps running | | `SuspendMsg` | After a `Suspend` Cmd's function has returned | | `CapabilitiesMsg` | Once, with `WithCapabilityProbe` ([capabilities.md](https://tui.nizaami.com/capabilities/)) | | `BackgroundColorEvent`, `PaletteColorEvent`, `BackgroundUnknownMsg` | With `WithBackgroundDetection` or `WithTheme` | | `ClipboardMsg` | The answer to `ReadClipboard`, if the terminal answers | A `WithInput` reader that isn't a file quits the Program at `io.EOF`, after the events read before it. Any other read error is an `InputErrorMsg`. From outside the loop, `Program.Send` delivers a message as if it came from the terminal. It is safe from any goroutine. Before Run starts it queues the message, and after Run returns it drops it. The queue holds 64 messages, and `Send` blocks while it is full, so don't call `Send` from `Update` or `View`: use `TrySend`, which never blocks and reports whether the message was queued, or return a Cmd. `Program.Quit` sends a quit after the messages already queued. ## Commands A Cmd is a `func() Msg` that the Program runs on its own goroutine. Its result goes to `Update`. A nil Cmd does nothing. | Cmd | What it does | | --- | --- | | `Quit()` | Ends Run | | `Batch(cmds...)` | Runs the Cmds concurrently; their messages arrive in no particular order | | `Sequence(cmds...)` | Runs the Cmds one at a time and delivers their messages in order; stops after a `QuitMsg` | | `Tick(d, fn)` | Waits `d`, then sends `fn(t)`. The wait ends with no message if Run returns first | | `Every(d, fn)` | Returns a Cmd that sends `fn(t)` every `d`, plus a cancel func; no tick arrives after cancel returns | | `FromCtx(fn)`, `Go(fn)` | Run `fn` with `Program.Context`, which is cancelled when Run returns | | `Println(text)`, `Eprintln(text)` | Write text above the live region, into scrollback (see below) | | `Suspend(fn)` | Hands the terminal back while `fn` runs (see below) | | `EnableMouse(mode)`, `EnterAltScreen()`, `ExitAltScreen()`, `ClearScreen()` | Change a terminal mode while running | | `SetWindowTitle(s)`, `SetCursorShape(s)` | Both are undone on every exit path | | `ReadClipboard()` | Asks the terminal for the clipboard over OSC 52; many terminals refuse, so no reply may come | | `Announce(text)`, `AnnounceWith(text, level)` | See [accessibility.md](https://tui.nizaami.com/accessibility/) | `WithMaxConcurrentCmds(n)` caps how many Cmds run at once; queued Cmds start in the order the loop received them. Sequence steps and Every timers aren't counted against the cap. A Cmd made by `Tick`, `FromCtx` or a `motion` wait does nothing useful when you call it directly; it returns an internal message the Program runs. In a test, run it with `tui.RunCmd`: ```go cmd := tui.Tick(time.Millisecond, func(time.Time) tui.Msg { return "tick" }) fmt.Println(tui.RunCmd(context.Background(), cmd)) // Output: tick ``` (From `ExampleTick` in `example_test.go`.) `RunCmd` doesn't expand `Batch` or `Sequence`. ## Options Every option is a `ProgramOption` passed to `NewProgram`; their godoc has the details. | Option | Default | | --- | --- | | `WithAltScreen(bool)` | on | | `WithInput(r)`, `WithInputCloser(c)` | `os.Stdin` | | `WithOutput(w)`, `WithErrOutput(w)` | `os.Stdout`, `os.Stderr` | | `WithMouse(mode)` | `MouseOff` | | `WithBracketedPaste(bool)` | on | | `WithFocusReporting(bool)` | off | | `WithKittyKeyboard(bool)`, `WithKeyboard(flags)` | off | | `WithChords(defs...)`, `WithChordTimeout(d)` | no chords; 500ms timeout | | `WithEscTimeout(d)` | `input.DefaultEscTimeout` (30ms) | | `WithMaxFPS(fps)` | 60 fps cap on non-input messages; input and resizes draw at once | | `WithMaxConcurrentCmds(n)` | no limit | | `WithContext(ctx)` | `context.Background()` | | `WithClock(c)` | wall clock, for `Every` | | `WithExitOnSignal(bool)` | on | | `WithRecover(bool)` | off | | `WithSuspendOnCtrlZ(bool)` | off | | `WithTerminal(t)` | the OS terminal behind input and output | | `WithResizePoll(d)` | 250ms, Windows only | | `WithTheme(auto)`, `WithBackgroundDetection(d)` | off; see [theming.md](https://tui.nizaami.com/theming/) | | `WithColorProfile(p)`, `WithCapabilityProbe(d)` | detected from the environment; probe off. See [capabilities.md](https://tui.nizaami.com/capabilities/) | | `WithAccessible(bool)`, `WithAccessibleAuto()`, `WithAnnounceRegion(rows)`, `WithLinearizeFullLine()`, `WithReducedMotion(bool)` | See [accessibility.md](https://tui.nizaami.com/accessibility/) | | `WithCellRenderer(bool)`, `WithBidi(bool)`, `WithFrameLog(w)`, `WithInspector(keys)`, `WithRecorder(w)`, `WithRecorderSidecar(w)` | See [rendering.md](https://tui.nizaami.com/rendering/) | Options apply in order, so when two set the same value the later one wins (`WithChords` adds to the chords instead). Accessible mode is the exception: once it is on, `NewProgram` turns the alternate screen off and reduced motion on after every option has run, so no option order can undo that. ## Inline or alternate screen On the alternate screen (the default) the app gets a full screen of its own, and the user's shell history comes back when it quits. Under `TERM=dumb`, a Program given neither `WithAltScreen` nor `WithAccessible` runs in accessible mode instead, which never uses the alternate screen ([accessibility.md](https://tui.nizaami.com/accessibility/)). A View taller than the terminal is cut to the terminal's height. With `WithAltScreen(false)` the Program draws inline, below the shell prompt. The live region is as tall as the View. When the View grows taller than the terminal, the rows that no longer fit are written into scrollback once and are not redrawn after that. On exit the last frame stays on screen, and the Program moves to a new line so the prompt starts below it. `Println` and `Eprintln` write lines above the live region, into scrollback, and redraw the live region below them. On the alternate screen there is no scrollback, so the Program holds those lines (up to 10,000; the oldest are dropped beyond that) and writes them once it leaves the alternate screen, with `ExitAltScreen` or on exit. `examples/inlinebuild` is an inline program that commits finished lines with `Println`. ## Suspend and resume `Suspend(fn)` undoes the terminal setup (modes off, raw mode off), stops reading input so `fn` owns stdin, runs `fn`, then puts everything back and repaints the whole frame. Use it to run an editor or a shell. `fn` starts and waits for the external program itself. Its error reaches `Update` as `SuspendMsg.Err`, and the Program keeps running either way. `WithSuspendOnCtrlZ(true)` makes Ctrl+Z stop the process as a shell job does. The Program restores the terminal, stops itself with SIGSTOP, and on `fg` re-enters raw mode and every mode, re-reads the size (sending a `ResizeMsg` if it changed), and repaints. `Update` sees neither the Ctrl+Z nor a `SuspendMsg`. Without the option, or on Windows, Ctrl+Z is an ordinary `Key`. ## Signals, panics and terminal restore Run restores the terminal on every return path: alternate screen, cursor, mouse, bracketed paste, keyboard and focus modes, the window title and cursor shape, and raw mode. SIGTERM and SIGHUP terminate a Go process without running deferred functions, so Run catches them. It restores the terminal and cancels `Program.Context`, then by default exits with status 1. With `WithExitOnSignal(false)`, Run instead returns `ErrInterrupted`, so the app can run its own shutdown. A panic in `Init`, `Update` or `View` restores the terminal and then continues as a panic. A panic in a Cmd's goroutine restores the terminal (waiting up to 500ms for the loop to do it, then writing a fixed restore sequence) and re-panics with the same value. With `WithRecover(true)`, Run returns a `*PanicError` holding the value and stack for a panic in `Init`, `Update`, `View` or any Cmd: a plain Cmd, a `Tick` or `FromCtx` Cmd, a `Sequence` step or an `Every` callback (`recover_test.go` covers each). ## Context and cancellation `Program.Context` is valid as soon as `NewProgram` returns, so you can pass it to the model before Run. Run cancels it on every return path, as does cancelling the parent given to `WithContext`. Cmds made by `Tick`, `FromCtx` and `Go` receive this context. A Cmd that does I/O can use the context to stop early instead of outliving Run. ## Other terminals `WithTerminal` replaces the terminal the Program controls (size, raw mode, output VT processing) with your own `Terminal`. Use it with `WithInput` and `WithOutput` for a remote session such as SSH. A `Terminal` that also implements `ResizeNotifier` delivers resizes through the same port. The headless test harness is described in [testing.md](https://tui.nizaami.com/testing/). --- # Layout > Lay out a Go terminal UI with layout.Node: two-pass measure and render, rows, columns, flex, grids, overlays, and how to place widgets in a layout. Web: https://tui.nizaami.com/layout/ Source: https://github.com/ows4444/tui/blob/code/docs/layout.md A [`layout.Node`](https://pkg.go.dev/github.com/ows4444/tui/layout#Node) is sized in two passes: `Measure` reports the size the node wants under some `Constraints`, then `Render` is handed the final `Size` and must return exactly that many columns and rows. `layout.Draw` runs both passes and returns the string your `View` returns. Watch the zero value: `Constraints{}` bounds both axes to 0, so it draws nothing. Use `layout.Unconstrained()`, `layout.Loose`, or set `MaxW`/`MaxH` to `layout.Unbounded`. ```go ui := layout.Column(0, layout.FlexChild{Node: layout.BoxNode(layout.NewBox().Border(layout.NormalBorder()), layout.Block("title"))}, layout.FlexChild{Grow: 1, Node: layout.Row(1, layout.FlexChild{Node: layout.Block("nav"), Basis: 5}, layout.FlexChild{Node: layout.Block("body"), Grow: 1}, )}, layout.FlexChild{Node: layout.Block("q: quit")}, ) show(layout.Draw(ui, layout.Constraints{MinW: 24, MaxW: 24, MinH: 6, MaxH: 6})) // Output: // ┌──────────────────────┐ // │title │ // └──────────────────────┘ // nav body // // q: quit ``` From `Example_measureRender` in `layout/example_test.go`. `show` is that file's helper: it prints each line with trailing spaces trimmed. ## Drawing a screen in View Most programs in `examples/` build the screen as a `layout.Node` in a helper and draw it in `View`: ```go func (m model) View() string { return layout.Draw(m.screen(), layout.Unconstrained()) } ``` From `examples/focus/main.go` (the same line appears in `examples/pager`, `examples/login`, `examples/procstream` and others). `Draw` renders at the measured size. To fill the terminal exactly, keep the `Width` and `Height` of the last `tui.ResizeMsg` in your model and call `layout.DrawTight(root, layout.Size{W: w, H: h})`: the root is rendered at exactly that size, so `Fill` children take all the remaining space. `examples/dashboard` caps only the width: `layout.Constraints{MaxW: maxW, MaxH: layout.Unbounded}`. `Draw` always returns a rectangle: every row is padded to the widest one (`ExampleDraw_rectangle`). Compare drawn output by content, not raw width. ## Sizing children of a Row or Column `Row` and `Column` take a gap and a list of [`FlexChild`](https://pkg.go.dev/github.com/ows4444/tui/layout#FlexChild) values. The zero `FlexChild` is sized to its content and neither grows nor shrinks. `Basis`, `BasisLen` (`Pct`, `Fr`), `Grow`, `Shrink`, `Min`, `Max` and `CrossAlign` are documented on the type. The behaviours that catch people: - **A growing child that measures taller than the screen pushes its siblings out of view.** A viewport or a long log measures to its full content. Wrap it in `layout.Fill` (Basis 1, Grow 1, Shrink 1) instead of setting `Grow: 1`. Compare `ExampleFill`, which keeps the footer, with `Example_growTrap`, which loses it. `FillWeight` splits leftover space unevenly. - **Without a constrained height, give a scrolling child a `Basis`.** It is the window height (`ExampleColumn_window`). - **Use the gap for blank rows, not `Block("")`.** An empty `Block` is 0x0 (`ExampleColumn_gap`). - **Children are stretched across the cross axis by default.** The zero `CrossAlign` is `CrossStretch`: a short box beside a tall one in a `Row` grows to the row's height, and one long line in a `Column` widens a bordered box above it. `CrossStart`, `CrossCenter` and `CrossEnd` keep the child's own cross size (`ExampleCrossAlign`, `ExampleColumn_crossStart`). - **Measure may run more than once.** It must be side-effect free. Within one `Draw`, the built-in containers measure each child at most once per distinct `Constraints`. `RowJustify` and `ColumnJustify` add main-axis spacing (`JustifyStart`, `JustifyEnd`, `JustifyCenter`, `JustifySpaceBetween`, `JustifySpaceAround`, `JustifySpaceEvenly`). ## Putting a widget in a layout Every component package's `Model` has a `LayoutNode` method. `TestEveryModelHasLayoutNodeOrIsAllowlisted` (`layoutnode_conformance_test.go`) fails if a package with a `Model` lacks one, and its allowlist is empty. `wizard.Model.LayoutNode` and `markdown.Model.LayoutNode` take a `theme.Theme`. All the others take no arguments. A widget's node is not the same picture as its `View`. The node takes whatever size the layout gives it, and does not change the Model. A `textinput.Model`'s `Width` limits its `View`, but its node measures the whole value and is cut to the width the layout allots: ```go in := textinput.New() in.Prompt = "Name: " in.Width = 12 in.SetValue("a value much longer than the width") fmt.Println("View width:", ansi.Width(in.View())) fmt.Println("node width:", ansi.Width(layout.Draw(in.LayoutNode(), layout.Unconstrained()))) fixed := layout.Row(0, layout.FlexChild{Node: in.LayoutNode(), Basis: 20}) fmt.Println("node in a Basis-20 Row:", ansi.Width(layout.Draw(fixed, layout.Unconstrained()))) // Output: // View width: 18 // node width: 41 // node in a Basis-20 Row: 20 ``` From `Example_widgetNodeIsNotItsView` in `layout/example_test.go`. For anything that is already a string, such as the output of a `widgets` function, use `layout.Block` (`widgets.Node` is the same adapter). `layout.Block` pads or clips the string to the allotted size without splitting a rune or an escape sequence. For a view that changes on its own, such as a clock, use `layout.ViewFunc`, which calls the function each time it is measured or rendered. ## Choosing a node | Need | Node | | --- | --- | | Columns that line up across rows | `GridNode`, `GridNodeGaps` with `[]Track` (`ExampleGridNode`) | | Different trees for different terminal sizes | `Responsive` with `[]Break` (`ExampleResponsive`) | | A "terminal too small" screen | `MinSize(n, min, fallback)` | | An exact or bounded size | `Fixed` (`ExampleFixed`), `MinMax` | | Border, padding, title, background | `BoxNode(layout.NewBox()..., child)` | | Wrapping or styled text | `Text` with `WithWrap`, `WithAlign`, `WithEllipsis`, `WithStyle`; `StyledText` | | A scrolled window onto a tall child | `Scroll(child, offset)`. Rows wrapped in `Sticky` pin to the top. A child implementing `Windowed` renders only the visible rows | | Layers | `Stack` with `Layer(z, n)` and `Absolute(x, y, n)`, or `OverlayNode(base, over, x, y)` | Pre-rendered strings have their own helpers: `layout.Overlay(base, overlay, x, y)`, `JoinHorizontalJustify`, and `Window`/`WindowRange` for keeping a cursor line in view. `Box.Render` is deprecated in favour of `BoxNode`. ## Overlays on a drawn screen The overlay widgets (`dialog`, `drawer`, `popover`, `toast`, `helpscreen`, `contextmenu`, `menubar`) implement `tui.Overlay`: `Render(base string) string` composites them onto an already drawn frame. Draw the screen first, then layer them: ```go base := layout.Draw(m.screen(), layout.Constraints{MaxW: maxW, MaxH: layout.Unbounded}) return m.notice.Render(m.about.Render(base)) ``` From `View` in `examples/dashboard/main.go`, where `about` is a `dialog.Model` and `notice` a `toast.Model`. ## Finding where a node was drawn Wrap a node in `layout.Named(name, n)`; layout is unchanged. Then: - `layout.Rects(root, size)` lists every node with its rectangle, parent before children. - `layout.RectOf(root, size, name)` returns one rectangle. - `layout.NamedAt(root, size, x, y)` returns the innermost named node at a cell. Pass the size the root was rendered at. These feed mouse hit-testing and tab order; see [Input](https://tui.nizaami.com/input/#mouse-and-hit-testing). ## Drawing into cells A node that also implements `layout.CellNode` draws straight into a `layout.CellSurface` with `layout.DrawTo`, without building a string. `cellbuf.Layout` adapts a `cellbuf.Buffer`. See [Rendering](https://tui.nizaami.com/rendering/). --- # Widgets > Stateless widget functions and stateful component packages for Go terminal UIs: the shared contracts, and a catalog of every component with its example program. Web: https://tui.nizaami.com/widgets/ Source: https://github.com/ows4444/tui/blob/code/docs/widgets.md Widgets come in two shapes. The functions in [`widgets`](https://pkg.go.dev/github.com/ows4444/tui/widgets) and [`widgets/chart`](https://pkg.go.dev/github.com/ows4444/tui/widgets/chart) are stateless: each takes its data (and usually a `theme.Theme`) and returns a styled string to place in a view. Anything with state, such as a cursor, a scroll offset or an open/closed flag, is a component in its own top-level package, with a value-typed `Model`, a `New` constructor, and `Update` and `View` methods. ```go fmt.Println(ansi.StripANSI(widgets.Box("Status", "all good", theme.DarkTheme(), 20))) // Output: // ┌──────────────────┐ // │ │ // │ Status │ // │ all good │ // │ │ // └──────────────────┘ ``` From `ExampleBox` in `widgets/example_test.go`. ## The widget contract A component is embedded in your own model. You hold its `Model` in a field, pass messages to its `Update`, keep the Model it returns, and draw its `View` or `LayoutNode`. It has no `Init`. The root package names these shapes, and packages assert them at compile time with lines such as `var _ tui.Component[Model] = Model{}`: | Contract | Shape | Asserted by | | --- | --- | --- | | `tui.Component[T]` | `Update(tui.Msg) (T, tui.Cmd)`, `View() string` | accordion, appshell, autocomplete, clipboard, clockview, colorpicker, commandpalette, confirm, datatable, datepicker, emailinput, errorretry, faces, filepicker, form, loadingbar, logview, maskedinput, menu, multiselect, numberinput, passwordinput, picker, skeleton, spinner, streamtext, tabs, taginput, textarea, textinput, toolapproval, treeview, viewport, virtuallist | | `tui.Overlay[T]` | `Open() bool`, `Update`, `Render(base string) string`. It composites onto a drawn frame. `Show` and `Hide` are pointer methods outside the interface | contextmenu, dialog, drawer, helpscreen, menubar, popover, toast | | `tui.ThemeSetter[T]` | `SetTheme(theme.Theme) T` | every component except clockview, logview, markdown, viewport and wizard; see [Theming](https://tui.nizaami.com/theming/) | | `tui.Linearizer` | `Linearize() string`, plain text for accessible output | every component has the method; most assert it. See [Accessibility](https://tui.nizaami.com/accessibility/) | | `tui.CursorProvider` | `CursorCell() (x, y int, ok bool)` | textarea, textinput | Every component `Model` also has `LayoutNode()`, a test enforces it, and most have `Tokens()` and `WithTokens()`. Some packages implement a contract's methods without the assertion: `scrollbar` and `splitpane` have `Update`/`View`, and `notificationcenter` has `Render(base string)` with no `Update`. `wizard.Model` and `markdown.Model` take the theme as an argument to `View` and `LayoutNode`. For layout see [Layout](https://tui.nizaami.com/layout/). For keys, focus and the `KeyMap` convention see [Input](https://tui.nizaami.com/input/). ## Component packages "Keys" marks packages with a `KeyMap` field, `DefaultKeyMap()` and `Bindings()`. "Experimental" packages say so in their package comment, and their API may change in any minor release. The example column names a program in `examples/` that imports the package. | Package | Purpose (from the package comment) | Keys | Example program | | --- | --- | --- | --- | | [accordion](https://github.com/ows4444/tui/tree/code/accordion) | A list of collapsible sections | yes | `examples/settings` | | [appshell](https://github.com/ows4444/tui/tree/code/appshell) | Header, full-width input, scrollable content and optional key-hints footer, composed from existing widgets. Experimental | | | | [autocomplete](https://github.com/ows4444/tui/tree/code/autocomplete) | A text input with a filtered suggestion dropdown | yes | `examples/form` | | [clipboard](https://github.com/ows4444/tui/tree/code/clipboard) | A "copy to clipboard" button that writes OSC 52. Experimental | | | | [clockview](https://github.com/ows4444/tui/tree/code/clockview) | A wall-clock, stopwatch or countdown timer | | | | [colorpicker](https://github.com/ows4444/tui/tree/code/colorpicker) | A palette-swatch and hex-input colour picker | yes | | | [commandpalette](https://github.com/ows4444/tui/tree/code/commandpalette) | A text input with a fuzzy-filtered dropdown of Commands ("Ctrl+K" style). Experimental | yes | | | [confirm](https://github.com/ows4444/tui/tree/code/confirm) | A yes/no prompt | yes | `examples/form` | | [contextmenu](https://github.com/ows4444/tui/tree/code/contextmenu) | A popup menu opened at an anchor point | yes | | | [datatable](https://github.com/ows4444/tui/tree/code/datatable) | `widgets.Table` plus row navigation | yes | `examples/table`, `examples/inspector` | | [datepicker](https://github.com/ows4444/tui/tree/code/datepicker) | A keyboard-navigable calendar on `time.Time` | yes | | | [dialog](https://github.com/ows4444/tui/tree/code/dialog) | A modal box with title and message, composited over the screen, dismissed with Enter/Esc | yes | `examples/dashboard` | | [drawer](https://github.com/ows4444/tui/tree/code/drawer) | An overlay anchored to an edge of the base view | yes | | | [emailinput](https://github.com/ows4444/tui/tree/code/emailinput) | A `textinput.Model` wrapper that rejects whitespace | | | | [errorretry](https://github.com/ows4444/tui/tree/code/errorretry) | An error with retry (Enter or `r`, up to `MaxRetries`) and dismiss (Esc). Experimental | yes | | | [faces](https://github.com/ows4444/tui/tree/code/faces) | A catalog of 50 animated Braille characters and a widget that plays them. Experimental | | `examples/faces` | | [filepicker](https://github.com/ows4444/tui/tree/code/filepicker) | A filesystem browser, one directory at a time | yes | | | [form](https://github.com/ows4444/tui/tree/code/form) | A validating column of labelled fields with Submit | yes | `examples/login`, `examples/signup` | | [helpscreen](https://github.com/ows4444/tui/tree/code/helpscreen) | A full-screen key-binding help overlay | | | | [imageview](https://github.com/ows4444/tui/tree/code/imageview) | A PNG drawn with the kitty graphics protocol or Sixel, or a text placeholder. Experimental | | | | [loadingbar](https://github.com/ows4444/tui/tree/code/loadingbar) | An indeterminate progress animation | | `examples/dashboard` | | [logview](https://github.com/ows4444/tui/tree/code/logview) | An append-only scrolling log | | `examples/procstream` | | [markdown](https://github.com/ows4444/tui/tree/code/markdown) | A CommonMark subset rendered as styled, width-aware text | | `examples/chat`, `examples/agentshell` | | [maskedinput](https://github.com/ows4444/tui/tree/code/maskedinput) | A `textinput.Model` wrapper that masks each character with a configurable rune | | | | [menu](https://github.com/ows4444/tui/tree/code/menu) | Nested-navigation list on top of `picker.Model` | yes | | | [menubar](https://github.com/ows4444/tui/tree/code/menubar) | A horizontal bar of titled dropdown menus | yes | | | [multiselect](https://github.com/ows4444/tui/tree/code/multiselect) | A multi-choice list: Space toggles, Enter confirms | yes | `examples/list` | | [notificationcenter](https://github.com/ows4444/tui/tree/code/notificationcenter) | A panel showing every queued notification at once. Experimental | | | | [numberinput](https://github.com/ows4444/tui/tree/code/numberinput) | A `textinput.Model` wrapper that accepts digits and one leading `-` | | | | [passwordinput](https://github.com/ows4444/tui/tree/code/passwordinput) | A `textinput.Model` wrapper that masks the value | | `examples/focus` | | [picker](https://github.com/ows4444/tui/tree/code/picker) | A single-choice list (InkUI's "Select") | yes | `examples/loginflow`, `examples/router`, `examples/setupflow` | | [popover](https://github.com/ows4444/tui/tree/code/popover) | An overlay anchored near a point | yes | | | [scrollbar](https://github.com/ows4444/tui/tree/code/scrollbar) | A track and thumb showing how much content is visible and where | yes | | | [skeleton](https://github.com/ows4444/tui/tree/code/skeleton) | A loading placeholder block | | | | [spinner](https://github.com/ows4444/tui/tree/code/spinner) | An animated loading indicator | | `examples/asyncload`, `examples/buildlog`, `examples/inlinespinners` | | [splitpane](https://github.com/ows4444/tui/tree/code/splitpane) | Two `layout.Node`s with a divider moved by keyboard or mouse | yes | | | [streamtext](https://github.com/ows4444/tui/tree/code/streamtext) | Text revealed a few characters at a time (`New` streams, `NewTypewriter` types). Experimental | | `examples/chat`, `examples/agentshell` | | [tabs](https://github.com/ows4444/tui/tree/code/tabs) | A horizontal tab bar | yes | `examples/settings`, `examples/inspector` | | [taginput](https://github.com/ows4444/tui/tree/code/taginput) | A text input plus a list of committed tags drawn as `widgets.Tag` chips | yes | | | [textarea](https://github.com/ows4444/tui/tree/code/textarea) | A multi-line text input | yes | `examples/focus`, `examples/agentshell` | | [textinput](https://github.com/ows4444/tui/tree/code/textinput) | A single-line text input | yes | `examples/focus`, `examples/cursorfield`, `examples/form` | | [toast](https://github.com/ows4444/tui/tree/code/toast) | A transient notification in a screen corner that closes after `Duration` | | `examples/dashboard` | | [toolapproval](https://github.com/ows4444/tui/tree/code/toolapproval) | A gate-before-execution prompt for an agent tool call. Experimental | yes | `examples/agentshell` | | [treeview](https://github.com/ows4444/tui/tree/code/treeview) | A hierarchical expandable tree | yes | `examples/inspector` | | [viewport](https://github.com/ows4444/tui/tree/code/viewport) | A scrollable window onto content taller than it | yes | `examples/pager` | | [virtuallist](https://github.com/ows4444/tui/tree/code/virtuallist) | A scrolling window onto a large uniform-height list that never builds off-screen rows | yes | | | [wizard](https://github.com/ows4444/tui/tree/code/wizard) | Step navigation for multi-step flows, drawn with `widgets.Stepper` | | `examples/form` | Every package listed has an `Example` in its `example_test.go` (`TestEveryPackageHasExample` in `exampleevery_test.go` requires one per package). ## The widgets package Everything in `widgets` is a pure function of its arguments that returns a string. It needs no `Msg` handling and never imports `tui` or a component package: it sits below the components. Use `layout.Block(s)` (or `widgets.Node(s)`, the same adapter) to place the result in a layout. | Kind | Functions | | --- | --- | | Containers and labels | `Alert`, `Badge`, `Banner`, `Box`, `Card`, `InfoBox`, `Panel`, `Tag`, `Tooltip`, `TooltipOverlay`, `Center`, `Spacer` | | Separators and headings | `Divider`, `DividerLabel`, `DividerWith`, `DividerLabelWith`, `Header`, `HeaderWithAccessory`, `Breadcrumb` | | Progress and status | `ProgressBar`, `ProgressCircle`, `MultiProgress`, `StatusIndicator`, `Stepper`, `Pagination`, `PaginationDots` | | Controls drawn as text | `Checkbox`, `Toggle`, `FormField`, `Form` | | Lists and tables | `List`, `ListWith`, `KeyValue`, `Table`, `TableRows`, `TableRowsWith` | | Key hints | `KeyHint`, `KeyHints`, `HintsFromKeymap` | | Text formatting | `CodeBlock`, `CodeBlockLang`, `DiffView`, `Gradient`, `BigText`, `BigTextGradient`, `Link` | | Chat and agent helpers | `ChatMessage`, `ChatMessageRaw`, `TokenCounter`, `UsageMonitor`, `CompactCount` | | Error isolation | `ErrorBoundary` renders a fallback if the render function panics | `widgets/chart` holds the data plots: `Sparkline`, `SparklineWith`, `BarChart`, `LineChart`, `HeatMap` and `Gauge`, each with a `Linearize...` function for accessible output. The functions of the same names in `widgets` are deprecated wrappers for the chart versions. `examples/chat` and `examples/dashboard` use `widgets/chart`. `examples/welcomescreen`, `examples/splashscreen`, `examples/loginflow` and `examples/setupflow` build whole screens from `widgets` functions and a `layout.Row`. --- # Input, key bindings, focus and mouse > The Program decodes terminal bytes with an input.Reader and passes each event to Update as a message: tui.Key, tui.MouseEvent, tui.PasteEvent, tui.FocusEvent, and the others listed below. Web: https://tui.nizaami.com/input/ Source: https://github.com/ows4444/tui/blob/code/docs/input.md The Program decodes terminal bytes with an [`input.Reader`](https://pkg.go.dev/github.com/ows4444/tui/input#Reader) and passes each event to `Update` as a message: `tui.Key`, `tui.MouseEvent`, `tui.PasteEvent`, `tui.FocusEvent`, and the others listed below. These root types are aliases of the `input` types, so `tui.Key` and `input.Key` are the same type. The `KeyType`, `MouseButton` and `MouseAction` constants are re-exported too (`tui.KeyEnter`, `tui.MouseButtonLeft`). The modifier bits are not: use `input.ModCtrl`, `input.ModShift` and so on. ```go func (c counter) Update(msg tui.Msg) (tui.Model, tui.Cmd) { if k, ok := msg.(tui.Key); ok && k.Type == tui.KeyRunes { switch k.Text { case "+": c.n++ case "q": return c, tui.Quit() } } return c, nil } ``` From the `counter` model of `ExampleNewProgram` in `example_test.go`. Program options (`WithMouse`, `WithKeyboard`, ...) are covered in [Program](https://tui.nizaami.com/program/). This page covers handling the events. ## Keys A [`Key`](https://pkg.go.dev/github.com/ows4444/tui/input#Key) has a `Type`. Printable input is `KeyRunes`, with the typed text in `Key.Text`. Ctrl+C is `KeyCtrlC`. Other Ctrl+letter keys are `KeyCtrl`, with the letter in `Key.Code`. Modifiers on arrows, Home/End, PgUp/PgDn and Alt+rune arrive in `Key.Mod`. `Key.String()` gives the name used everywhere else in the library (`"a"`, `"up"`, `"ctrl+right"`, `"enter"`): ```go rd := input.NewReader(strings.NewReader("a\x1b[A\x1b[1;5C")) for { ev, err := rd.ReadEvent() if err != nil { break } fmt.Println(ev.(input.Key)) } // Output: // a // up // ctrl+right ``` From `ExampleReader` in `input/example_test.go`. The Reader is a plain decoder over an `io.Reader`, so it needs no TTY. A lone ESC waits `input.DefaultEscTimeout` (30ms) for the rest of a sequence before it becomes `KeyEsc`. Change this with `tui.WithEscTimeout`. The legacy encoding cannot tell Shift+Enter from Enter. `tui.WithKittyKeyboard(true)` turns on the kitty keyboard protocol's "disambiguate" flag, and terminals without the protocol ignore it. `tui.WithKeyboard(tui.KeyboardReportEvents)` also reports repeats and releases. They arrive as `tui.KeyRepeatMsg` and `tui.KeyReleaseMsg`, never as `Key`, so `case tui.Key` still sees only presses. ## Paste and terminal focus Bracketed paste is on by default. A paste arrives as one `tui.PasteEvent` with the whole text, not as keys, so a pasted newline is not an Enter press. One event holds at most `input.MaxPasteBytes` (16 MiB). `Truncated` is set if the paste was longer. If the end marker does not arrive within `input.PasteIdleTimeout` (250ms) of the last byte, the event carries what was received and `Incomplete` is set. `tui.WithBracketedPaste(false)` turns this off. `tui.WithFocusReporting(true)` sends a `tui.FocusEvent{Focused: bool}` when the terminal window gains or loses focus. Only changes are delivered. This is the window's focus, not focus between widgets, which is covered [below](#focus-between-widgets). ## Key bindings [`keymap`](https://pkg.go.dev/github.com/ows4444/tui/keymap) keeps a widget's keys and its help text in one place. A `keymap.Binding` is a description plus key names (the `Key.String()` forms). `keymap.Matches(msg, b)` reports whether `msg` is a press of one of them. Releases and disabled bindings never match. Repeats count as presses. Widgets that react to keys follow one convention. The package has a `KeyMap` struct with one `Binding` per action, `DefaultKeyMap()`, a `KeyMap` field on `Model`, and a `Bindings()` method that returns the bindings in effect. To rebind a widget, change its field, for example `m.KeyMap.Down`. The behaviour and the help text change together. The packages that follow it are marked in the [widget catalog](https://tui.nizaami.com/widgets/#component-packages). A `keymap.Registry` collects bindings for help screens and reports two actions bound to the same key in one scope: ```go var r keymap.Registry r.Add(keymap.NewBinding("quit", "q")) conflicts, _ := r.Add(keymap.NewBinding("close", "q")) fmt.Println(len(conflicts)) for _, h := range r.Hints("") { fmt.Println(h.Key, h.Desc) } // Output: // 1 // q quit // q close ``` From `Example` in `keymap/example_test.go`. To build help from what is actually bound, implement `tui.BindingsProvider` on your root model by returning the focused widget's `Bindings()` and your own. `(*tui.Program).Keymap()` then returns a fresh `Registry` built from it, as of the last frame drawn. Pass that registry to `helpscreen.FromRegistry` or `widgets.HintsFromKeymap`. Build the help screen when it opens, so it follows the focus. ## Chords `tui.WithChords(defs...)` recognises multi-key sequences such as `g g` or `ctrl+x ctrl+s`. Each is a `tui.ChordDef{Name, Keys}`, with `Keys` in `Key.String()` form. When the keys complete a chord, `Update` receives one `tui.ChordMsg{Name}` instead of the keys. A key that cannot start or continue a chord is delivered at once. A prefix that turns out not to be a chord is delivered as its original keys, in order. So is a prefix still waiting when the timeout passes, even if no other key is typed. A key that can start a chord is therefore held back for up to the timeout: `input.DefaultChordTimeout` (500ms), changed with `tui.WithChordTimeout`. `chords_test.go` covers each case. Outside a Program, `input.NewChordMatcher` does the same matching. You call `Feed` for each `Key` and `Expire` from a timer. The repository has no Example for chords. ## Focus between widgets A [`focus.Ring`](https://pkg.go.dev/github.com/ows4444/tui/focus#Ring) tracks which of n items has focus. Tab and Shift+Tab move it, wrapping at the ends and skipping disabled items. Like the widget Models it is a value: every method returns a new Ring. `Ring.Route` does the whole dispatch. On Tab and Shift+Tab it blurs the old item and focuses the new one. It sends every other message to the focused item only. ```go func (m *model) fields() []focus.Field { return []focus.Field{ focus.Bind(&m.name), focus.Bind(&m.password.Model), // passwordinput embeds a textinput.Model focus.Bind(&m.bio), } } ``` ```go // Route does the whole dispatch: Tab and Shift+Tab (the input reader // decodes both ESC [ Z and the kitty form to a Tab key with the Shift // modifier) blur the old widget and focus the next one, wrapping in both // directions; every other message goes to the focused widget only. var cmd tui.Cmd m.ring, cmd = m.ring.Route(msg, m.fields()...) return m, cmd ``` From `examples/focus/main.go`, the reference program for focus. - `focus.Bind(&w)` works for any widget whose pointer has `Focus() tui.Cmd` and `Blur()` (`focus.Focusable`). Bind fields of the model value you are about to return. The pointers must point into that copy. - Call `ring.Sync(fields...)` once at start-up and return its Cmd from `Init`. Call it again after `Set` or `SetDisabled`. - `Route` does not broadcast. Forward messages every widget needs, such as blink ticks for an unfocused field, yourself. - `ring.Push(n)` opens a scope that traps Tab inside a modal. `Pop` restores the previous scope and its focused item. - `ring.WithOrder(focus.LayoutOrder(root, size, names...))` makes Tab follow screen position rather than index order (`ExampleLayoutOrder` in `focus/order_test.go`). - `ring.RouteAuto(msg, focus.Zones(root, size, names...), fields...)` also focuses the item under a left click. It also lets an item keep Tab for itself through `Field.Consumes`. `examples/settings` uses it. ## Mouse and hit-testing Mouse reporting is off by default. Turn it on with `tui.WithMouse(tui.MouseClick)` (press and release), `tui.MouseCellMotion` (also drags) or `tui.MouseAllMotion` (all movement). `tui.EnableMouse(mode)` returns a Cmd that switches it at run time. A `tui.MouseEvent` has 0-indexed cell coordinates `X`, `Y`, a `Button` (the wheel is four buttons), an `Action` and a `Mod`. [`hittest`](https://pkg.go.dev/github.com/ows4444/tui/hittest) answers which region a cell is in. A `hittest.Map[ID]` is an ordered set of rectangles. Later additions win where they overlap. Take the rectangles from the layout rather than computing them: ```go ui := layout.Row(1, layout.FlexChild{Node: layout.Named("list", layout.Block("one\ntwo\nthree")), Basis: 8}, layout.FlexChild{Node: layout.Named("detail", layout.Block("details")), Grow: 1}, ) size := layout.Size{W: 30, H: 5} var m hittest.Map[string] for _, p := range layout.Rects(ui, size) { if p.Name != "" { m = m.Add(p.Name, p.Rect) } } for _, click := range [][2]int{{2, 1}, {20, 4}} { if h, ok := m.At(click[0], click[1]); ok { fmt.Printf("click %v -> %s at %d,%d\n", click, h.ID, h.LX, h.LY) } } // Output: // click [2 1] -> list at 2,1 // click [20 4] -> detail at 11,4 ``` From `Example_fromLayout` in `hittest/layout_test.go`. `hittest.HitMap(root, size)` builds the same map in one call, keyed by name. `Map.AtEvent(ev)` tests a `MouseEvent` directly, and you decide which actions count. `layout.Rects` coordinates are relative to the root's top-left. If the root is not drawn at the screen origin, add its offset. In inline mode (`WithAltScreen(false)`) mouse coordinates are absolute in the terminal, so offset your rectangles by where the live region starts. ## Clipboard [`clipboard.Model`](https://pkg.go.dev/github.com/ows4444/tui/clipboard) is a button that copies its `Text` to the system clipboard with an OSC 52 sequence on Enter or Space, then shows "Copied!" for `Timeout` (2 seconds from `New`). With `Mouse` set, a left click inside `Bounds` activates it too. The package is experimental. ```go m := clipboard.New("secret-token", "Copy") var sent string m.Write = func(s string) (int, error) { sent = s; return len(s), nil } m, _ = m.Update(tui.Key{Type: tui.KeyEnter}) fmt.Println(m.Copied(), strings.HasPrefix(sent, "\x1b]52;")) // Output: // true true ``` From `Example` in `clipboard/example_test.go`. `Write` defaults to `os.Stdout.WriteString`. The example replaces it to capture the bytes. OSC 52 is write-only and best-effort, and the program cannot detect failure. The terminal must support it. Under tmux, `set-clipboard` must be `on` or `external`. If it is `off`, the button still shows "Copied!" but nothing reaches the clipboard. --- # Theming > Theme a Go terminal UI with theme.Theme: colour roles, preset themes, component tokens, glyph sets with an ASCII fallback, and colour depth. Web: https://tui.nizaami.com/theming/ Source: https://github.com/ows4444/tui/blob/code/docs/theming.md A [`theme.Theme`](https://pkg.go.dev/github.com/ows4444/tui/theme#Theme) is a plain, comparable value: colour roles (`Primary`, `Text`, `Muted`, `Focus`, `Selection`, `BorderColor`, `Background`, `Surface`, `Overlay`, ...), a `Border` style, glyphs, spacing, state and typography styles. There is no global theme. Themed code takes a `Theme` as an argument or holds one in a field, so switching themes is an assignment in `Update`. The preset constructors (`theme.DarkTheme()`, `theme.LightTheme()`, ...) each return a fresh copy, so no caller can change a preset for everyone else. To follow the terminal's background, ask for it with `tui.WithBackgroundDetection` and pass every message to `theme.Detect`: ```go func (a app) Update(msg tui.Msg) (tui.Model, tui.Cmd) { if th, ok := theme.Detect(msg, theme.DarkTheme()); ok { a.theme = th } return a, nil } ``` ```go var m tui.Model = app{theme: theme.DarkTheme()} // A light terminal answers with a near-white background. m, _ = m.Update(tui.BackgroundColorEvent{R: 250, G: 250, B: 250}) fmt.Println("light background -> Text is Light's:", m.(app).theme.Text == theme.LightTheme().Text) // A dark one answers dark. m, _ = m.Update(tui.BackgroundColorEvent{R: 10, G: 10, B: 10}) fmt.Println("dark background -> Text is Dark's:", m.(app).theme.Text == theme.DarkTheme().Text) // Silence keeps the fallback and tells the app to stop waiting. m, _ = m.Update(tui.BackgroundUnknownMsg{}) fmt.Println("no answer -> Text is Dark's:", m.(app).theme.Text == theme.DarkTheme().Text) // Unrelated messages change nothing. m, _ = m.Update(tui.Key{Type: tui.KeyEnter}) fmt.Println("other message -> unchanged:", m.(app).theme.Text == theme.DarkTheme().Text) // Output: // light background -> Text is Light's: true // dark background -> Text is Dark's: true // no answer -> Text is Dark's: true // other message -> unchanged: true ``` From `ExampleDetect_switchTheme` and its `app` model in `theme/example_test.go`. `theme.ForBackground` makes the Light or Dark choice from the background's perceived luminance. ## How a theme reaches widgets There are three paths, depending on the kind of widget. **Stateless helpers** in `widgets` and `widgets/chart` take the theme as an argument, for example `widgets.Box(title, content, t, width)`. `wizard.Model` and `markdown.Model` work the same way: their `View` and `LayoutNode` take a `theme.Theme`. **Component Models** that are themed have a `Theme theme.Theme` field, which `New` sets to `theme.DarkTheme()`, and a `SetTheme(theme.Theme) Model` method. That method makes them a [`tui.ThemeSetter[Model]`](https://pkg.go.dev/github.com/ows4444/tui#ThemeSetter). Each package asserts it at compile time with `var _ tui.ThemeSetter[Model] = Model{}`. Some widgets, such as `textinput`, have no `Theme` field and copy styles out of the theme instead. Their exported style fields can be overridden after `SetTheme`. Your root model forwards the theme to each widget: ```go // restyle rebuilds the theme from the design settings and hands it to every // themed widget. func (m *model) restyle() { m.t = m.design.theme() m.form = m.form.SetTheme(m.t) m.spin = m.spin.SetTheme(m.t) } ``` From `examples/login/main.go`. **The Program** can own the theme. With `tui.WithTheme(auto)`, a root model that implements [`tui.Themeable`](https://pkg.go.dev/github.com/ows4444/tui#Themeable) (`SetTheme(theme.Theme) tui.Model`) gets `auto.Dark` before `Init`, so before the first frame. The Program then queries the terminal background (OSC 11, 300ms timeout unless `WithBackgroundDetection` set one). If the terminal answers, it calls `SetTheme` again with `auto.Light` or `auto.Dark` to match. A terminal that does not answer keeps `auto.Dark`. The theme is not delivered again if it equals the one the model already has. The `BackgroundColorEvent` or `BackgroundUnknownMsg` still reaches `Update`. Without `WithTheme`, nothing is delivered and nothing is queried (`theme_program_test.go`). `theme.DefaultAuto()` is `Auto{Dark: DarkTheme(), Light: LightTheme()}`. `theme.Pair(preset)` uses `preset` on dark backgrounds and `preset.LightTwin()` on light ones. The Program looks for `Themeable` along the model's `Unwrap` chain (see `tui.Unwrapper`). A wrapper must also implement `tui.Rewrapper`, or a theme set through it is not applied. `appshell.Router` passes its theme to each child that is a `ThemeSetter`, or a `Themeable` whose `SetTheme` returns the child's type. No program in `examples/` uses `WithTheme` yet, and the repository has no Example for it. ## Presets `DarkTheme` (the default), `LightTheme`, `CatppuccinTheme`, `DraculaTheme`, `EverforestDarkTheme`, `GitHubDarkTheme`, `GruvboxTheme`, `HighContrastTheme`, `MonokaiTheme`, `NightOwlTheme`, `NordTheme`, `OneDarkTheme`, `RosePineTheme`, `SolarizedDarkTheme`, `SolarizedLightTheme`, `TokyoNightTheme`. `examples/login` has a design panel where you can try each preset in dark or light mode, with different borders, glyph sets, colour depths, border colours, accent tokens and padding. ## Overriding colours for one widget Component tokens change some colour roles for one kind of widget, or one instance, and leave the rest of the theme alone. A [`theme.Tokens`](https://pkg.go.dev/github.com/ows4444/tui/theme#Tokens) names only the roles that differ. A nil field inherits. - `t.WithTokens(theme.ComponentTabs, tok)` returns a copy of the theme that overrides every `tabs` widget it reaches. Use the `theme.Component*` constants: a misspelt name overrides nothing, silently. `theme.Components()` lists them. - `m.WithTokens(tok)` on a widget Model overrides that instance only. When a widget renders, it calls `Theme.Resolve(component, instanceTokens)`. The registered component tokens are applied first, then the instance tokens on top. `m.Tokens()` reports what the widget will use. ```go if a := d.pick[setAccent]; a > 0 { // Component tokens override the accent of these widgets only; the // rest of the theme keeps its roles. c := roles[a-1].of(t) tok := theme.Tokens{Accent: c, Focus: c} t = t.WithTokens(theme.ComponentForm, tok).WithTokens(theme.ComponentSpinner, tok) } ``` From `design.theme` in `examples/login/design.go`. ## States, typography and spacing Each is a field on `Theme`. A zero field means "derive it", so read them through the resolving methods: - `ResolvedStates()`: `Focus` in `t.Focus`, `Hover` in `t.Primary`, `Disabled` in `t.Muted`, `Selected` on a `t.Selection` background in `t.Text`. - `ResolvedTypography()`: H1 bold underlined `Primary`, H2 bold `Primary`, Emphasis italic, Strong bold, Code in `Secondary`, Link underlined `Info`. - `ResolvedSpacing()`: defaults `XS: 1, S: 1, M: 2, L: 3` cells. A style or size the theme sets explicitly is kept. ## Glyphs `Theme.Glyphs` holds the non-letter symbols widgets draw: progress fill, tree and accordion markers, spinner frames, status dots, check and cross, bullets, rules, cursors, heat and spark ramps, the password mask, the ellipsis. Empty fields fall back to `theme.UnicodeGlyphSet()`, so a theme that never sets `Glyphs` draws Unicode. The other sets are `ASCIIGlyphSet()` and `NerdGlyphSet()`. Every ASCII glyph is as wide as its Unicode counterpart except `Arrow` (`->` against `→`). `theme.DetectGlyphs()` picks a set from the environment. It returns ASCII when `TERM` is `dumb`, or when the first non-empty of `LC_ALL`, `LC_CTYPE`, `LANG` is `C`, `POSIX` or a charset other than UTF-8. Otherwise it returns Nerd when `TUI_NERD_FONT` is exactly `1`, and Unicode in every other case. It does not run automatically, so assign the result to `t.Glyphs` yourself. `t.ASCII()` returns the theme with the ASCII glyphs and `layout.ASCIIBorder()`. Code that reads glyphs from the theme: - Component packages: accordion, commandpalette, contextmenu, datatable, errorretry, faces, loadingbar, markdown, maskedinput, passwordinput, scrollbar, skeleton, spinner, splitpane, streamtext, toast, treeview. - layout: the `Text` ellipsis. - widgets files: alert, bigtext, chatmessage, codeblock, codeblock_lang, divider, infobox, list, pagination, panel, progressbar, progresscircle, status, stepper, table, toggle. - widgets/chart: barchart, gauge, heatmap, linechart, sparkline. `TestDocsListEveryGlyphReaderAndLexer` (`internal/tools/doccheck`) fails if a package or widgets file that names `Glyphs` is missing from this list. ## Colour depth, borders and contrast - `t.ForProfile(p)` downgrades every colour to what an `ansi.Profile` can show. Under `ansi.NoColor` all colours become nil and `Border` is kept. The Program also downgrades its own output; see [Capabilities](https://tui.nizaami.com/capabilities/). - `t.Plain()` clears `Border`, so bordered widgets render without box drawing. Pair it with reduced motion; see [Accessibility](https://tui.nizaami.com/accessibility/). - `t.LightTwin()` derives a light-background theme. Accents are darkened until they reach 4.5:1 against the new background. A theme that is already light is returned unchanged. - `t.Check(min)` lists text and accent roles whose contrast against `Background` is below `min`. If `Background` is unset, it measures against `TextInverse`. `t.CheckOn(bg, min)` measures against another colour. Use them in your CI to vet a custom theme. - `theme.NewPalette()` starts from xterm's ANSI 0-15 defaults. `Observe` records the terminal's `tui.PaletteColorEvent` answers (sent under `WithBackgroundDetection`), so `Palette.Check` measures named colours as the terminal shows them. --- # Rendering > After each Update, the Program calls View and writes only the difference from the frame already on screen. Web: https://tui.nizaami.com/rendering/ Source: https://github.com/ows4444/tui/blob/code/docs/rendering.md After each `Update`, the Program calls `View` and writes only the difference from the frame already on screen. The default renderer is the cell renderer. It parses each frame into a grid of cells and rewrites only the cells that changed. `WithCellRenderer(false)` selects the line renderer, which rewrites every row that changed. You don't call the renderer yourself. This page covers what it does with a View, when it skips a frame, and the tools for seeing what it wrote. ## From View to terminal For each frame the Program: 1. Calls `View` (or, for a `CellDrawer` root, `DrawCells`; see below). 2. Draws the inspector overlay (`WithInspector`) and the announcement rows (`WithAnnounceRegion`) over or under the View, if they're on. 3. Rewrites colours to the Program's colour profile, and flattens styled underlines when the capability probe found no support ([capabilities.md](https://tui.nizaami.com/capabilities/)). 4. Splits the View into lines. With `WithBidi(true)` it reorders each line for display. 5. Fits the lines to the terminal: expands tabs, cuts lines wider than the terminal, and on the alternate screen drops rows below the last one. Inline, rows that no longer fit go into scrollback once ([program.md](https://tui.nizaami.com/program/#inline-or-alternate-screen)). 6. Diffs against the previous frame, with relative cursor movement from the top of the live region. 7. Wraps the write in synchronized output (mode 2026), so the terminal paints the frame at once. A terminal without mode 2026 ignores the brackets. With `WithCapabilityProbe`, the brackets are left out when the terminal reports no support. 8. Shows the hardware cursor at the cell a `CursorPlacer` or `CursorProvider` asked for, if any. Otherwise the cursor stays hidden. All terminal writes (frames, `Println`, mode changes, the restore sequence) go through one mutex, and nothing is written after the terminal has been restored. ## When a frame is skipped or forced - If `View` returns the same string as the last frame, nothing is written. If the cursor position changed, only the cursor moves. A `CellDrawer` root has no string to compare, so it is diffed every time. - Without `WithMaxFPS`, messages that don't come from input (ticks, Cmd results, `Send`) are drawn at most 60 times a second. Keys, mouse, paste, focus and chords draw at once. `Update` still runs for every message, and a frame the cap skipped is drawn when the interval ends. `WithMaxFPS(n)` with `n > 0` caps every message, input included. `WithMaxFPS(0)` draws every message. `WithRecorder` and `WithRecorderSidecar` turn the cap off. - A resize, a `Suspend` or Ctrl+Z resume, and `ClearScreen` repaint the whole frame instead of diffing, because the terminal may have cropped or re-wrapped what was there. - `Println` and the quit frame are never delayed by the cap. ## Cell renderer and line renderer The two renderers produce the same screen. `TestCellRendererEquivalentToLineRenderer` and `FuzzRendererEquivalence` (in the root package) drive both with the same frames and compare every cell, its style and the cursor row. There is one difference: in the cell renderer each row's style stands alone, so a style that a View row leaves open doesn't carry into the next row. The cell renderer falls back in two ways: - **A row it can't represent** (a control character, an unknown escape or SGR code) is drawn alone with the line strategy. The rest of the frame stays a cell frame. - **A whole frame** goes to the line renderer in a few cases. One is a View taller than the terminal. The frame log names the reason. The current godoc of `WithCellRenderer` lists tabs, control characters and non-SGR escapes as whole-frame fallbacks. In the current code, tabs are expanded before rendering, and the other two trigger only the per-row fallback (`TestCellRendererFallsBackForUnsupportedRowOnly`). ## Drawing cells directly A root Model that implements `CellDrawer` draws each frame straight into a `cellbuf.Buffer` the size of the terminal, so no View string is built or parsed. `DrawView` and `DrawChild` compose children that only have a View. `View` is still required and should show the same screen, because the Program uses it whenever the direct path is unavailable: - with `WithCellRenderer(false)` or in accessible mode - while an inspector pane is showing, or with an announcement region - when the colour profile is below TrueColor, or the probe found no styled underlines - when the terminal size is unknown - once an inline frame has scrolled rows into scrollback `examples/canvas` is a `CellDrawer` widget. ## Frame budget Tests in the root package hold the cell renderer to a budget for a steady-state 200x60 frame of styled rows that all change (`BenchmarkFrame200x60Styled`): | Test | Limit | | --- | --- | | `TestFrame200x60StyledAllocsCriterion34` | at most 7 allocations per frame | | `TestFrame300x80StyledAllocs` | at most 8 allocations per frame at 300x80 | | `TestFrame200x60StyledTime` | at most 60µs per frame; runs only with `TUI_TIMING_TESTS` set | The allocation tests skip under the race detector. ## Seeing what was drawn ### Frame log `WithFrameLog(w)` writes one line per frame to `w`: ```text n=7 t=1042 kind=diff rows=24 changed=2 bytes=118 us=61 ``` `kind` is `diff`, `full` (no previous frame: the first frame, after a resize, `Suspend` or `Println`), `accessible`, or `fallback`, which adds `reason=`. A frame with rows drawn by the per-row fallback adds `fallback_rows=:,...`. The log doesn't change what reaches the terminal, and write errors are ignored. Don't point it at the terminal's own output. The `WithFrameLog` godoc defines each field. ### Inspector `WithInspector(tui.InspectorKeys{})` turns on four overlay panes, each toggled by a key that never reaches `Update`: | Key | Pane | | --- | --- | | F12 | Info panel: named layout rectangles (`LayoutInspector`), the focused id (`FocusInspector`), the last frame's bytes and draw time | | F11 | Outlines of every named layout rectangle | | F10 | The last 200 messages `Update` received, with how long each took. Recorded only while the pane is on | | F9 | A `%#v` dump of the root Model | Set a field of `InspectorKeys` to another key name, or to `tui.InspectorOff` to leave that pane out. The inspector is ignored in accessible mode. ### Recording and replay `WithRecorder(w)` writes the session as an asciicast v2 file that plays in asciinema. `WithRecorderSidecar(w)` writes the input bytes, sizes, `Every` ticks and a hash of every write. `tuitest.Replay` reads the pair and fails a test if any write differs. Messages from `Send`, and Cmd results that depend on the outside world, aren't recorded, so a model that needs them won't replay. The recording contains input as typed, passwords included. ### Logging The terminal belongs to the Program, so don't log to stdout or stderr while it runs. `tui.LogToFile(path, prefix)` points the standard `log` package at a file (created, or appended to) and returns it for you to close. --- # Terminal capabilities > A Program learns about the terminal in two ways. At NewProgram, it reads the colour profile from the environment. Web: https://tui.nizaami.com/capabilities/ Source: https://github.com/ows4444/tui/blob/code/docs/capabilities.md A Program learns about the terminal in two ways. At `NewProgram`, it reads the colour profile from the environment. Optionally, at startup, it can query the terminal itself with `WithCapabilityProbe`. Every other protocol the library can use (mouse, focus reporting, the kitty keyboard protocol, background detection) is off until an option asks for it, so a Program behaves the same whichever terminal it runs on. ## Colour profile Without `WithColorProfile`, `NewProgram` calls `ansi.DetectColorProfileFor` on the output. The first rule that matches wins: 1. `NO_COLOR` non-empty: `ansi.NoColor`. 2. Unless `CLICOLOR_FORCE` is non-empty and not `0`: an output that isn't a terminal, or `CLICOLOR=0`, gives `ansi.NoColor`. 3. `TERM=dumb`: `ansi.NoColor`. 4. `COLORTERM` is `truecolor` or `24bit`, or `WT_SESSION` is set: `ansi.TrueColor`. 5. `TERM_PROGRAM` is `iTerm.app`, `WezTerm`, `vscode`, `ghostty` or `Hyper`: `ansi.TrueColor`. `Apple_Terminal`: `ansi.ANSI256`. 6. `TERM` is `xterm-kitty`, `xterm-ghostty`, `alacritty` or `wezterm`: `ansi.TrueColor`. A `TERM` containing `256color`: `ansi.ANSI256`. An empty `TERM`: `ansi.NoColor`, or `ansi.ANSI16` on Windows. Anything else: `ansi.ANSI16`. The Program rewrites every colour it writes to the nearest one the profile can show, in frames and in `Println`/`Eprintln` text, so widgets need no changes. Under `ansi.NoColor` all colour is removed, and bold, underline and the other attributes stay. `Program.ColorProfile` reports the profile in use. Truecolor terminals that set none of these variables (some SSH sessions, tmux with `TERM=screen`) are detected at a lower depth. Pin the profile with `WithColorProfile(ansi.TrueColor)`. Any `WithColorProfile`, even `ansi.TrueColor`, overrides `NO_COLOR` (`TestExplicitColorProfileOverridesNoColor`). An empty `NO_COLOR` counts as unset. `examples/probe` shows the detected profile, focus reports and the background colour for the terminal you run it in. ## Dumb terminals With `TERM=dumb`: - The colour profile is `ansi.NoColor`. - A Program given neither `WithAltScreen` nor `WithAccessible` runs in accessible mode, which writes no escape sequences at all ([accessibility.md](https://tui.nizaami.com/accessibility/)). An explicit `WithAltScreen(true)` or `WithAccessible(false)` is honoured (`TestDumbTermDefaults`). - `theme.DetectGlyphs`, if the app calls it, returns the ASCII glyph set ([theming.md](https://tui.nizaami.com/theming/)). ## The capability probe `WithCapabilityProbe(timeout)` makes Run send these queries at startup: DECRQM for modes 2026 and 2027, the kitty keyboard and kitty graphics queries, the kitty notification query (OSC 99), and XTVERSION, followed by DA1. The DA1 reply ends the probe. If DA1 doesn't arrive within `timeout`, every capability is false. A non-positive timeout means `tui.DefaultCapabilityProbeTimeout` (500ms). The probe is off by default and ignored in accessible mode. When the probe ends, `Update` receives one `CapabilitiesMsg`, and `Program.Capabilities` returns the same `Capabilities` value. `Program.CapabilitiesKnown` tells "nothing supported" from "not finished yet". Replies to the probe's queries don't reach `Update`. What the Program does with the answer: | Capability | Effect | | --- | --- | | `SyncOutput` false | Frames are written without the mode 2026 brackets | | `StyledUnderline` false | Styled underlines (`SGR 4:n`) are written as plain underline. The value is inferred from the kitty replies or XTVERSION, not queried | | `GraphemeClusters` | When true, the Program sets mode 2027 (reset on exit) and measures grapheme clusters as one unit; otherwise it measures per codepoint | | `Notifications` | `Assertive` announcements are also sent as desktop notifications | | `KittyKeyboard`, `KittyGraphics`, `Sixel`, `XTVersion` | Reported only | Before the probe finishes, and without it, the Program uses synchronized output and styled underlines as the View asks. ## Grapheme cluster width `ansi.Width` and the other package-level width functions count a grapheme cluster (a ZWJ emoji, a flag, a letter with combining marks) as one unit by default. `TUI_NO_CLUSTERS` set to any non-empty value, or `ansi.SetClusterWidth(false)`, counts each rune on its own instead. That setting is process-wide. Each Program also has its own `ansi.Measurer`, returned by `Program.Measurer` and carried in every `ResizeMsg`. It follows the process-wide setting until the probe decides. The probe then sends a second `ResizeMsg` with the new `Measurer` and repaints. A component that measures text for one terminal should use `msg.Measurer` rather than `ansi.Width`, so two Programs in one process (an SSH server, parallel tests) don't measure for each other. No library code calls `ansi.SetClusterWidth` (`TestNoGlobalWidthToggle` in `internal/archtest`). ## Background colour `WithBackgroundDetection(timeout)` asks the terminal for its background colour (OSC 11) and for palette colours 0 to 15 (OSC 4) at startup. The reply comes as a `BackgroundColorEvent`, and each palette reply as a `PaletteColorEvent`. If nothing arrives within `timeout`, `Update` gets one `BackgroundUnknownMsg`. `WithTheme` uses the same query, with a 300ms timeout unless `WithBackgroundDetection` set one, to pick a light or dark theme ([theming.md](https://tui.nizaami.com/theming/)). Neither query is sent in accessible mode. ## Environment variables Library code reads only these: | Variable | Read by | Effect | | --- | --- | --- | | `NO_COLOR` | colour detection | Non-empty: no colour | | `CLICOLOR_FORCE`, `CLICOLOR` | colour detection | Force colour on a non-terminal; `CLICOLOR=0` turns it off | | `COLORTERM`, `WT_SESSION`, `TERM_PROGRAM`, `TERM` | colour detection | Pick the depth (see above) | | `TERM=dumb` | `NewProgram`, `WithAccessibleAuto`, glyph detection | Accessible mode by default; ASCII glyphs | | `ACCESSIBLE=1` | `WithAccessibleAuto` | Accessible mode | | `NO_ANIMATION`, `REDUCE_MOTION` | `motion.Detect` | Non-empty: reduced motion ([accessibility.md](https://tui.nizaami.com/accessibility/)) | | `TUI_NO_CLUSTERS` | `ansi` | Non-empty: measure per rune | | `LC_ALL`, `LC_CTYPE`, `LANG`, `TUI_NERD_FONT` | `theme.DetectGlyphs` | Glyph set ([theming.md](https://tui.nizaami.com/theming/)) | `WithRecorder` also copies `TERM` into the asciicast header. --- # Accessibility > tui.WithAccessible(true) switches a Program to accessible output: an append-only plain-text transcript that a screen reader can follow, with no escape sequences. Web: https://tui.nizaami.com/accessibility/ Source: https://github.com/ows4444/tui/blob/code/docs/accessibility.md `tui.WithAccessible(true)` switches a Program to accessible output: an append-only plain-text transcript that a screen reader can follow, with no escape sequences. It is off by default. `tui.WithAccessibleAuto()` turns it on when `ACCESSIBLE=1` or `TERM=dumb`. Under `TERM=dumb` it is also on by default for a Program given neither `WithAltScreen` nor `WithAccessible`. `Program.Accessible` reports whether it is on. ## What accessible mode changes - No alternate screen, even with `WithAltScreen(true)`, and no cursor movement, line clearing, synchronized output, colour or other SGR styling, or OSC sequences. - Each frame writes only the lines that differ from the previous frame, once each, in order. Lines that disappear aren't erased. - When a line grows (the new line starts with the old one), only the new suffix is written. `WithLinearizeFullLine()` writes the whole line again instead. - When the new frame is the previous one scrolled by a fixed number of rows, lines already written aren't written again. Otherwise every changed line is written, even if the same text appears elsewhere. - Reduced motion is on, and `WithReducedMotion(false)` can't turn it off. - The inspector, the capability probe and background detection are off. - A resize doesn't repaint anything. The text comes from the root model's `Linearize()` if it implements `Linearizer`, and otherwise from `View()` with escape sequences removed. `Linearize` should return one self-contained line per item, with state in words rather than glyphs, and no padding or box drawing. Composite models delegate to their children. Every stateful widget package has a `Linearize` method; `TestEveryStatefulWidgetHasLinearizeAndLayoutNode` in `internal/archtest` fails for a widget package that lacks one. The Program doesn't own the theme, so it can't drop borders by itself. When `Program.Accessible()` is true, give your widgets `theme.Theme.Plain()`, which clears the border. ## Announcements `tui.Announce(text)` returns a Cmd that reports a change that isn't otherwise visible, such as "saved". `tui.AnnounceWith(text, tui.Assertive)` marks one as urgent; `Announce` is `tui.Polite`. In accessible mode, an announcement is written to the transcript as its own line, with escape sequences removed. Polite ones are de-duplicated: the same text within one second is written once. Assertive ones are always written, prefixed `Alert: `. Outside accessible mode, `Announce` does nothing unless one of these is set: - `WithAnnounceRegion(rows)` reserves `rows` rows below the View that show the latest announcements for `tui.AnnounceTTL` (3 seconds) each. Inline, the rows are added below the View. On the alternate screen, the View is cut to make room. - With `WithCapabilityProbe`, if the terminal supports OSC 99, Assertive announcements are also sent as desktop notifications, with or without a region. ## Reduced motion `Program.ReducedMotion` reports the preference. It is `motion.Detect()`, which is reduced when `NO_ANIMATION` or `REDUCE_MOTION` is non-empty, unless `WithReducedMotion` set it. Accessible mode forces it on. The Program doesn't enforce the preference, because it can't tell which Cmds animate or which parts of a View are decoration. Your app reads it and passes it on: - The animated widgets (`spinner`, `skeleton`, `loadingbar`, `faces`, `streamtext`, `textinput`, `textarea`, `drawer`) have a `Motion` field of type `motion.Preference`. Its zero value is `motion.Normal`, so the environment alone doesn't change them. Set it to `motion.Reduced` and the widget is at its final frame as soon as it starts, and schedules no tick (`TestReduceMotionEnvEveryAnimatedWidget`). - `theme.Theme.Plain()` drops decorative borders. ## Colour independence No status-bearing widget relies on colour alone. `TestColourIndependence` sets `NO_COLOR=1`, renders every state of each listed widget (alerts, badges, checkboxes, toggles, toasts, progress and more), removes the escape sequences, and fails if two states of one widget give the same text. A new widget whose meaning is a state has to be added to that test by hand. `NO_COLOR` itself is covered in [capabilities.md](https://tui.nizaami.com/capabilities/#colour-profile). ## The hardware cursor By default the terminal cursor is hidden. A screen magnifier or an IME needs it to sit on the focused field. A root model that implements `CursorPlacer` or `CursorProvider` gets the real cursor placed at the cell it names after each frame. `textinput` and `textarea` implement `CursorProvider`, so a root model can forward the focused field's `CursorCell`. The Program checks only the root model and its `Unwrap` chain, not fields inside it. The cursor isn't shown in accessible mode. --- # Migrating to v1 > Each identifier removed before v1, and what to use instead. The - BREAKING: entries under Removed in CHANGELOG.md list the same removals. Web: https://tui.nizaami.com/migrating-to-v1/ Source: https://github.com/ows4444/tui/blob/code/docs/migrating-to-v1.md Each identifier removed before v1, and what to use instead. The `- BREAKING:` entries under Removed in [CHANGELOG.md](https://tui.nizaami.com/changelog/) list the same removals. Deprecated identifiers that still exist, such as `layout.Box.Render`, are listed under Deprecated there. ## Program options (package tui) `WithInput`, `WithOutput` and `WithErrOutput` now take any `io.Reader` or `io.Writer`, file or not, so the separate options for non-file streams are gone. Option defaults are in [program.md](https://tui.nizaami.com/program/). ### WithInputReader Use `WithInput`. Given a reader that is not an `*os.File`, `WithInput` behaves as `WithInputReader` did: `Run` needs no terminal and doesn't switch raw mode, and the Program quits when the reader returns `io.EOF` (`TestWithInputPlainReaderNeedsNoTerminal`). Given an `*os.File`, `WithInput` reads it as a terminal, and `Run` fails with a "not a terminal" error if it isn't one (`TestWithInputFileIsReadAsATerminal`). To read a file as a plain stream instead, wrap it so it is no longer an `*os.File`, as the `WithInput` godoc shows (`struct{ io.Reader }{f}`). A reader that also implements `io.Closer` is closed when the Program quits. `WithInputCloser` names a different closer to use instead. ### WithOutputWriter Use `WithOutput`. A writer that is not an `*os.File`, such as a `bytes.Buffer`, has no terminal size: the Program assumes 80x24 and never queries or resizes it. A `ResizeMsg` can still deliver another size. ### WithErrWriter Use `WithErrOutput`, which sets where `Eprintln` writes (default `os.Stderr`). Any `io.Writer` receives the text as it is. ### WithLineRenderer Use `WithCellRenderer(false)`. The cell renderer is the default, and `WithCellRenderer(false)` selects the line renderer. See [rendering.md](https://tui.nizaami.com/rendering/#cell-renderer-and-line-renderer) for how the two differ. ## String layout helpers (package layout) The string join and grid helpers are replaced by Nodes. Wrap each pre-rendered string in `layout.Block` to make it a Node, then compose Nodes with `Row`, `Column` and `GridNode`. `ExampleCrossAlign` and `ExampleGridNode` in `layout/example_test.go` show the replacements in use, and [layout.md](https://tui.nizaami.com/layout/) covers sizing with `FlexChild`. `Box`, `Overlay` and `JoinHorizontalJustify` still work on strings and are not removed. ### JoinHorizontal Use `layout.Row(gap, children...)`, with one `FlexChild` per block. ### JoinHorizontalAlign Use `layout.Row` and set `CrossAlign` on each `FlexChild`: `CrossStart`, `CrossCenter` or `CrossEnd`. The zero value, `CrossStretch`, stretches the child to the row's full height. ### JoinVertical Use `layout.Column(gap, children...)`, with one `FlexChild` per block. ### JoinVerticalAlign Use `layout.Column` and set `CrossAlign` on each `FlexChild`, as for `JoinHorizontalAlign`. In a Column the cross axis is horizontal, so `CrossStart` keeps a child at its own width, flush left (`ExampleColumn_crossStart`). ### FlexRow Use `layout.Row`. Sizing that was set per item now goes in each child's `FlexChild` fields (`Basis`, `BasisLen`, `Grow`, `Shrink`, `Min`, `Max`). ### FlexItem Use `layout.FlexChild`. The zero `FlexChild` is sized to its content and neither grows nor shrinks; `layout.Fill(n)` makes a child take the space left over. ### Grid Use `layout.GridNode(tracks, gap, cells...)`. It takes one `Track` per column and fills cells row by row. `GridNodeGaps` gives rows and columns different gaps. ### GridFlex Use `layout.GridNode`. A `Track` with `Grow > 0` shares the grid's leftover width by weight, on top of the column's content width. ### ColSpec Use `layout.Track`. The zero `Track` sizes the column to its widest cell, `Size > 0` fixes the width, and `Grow > 0` shares leftover width. --- # Testing > Run and write tests for tui: the tuitest headless harness, golden files, PTY tests, race, fuzz and benchmark runs, and the test environment variables. Web: https://tui.nizaami.com/testing/ Source: https://github.com/ows4444/tui/blob/code/docs/testing.md ## Run the tests Run one package, or one test, while you work: ```console $ go test ./tuitest -run '^Example$' ok github.com/ows4444/tui/tuitest 0.399s ``` Before you push, run what the `test` job in [.github/workflows/ci.yml](https://github.com/ows4444/tui/blob/code/.github/workflows/ci.yml) runs: ```sh go build ./... go vet ./... go test -race ./... go test -coverprofile=cover.out -coverpkg=./... ./... go run ./internal/tools/covercheck cover.out go run ./internal/tools/doccheck ``` `covercheck` fails when a library package is under 90% statement coverage; [CONTRIBUTING.md](https://tui.nizaami.com/contributing/#coverage) lists what it skips. `-short` skips the slow tests: cross-compiling the module for every supported GOOS (`TestBuildsOnSupportedPlatforms`), building every example, the 1M-line `logview` heap test, PTY latency runs, and the wall-clock comparisons. CI does not pass `-short`. ## Where tests live Tests sit next to the code as `*_test.go`, in the package itself or in a `_test` package. Runnable Examples are in `example_test.go` in most packages (`focus` and `hittest` keep theirs in other test files). Golden files are under the package's `testdata/`. The module-wide rules have their own test-only packages: | Package | What it checks | |---|---| | `internal/archtest` | Import direction, stdlib-only imports, file size, widget method rules, `.golangci.yml` is current | | `internal/isolation` | `Update` on a Model never changes a value copy taken before it (`TestCopyIsolationSweep`) | | `internal/tools/doccheck` | Doc comments, and README and `docs/` claims against the code | The root package's `TestEveryPackageHasExample` fails when a public package has no `Example` function. ## Testing a model with tuitest `tuitest.New` runs a model in a real `tui.Program` against an in-memory terminal; keys, pastes, clicks, resizes and Msgs go in, and the emulated screen comes out. This is `Example` from [tuitest/example_test.go](https://github.com/ows4444/tui/blob/code/tuitest/example_test.go): ```go s := tuitest.New(typer{}, 20, 3) defer s.Close() s.Keys("h", "i") fmt.Println(strings.TrimRight(s.Screen()[0], " ")) fmt.Printf("%q\n", s.Cell(2, 0).Grapheme) // Output: // > hi // "h" ``` `tuitest.NewFakeClock` with `tuitest.WithClock` lets a test advance time by hand (`Session.Advance`) instead of waiting on the wall clock. `Session.Golden(t, name)` compares the screen with `testdata/.golden`. See the package godoc for the rest of `Session`. Most widget tests don't need a Program: they call `Update` with a `tui.Key` or other Msg and check the returned Model and `View`, as [examples/counter/main_test.go](https://github.com/ows4444/tui/blob/code/examples/counter/main_test.go) does. ## Golden files A golden test fails with a diff when the output changes. How to accept the new output depends on the helper that wrote the golden: | Goldens | Rewrite with | |---|---| | `testdata/size-x.golden` from `testutil.SizeMatrix` (every example) | `TUITEST_UPDATE=1 go test ./examples/`, or `-update-sizes` | | `tuitest.Session.Golden` | `TUITEST_UPDATE=1`, or `-update` where the test package declares that flag | | Packages with their own `-update` flag (`widgets`, `internal/highlight`, several examples) | `go test ./ -update` | `-update` on a package that doesn't declare it fails with "flag provided but not defined"; use `TUITEST_UPDATE=1` there. Review the rewritten files in the diff before committing. `.gitattributes` keeps `*.golden` at LF line endings on Windows, since goldens are compared byte for byte. ## Environment variables | Variable | Effect | |---|---| | `TUITEST_UPDATE=1` | Rewrite goldens, as above | | `TUI_TIMING_TESTS=1` | Run the wall-clock tests (`frame_budget_test.go`, `e2e_bench_test.go`); they also skip under `-race` and `-short` | | `ARCHTEST_UPDATE=1` | Regenerate `.golangci.yml` from the `internal/archtest` rules: `ARCHTEST_UPDATE=1 go test ./internal/archtest -run TestGolangciMirror` | | `BIDICHARACTERTEST=` | Run the full Unicode `BidiCharacterTest.txt` in `internal/bidi` instead of the built-in cases | The library itself reads `NO_COLOR`, `TERM`, `TUI_NO_CLUSTERS`, `NO_ANIMATION`, `REDUCE_MOTION` and others (see the README's [Platforms and limitations](https://tui.nizaami.com/overview/#platforms-and-limitations)), so a variable set in your shell can change what a test sees. For instance, `TestReadmeUnicodeClaimsMatchClusterDefault` skips when `TUI_NO_CLUSTERS` is set. ## PTY tests Files named `*_pty_test.go`, and the tests in `term` and `internal/cancelreader` that need a terminal, open a real pseudo-terminal through `internal/ptytest`. They build only on Linux and macOS; on other platforms those files are not compiled. Some re-run the test binary as a helper child process (a test that skips with "helper process for ..." is that child). `program_pty_test.go` skips when it can't open a pty. On Windows, `TestProbeUnderConPTY` in `examples/probe` drives the probe example inside a Windows pseudo console. CI runs it in its own job: ```sh go test -count=1 -v -run 'TestProbeUnderConPTY' ./examples/probe ``` ## Race detector CI runs `go test -race ./...` on Linux, macOS and Windows. Wall-clock tests skip under `-race`: the root package, `ansi`, `layout`, `markdown` and `streamtext` each define a `raceEnabled` constant in `race_on_test.go` and `race_off_test.go` for this. ## Fuzzing Fuzz targets run their seed corpus as ordinary tests in `go test`. To fuzz one: ```sh go test -run '^$' -fuzz '^FuzzReadEvent$' -fuzztime 60s ./input ``` | Package | Targets | |---|---| | root | `FuzzRendererEquivalence` (cell and line renderers draw the same screen; corpus in `testdata/fuzz/`) | | `ansi` | `FuzzStripANSI`, `FuzzClusterWidth`, `FuzzPlainASCIIWidth`, `FuzzWidthMatchesReference`, `FuzzTruncateMatchesReference`, `FuzzASCIIFastPathMatchesSlowPath`, `FuzzDowngradeString` | | `input` | `FuzzReadEvent` | | `layout` | `FuzzSolveFlex`, `FuzzBlockRender` | | `markdown` | `FuzzRender` | | `internal/highlight` | `FuzzLines` | No workflow runs fuzzing. ## Benchmarks CI's `test` job runs a smoke pass on Linux: ```sh go test -run '^$' -bench 'VirtualList10k|RenderDiff|Frame|Repaint' -benchtime=100x ./... ``` The allocation gate against `bench/baseline.txt` is described in [CONTRIBUTING.md](https://tui.nizaami.com/contributing/#benchmarks). ## CI [.github/workflows/ci.yml](https://github.com/ows4444/tui/blob/code/.github/workflows/ci.yml) runs on pushes to `main` and on pull requests. Beyond a local run, it adds: - `test` on `ubuntu-latest`, `macos-latest` and `windows-latest`, with `fail-fast: false`. Coverage, `doccheck` and the benchmark smoke run on Linux only. - `conpty`: the ConPTY test above, on Windows. - `freebsd`: `go build`, `go vet` and `go test ./...` in a FreeBSD 14.2 VM. - `api`: `api.txt` is current, and the CHANGELOG names every breaking change since the latest tag. - `widthtables`: the Unicode tables match a fresh generation. - `lint` (golangci-lint) and `security` (gosec). [.github/workflows/bench.yml](https://github.com/ows4444/tui/blob/code/.github/workflows/bench.yml) runs the allocation gate. --- # Architecture overview > tui is a terminal UI framework on the Elm Architecture: an application is a Model with Init, Update and View, and the root package's Program turns terminal input into messages for Update and turns ... Web: https://tui.nizaami.com/architecture/ Source: https://github.com/ows4444/tui/blob/code/docs/architecture/overview.md tui is a terminal UI framework on the Elm Architecture: an application is a Model with `Init`, `Update` and `View`, and the root package's Program turns terminal input into messages for `Update` and turns `View` into the fewest bytes that bring the screen up to date. The module imports only the Go standard library. Everything else is built on that runtime in layers: text and colour primitives below it, stateless renderers and stateful widgets above it. ## Bird's-eye view ```text terminal bytes -> input.Reader (reader goroutine) decode keys, mouse, paste, focus, replies -> Program message queue (64) also fed by Cmd goroutines, Send, timers, resize watcher -> event loop (one goroutine) intercepts runtime messages, calls Model.Update, starts the returned Cmd on its own goroutine -> Model.View or CellDrawer.DrawCells -> frame transforms inspector, announce region, colour downgrade, fit, bidi -> frame renderer internal/render cell diff, or the line renderer -> output writer (one mutex) wrapped in synchronized output ``` Terminal control (size, raw mode, output VT processing) doesn't go through the byte stream. It goes through the `Terminal` port, implemented by `internal/termio`, which calls package `term`. ## Entry points - `tui.NewProgram(model, opts...)` and `Program.Run`: the API every app uses. - `Model`, `Msg`, `Cmd` (`model.go`): the application contract. - `tuitest`: runs a Model headless against a virtual screen, for tests. - `examples/`: one runnable program per directory. ## Components ### Program and event loop (root package) `program.go` builds the Program and owns startup and terminal restore. `loop.go` is the event loop. `options.go` holds the `ProgramOption`s, and `cmds.go`, `cmds_runtime.go` and `cmds_ctx.go` the Cmds. The loop is the only goroutine that calls `Init`, `Update` and `View`, and the only one that touches frame state. Other goroutines (input reader, Cmds, signal handler, resize watcher) only put messages on the queue. A goroutine that needs the terminal restored (a signal, a panicking Cmd) asks the loop, and falls back to a fixed restore sequence after 500ms (`program_restore.go`). Runtime messages such as `Batch`, `Sequence`, `Println`, `Suspend` and mode changes are handled by the loop and never reach `Update`. Lifecycle, messages and options: [program.md](https://tui.nizaami.com/program/). ### Input Package `input` decodes bytes into `Key`, `MouseEvent`, `PasteEvent`, `FocusEvent` and terminal replies. The root package re-exports these types as aliases. `program_input.go` runs the reader goroutine. It takes capability-probe and clipboard replies out of the stream, and drops repeated focus events, before anything reaches the queue. Details: [input.md](https://tui.nizaami.com/input/). ### Rendering `frame.go` turns a View into lines and tracks the live region (how many rows, where the cursor is). `frame_render.go` defines the two frame renderers: the cell renderer (default, `internal/render`) and the line renderer, which is also the fallback. `celldraw.go` is the direct path for a `CellDrawer` root, drawing into a `cellbuf.Buffer`. `internal/render` does no I/O: it returns bytes and the Program writes them. Details: [rendering.md](https://tui.nizaami.com/rendering/). ### Terminal port `tui.Terminal` is the one terminal interface; `internal/termio` holds `OSTerminal` and a `Fake` for tests (`TestSinglePortDefinition`). Only `term` and `internal/termio` may import `term` (`TestOnlyTermioImportsTerm`). `WithTerminal` swaps the port for a remote session. ### Capabilities and accessibility `capabilities.go` runs the optional startup probe. The wire format and reply parser live in `internal/capprobe`, which does no I/O. `accessible.go` and `dumbterm.go` implement accessible mode and announcements, with their state in `internal/announce`. See [capabilities.md](https://tui.nizaami.com/capabilities/) and [accessibility.md](https://tui.nizaami.com/accessibility/). ### Diagnostics `framelog.go` (`WithFrameLog`), `inspector.go` (`WithInspector`) and `recorder.go` (`WithRecorder`, read back by `tuitest.Replay`). They only observe: the frame log and the inspector's timing run after the frame has been written. ## Package layers `internal/archtest` enforces import direction: a package may import only packages in lower tiers, or in its own tier, except that components can't import each other. `TestImportDirection` fails on any import that points up. | Tier | Packages | | --- | --- | | 0 | `ansi`, `layout`, `theme`, `motion`, `term`; `internal/basetypes`, `internal/a11y`, `internal/bidi`, `internal/fsutil`, `internal/highlight`, `internal/ptytest` | | 1 | `input`, `keymap` | | 2 | `hittest`, `cellbuf`; `internal/render`, `internal/termio`, `internal/capprobe`, `internal/announce`, `internal/braille`, `internal/cancelreader`, `internal/edit`, `internal/vtscreen` | | 3 | the root package `tui` | | 4 | `widgets`, `widgets/chart`, `markdown` (stateless, may not import the root); `focus`, `tuitest`; test support `internal/cellcheck`, `internal/testutil` | | 5 | every other package: the stateful components (`textinput`, `viewport`, `datatable`, ...) | Other rules in the same package: - Every package in the module, examples and tools included, imports only the standard library and the module, and `go.mod` requires nothing (`TestLibraryImportsStdlibOnly`). - A component imports another component only where `composition` in `layers_test.go` allows it (for example `form` imports `textinput` and `passwordinput`). - `widgets`, `widgets/chart` and `markdown` import neither the root runtime nor `focus` or `tuitest` (`TestStatelessKitsDoNotImportRuntime`). The root imports no component (`TestRootImportsNoComponent`). - No non-test file imports `internal/cellcheck` or `internal/testutil` (`TestTestHelpersStayOutOfProductionCode`). - No library code calls `ansi.SetClusterWidth`: width settings for one terminal travel as an `ansi.Measurer` (`TestNoGlobalWidthToggle`). - Every public package has a runnable Example (`TestEveryPublicPackageHasExample`). - Every stateful widget has `Linearize` and `LayoutNode` methods (`TestEveryStatefulWidgetHasLinearizeAndLayoutNode`). `.golangci.yml` mirrors the import rules for editors. It is generated from the same tables: `ARCHTEST_UPDATE=1 go test ./internal/archtest -run TestGolangciMirror`. ## Cross-cutting concerns **Concurrency.** Model code runs on the loop goroutine only. Each Cmd runs on its own goroutine (`WithMaxConcurrentCmds` caps them). `Program.Context` is cancelled when Run returns. Every send to the queue also watches for shutdown, so a Cmd that finishes after Run returns doesn't block delivering its message. All terminal writes share one mutex. Once the terminal is restored, later writes are dropped. **Terminal restore.** Every exit path restores the terminal exactly once: a normal return, an error, a panic, SIGTERM/SIGHUP, and a panicking Cmd goroutine. Modes are switched on in a fixed order and off in reverse (`enterModes` and `leaveModes` in `program.go`). **Platforms.** Linux, macOS, the BSDs and Windows. Building for any other `GOOS` fails on purpose with an import error naming the platform as unsupported (`platform_unsupported.go`). Resizes come from SIGWINCH on Unix and from console events, with polling as a fallback, on Windows. --- # Contributing > Most conventions here are enforced by a test or a CI job; each section names the check that fails when a rule is broken. Web: https://tui.nizaami.com/contributing/ Source: https://github.com/ows4444/tui/blob/code/CONTRIBUTING.md Most conventions here are enforced by a test or a CI job; each section names the check that fails when a rule is broken. How to run the tests, and what the test-only environment variables do, is in [docs/testing.md](https://tui.nizaami.com/testing/). ## Before you push These are the commands the `test` job in [.github/workflows/ci.yml](https://github.com/ows4444/tui/blob/code/.github/workflows/ci.yml) runs (the last three on Linux only): ```sh go build ./... go vet ./... go test -race ./... go test -coverprofile=cover.out -coverpkg=./... ./... go run ./internal/tools/covercheck cover.out go run ./internal/tools/doccheck ``` CI also checks that the generated files below are current, and runs `golangci-lint run ./...` (v2.1.6) and `gosec ./...` (v2.29.0). Both are dev tools that CI installs with `go install`; they are not module dependencies. ## No dependencies Every package, examples and tools included, imports only the standard library and this module, and `go.mod` has no `require` directive. `TestLibraryImportsStdlibOnly` in `internal/archtest` fails otherwise. CI actions (such as `vmactions/freebsd-vm`) and dev tools installed in CI are not module dependencies. ## Package rules - **Import direction.** Packages are layered in tiers, and an import may only point down. `TestImportDirection` in `internal/archtest` is the authoritative check; `.golangci.yml` mirrors it for golangci-lint. Every package under `internal/` needs a tier (`TestEveryInternalPackageHasATier`). The tiers are described in [docs/architecture/overview.md](https://tui.nizaami.com/architecture/). - **Test support stays in tests.** A non-test file must not import a test-support package (`TestTestHelpersStayOutOfProductionCode`). - **No exported mutable package variables**, except `Err*` sentinels. Export a function that returns a copy instead (`TestNoExportedMutableVars`). - **File size.** A hand-written non-test source file over 800 lines fails `TestFileSize` unless it is listed in `oversize` in `internal/archtest/layers_test.go` with a reason. Generated files are exempt. - **No process-wide width toggle in library code.** Only an application may call `ansi.SetClusterWidth`; pass an `ansi.Measurer` instead (`TestNoGlobalWidthToggle`). ## Widget packages A component is one package with a value-typed `Model` configured through exported fields. - No functional-option types in component packages: options belong to `tui.ProgramOption` (`TestComponentsHaveNoFunctionalOptions`). - Every stateful widget's `Model` has `Linearize` and `LayoutNode` methods (`TestEveryStatefulWidgetHasLinearizeAndLayoutNode`). A package that legitimately lacks one goes in `a11yAllowlist` in `internal/archtest/a11y_test.go` with a real reason; the list is empty today. - A widget that handles keys for navigation also handles the mouse (`TestKeyNavigationPackagesHandleMouse`), unless it is in `mouseAllowlist` with a reason. - `Update` must not mutate a value copy of the Model. Add the new Model to `TestCopyIsolationSweep` in `internal/isolation/sweep_test.go`; the list is maintained by hand. ## Documentation `go run ./internal/tools/doccheck` (CI, Linux) fails when: - an exported identifier in a public package has no doc comment; - a package, internal ones included, has no package comment; - the `Experimental:` list in [doc.go](https://github.com/ows4444/tui/blob/code/doc.go) and the packages whose comment says `Stability: experimental.` disagree. Mark a new experimental package in both places; - a Markdown file or workflow names a dot-slash relative path or a `github.com/ows4444/tui/...` import path that does not exist. `CHANGELOG.md` is skipped. `go test ./internal/tools/doccheck` also checks specific claims in [README.md](https://tui.nizaami.com/overview/) against the code (colour detection variables, grapheme clusters, unsupported platforms, the LayoutNode and Linearize claims, and that unverified terminals and screen readers are marked so). Every public package needs at least one runnable `Example` in a `_test.go` file (`TestEveryPackageHasExample` in the root package, and `TestEveryPublicPackageHasExample` in `internal/archtest`). Code shown in Markdown should be copied from an Example so `go test` compiles it. ### Website The site at is an [Astro Starlight](https://starlight.astro.build/) project in `site/`. Its npm packages are site tooling, not module dependencies. It has no pages of its own except the landing page (`site/src/content/docs/index.mdx`, with its components in `site/src/components/`; `Hero.astro` and `Footer.astro` replace Starlight's own, and the theme is `site/src/styles/custom.css`): `site/scripts/sync-docs.mjs` generates the rest from `README.md`, `CONTRIBUTING.md`, `CHANGELOG.md` and `docs/`, and the Examples page from each example's 80×24 size-matrix screen. For colour the sync runs `go test -run 'TestSizeMatrix/^80x24$' ./examples/...` with `TUI_SCREENS_DIR` set, which makes `testutil.SizeMatrix` write each frame unstripped; a frame is used only if, stripped, it equals `testdata/size-80x24.golden`, and without Go the screens show without colour. Their categories are `EXAMPLE_CATEGORIES` in the sync script. Edit those sources, not the generated files, which are gitignored. A page added to `docs/` needs an entry in `pages` in the sync script and in the sidebar in `site/astro.config.mjs`. The sync fails on a relative link that points nowhere. For search engines and AI assistants the sync also gives each page a descriptive `` (`seoTitle` in `pages`), a meta description (the `description` in `pages`, or else the first prose sentences), JSON-LD, and a plain Markdown copy at `/<page>.md`, indexed by `/llms.txt` and joined in `/llms-full.txt`. Site-wide JSON-LD and the social image tags are in `site/astro.config.mjs`; the landing page's FAQ JSON-LD in `site/src/content/docs/index.mdx` must match the FAQ text on that page. `site/public/og.png` is rendered from `site/scripts/og-image.html`, whose header comment has the command. ```console $ cd site $ npm ci $ npm run dev # http://localhost:4321, regenerates pages on start $ npm run build # static site in site/dist/ ``` ## Coverage Each library package needs at least 90% statement coverage, counted per package with coverage credited across packages (`-coverpkg=./...`). `internal/tools/covercheck` skips `examples/` and `internal/tools/`, and any `internal/` package whose non-test source has a `//covercheck:helper <reason>` line; the reason is required. ## Generated files Don't edit these by hand. Regenerate, then commit the result. | File | Regenerate | CI check | |---|---|---| | `api.txt` | `go run ./internal/tools/apilist -w` | `apilist -check` in the `api` job | | `.golangci.yml` | `ARCHTEST_UPDATE=1 go test ./internal/archtest -run TestGolangciMirror` | `TestGolangciMirror` in `go test ./...` | | `ansi/runewidth_tables.go` | `go run ./internal/tools/genwidth -o ansi/runewidth_tables.go` | `widthtables` job | | `ansi/graphemebreak_tables.go` | `go run ./internal/tools/gengrapheme -o ansi/graphemebreak_tables.go` | `widthtables` job | | `internal/bidi/tables.go` | `go run ./internal/tools/genbidi -o internal/bidi/tables.go` | `widthtables` job | | `testdata/*.golden` | `TUITEST_UPDATE=1` or `-update`, depending on the helper; see [docs/testing.md](https://tui.nizaami.com/testing/#golden-files) | the package's tests | The three Unicode generators fetch pinned Unicode 17.0.0 files from unicode.org and verify each against a SHA-256 pin, so they need network access; `-dir` reads local copies instead. To move to a newer Unicode version, change `unicodeVersion` in the generator and run it with `-print-hashes` to get the new pins. `bench/baseline.txt` is the allocation baseline for the `bench` workflow. No script in the repository regenerates it. `api.txt` and golden files are checked out with LF line endings on every platform ([.gitattributes](https://github.com/ows4444/tui/blob/code/.gitattributes)). ## Changelog Add user-visible changes to [CHANGELOG.md](https://tui.nizaami.com/changelog/) under `## [Unreleased]`. Removing or changing an exported identifier needs a `- BREAKING:` bullet in that section naming the identifier as a whole word (`Style.Render` or `Render`), and saying what to use instead. Once a tag exists, the `api` CI job runs `go run ./internal/tools/apilist -since-tag -changelog CHANGELOG.md`, which fails on a removed or changed line of `api.txt` that no such bullet names. With no tag yet it compares nothing. ## Benchmarks The `bench` workflow ([.github/workflows/bench.yml](https://github.com/ows4444/tui/blob/code/.github/workflows/bench.yml)) runs on pull requests that touch a `.go` file, `bench/` or the workflow itself, every Monday at 06:00 UTC, and on demand. It fails when allocs/op or B/op grow more than 10% over `bench/baseline.txt` (`internal/tools/benchgate`); ns/op is not gated. To compare locally: ```sh go test -run '^$' -bench . -benchmem -count=6 . ./ansi ./layout ./virtuallist ./logview ./viewport ./textarea ./markdown ./datatable ./streamtext > bench/new.txt go run ./internal/tools/benchgate -base bench/baseline.txt -new bench/new.txt ``` `bench/new.txt` is git-ignored. --- # Changelog > User-visible changes to github.com/ows4444/tui. The format loosely follows Keep a Changelog 1.1.0. Web: https://tui.nizaami.com/changelog/ Source: https://github.com/ows4444/tui/blob/code/CHANGELOG.md User-visible changes to `github.com/ows4444/tui`. The format loosely follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). There is no tagged release yet, so everything is under Unreleased. A removed or changed exported identifier needs a `- BREAKING:` entry under Unreleased that names it; see [CONTRIBUTING.md](https://tui.nizaami.com/contributing/#changelog). ## [Unreleased] ### Deprecated - The chart functions in `widgets` (`BarChart`, `Gauge`, `HeatMap`, `LineChart`, `Sparkline`, `SparklineWith` and the `BarItem` alias) moved to `widgets/chart`; the `widgets` versions forward to them. - `layout.Box.Render`: use `layout.BoxNode(box, layout.Block(content))`, or `BoxNode` with any Node child. - `ansi.SetClusterWidth`: use an `ansi.Measurer` for anything tied to one terminal. A Program no longer calls it. ### Removed - BREAKING: `WithInputReader`, `WithOutputWriter` and `WithErrWriter` are removed. `WithInput`, `WithOutput` and `WithErrOutput` take any reader or writer: a non-file reader given to `WithInput` needs no terminal and quits the Program at EOF, as `WithInputReader` did. - BREAKING: `WithLineRenderer` is removed. `WithCellRenderer(false)` selects the line renderer. - BREAKING: `layout.JoinHorizontal`, `JoinHorizontalAlign`, `JoinVertical`, `JoinVerticalAlign`, `FlexRow`, `FlexItem`, `GridFlex`, `Grid` and `ColSpec` are removed. Use the Node constructors: `JoinHorizontal` and `FlexRow` become `layout.Row`, `JoinVertical` becomes `layout.Column`, the `*Align` variants set `FlexChild.CrossAlign` on each child, `GridFlex` and `Grid` become `layout.GridNode`, `ColSpec` becomes `layout.Track`, and `FlexItem` becomes `layout.FlexChild`. ### Fixed - With `WithRecover(true)`, a panic in a `Tick` or `FromCtx` Cmd, a `Sequence` step or an `Every` callback now makes Run return a `*PanicError`. Before, these restored the terminal and re-panicked. --- # Examples > Every program under examples/, with the 80×24 screen its test checks. Web: https://tui.nizaami.com/examples/ Every directory under [examples/](https://github.com/ows4444/tui/tree/code/examples) is a runnable program. Clone the repository and run one from its root, for example `go run ./examples/dashboard`. The screens are not mock-ups: each is the first frame the example's own size-matrix test renders at 80×24, shown with the colours its `View` produces. Only the 16 basic terminal colours depend on a palette, as in any terminal. ## Showcase Full screens that show what a tui program can look like. ### dashboard Run: `go run ./examples/dashboard` ````text ┌────────────────────────────────────────────────────────────────────────┐ │ │ │ Service Dashboard v2.1.0 │ │ ─────────────────────────────── status ─────────────────────────────── │ │ │ │ ✓ API ✓ healthy ✓ Build → ● Test → ○ Deploy │ │ ⚠ Worker Queue ⚠ degraded ⠋ Deploying build █████░░░░░░░░░░ │ │ ✗ Database ✗ down │ │ │ │ ────────────────────────────── metrics ─────────────────────────────── │ │ │ │ CPU ▅▆▄▁▁▄▂▃▃▆▂▅▃▆▇▅▂█▅▆ 69.2% │ │ Mem █▄▆▂▃▇▄▄▃▂▄▇▃▂▆▁▅▁▁▃ 30.2% │ │ Disk ███░░░░░░░░░░░░ 17.3% │ │ │ │ ────────────────────────────────────────────────────────────────────── │ │ [r] refresh [d] about [q] quit │ │ │ └────────────────────────────────────────────────────────────────────────┘ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/dashboard) ### canvas `canvas` is a custom widget that draws straight into the cell grid through DrawCells: a colour gradient with a moving marker. Run: `go run ./examples/canvas` ````text @ arrows move, q quits ```` [Source](https://github.com/ows4444/tui/tree/code/examples/canvas) ### chat `chat` is a manual showcase of the InkUI-parity widgets added after the original dashboard/form/list examples: Gauge, CodeBlock, DiffView, markdown.Render, streamtext (Typewriter), TokenCounter and layout.Column and layout.Row. Run: `go run ./examples/chat` ````text Chat session #1 ⣀⣤⣤⣤⣤⣀ ⢀⣴⠟⠉⠁ ⠈⠉⠻⣦⡀ ⢠⡟⠁ ⠈⢻⡄ ⣿⠁ 35% ⠈⣿ ┌──────────────────────────────────────────────────────────┐ │ │ │ Refactor parseArgs to return an error instead of calling │ │ os.Exit. │ │ │ │ • keep the flag order │ │ • add a regression test │ │ │ └──────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────┐ │ │ │ █ │ │ │ └──────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────┐ │ 1 │ func parseArgs(args []string) (Config, error) { │ │ 2 │ if len(args) < 2 { │ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/chat) ### welcomescreen `welcomescreen` is the canonical reference for a static informational splash composed entirely from the existing widget catalog — widgets.Panel, widgets.BigText, widgets.Header and widgets.KeyValue side by side in a layout.Row. Run: `go run ./examples/welcomescreen` ````text ╭──────────────────────────────────────────────────────────╮ │ │ │ Welcome to TUI │ │ │ │ ┌──────────────────────┐ ┌────────────────────────────┐ │ │ │ │ │ │ │ │ │ █████ █ █ ███ │ │ Info │ │ │ │ █ █ █ █ │ │ ────────────────────────── │ │ │ │ █ █ █ █ │ │ Version: v1.0.0 │ │ │ │ █ █ █ █ │ │ User: Ada Lovelace │ │ │ │ █ ██ ███ │ │ Workspace: acline │ │ │ │ │ │ │ │ │ └──────────────────────┘ └────────────────────────────┘ │ │ │ │ enter/esc continue ctrl+c quit │ │ │ ╰──────────────────────────────────────────────────────────╯ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/welcomescreen) ### splashscreen `splashscreen` follows the same composition-only shape as examples/welcomescreen and examples/loginflow: widgets.BigText for the logo and widgets.Header for a tagline, dismissed by any keypress. Run: `go run ./examples/splashscreen` ````text █████ █ █ ███ █ █ █ █ █ █ █ █ █ █ █ █ █ ██ ███ Loading your workspace… press any key to continue ```` [Source](https://github.com/ows4444/tui/tree/code/examples/splashscreen) ### probe `probe` shows what your terminal supports, using the library's opt-in capability options: colour depth (with NO_COLOR, COLORTERM and TERM honoured), focus in/out reporting, and OSC 11 background detection. Run: `go run ./examples/probe` ````text tui probe: what does your terminal do? TERM="xterm-256color" COLORTERM="truecolor" NO_COLOR="" Colour profile: truecolor (detected; drives the strip below) Focus reports: in=0 out=0 last=unknown (click another window, then back) Background: waiting theme=- (asked once at start, 700ms timeout) Last key: - Size (resize): 80x24 q or ctrl+c to quit; a summary line stays in your scrollback probe: profile=truecolor focus_in=0 focus_out=0 background=waiting theme=- focus timeline: focus reporting enabled: (not yet) ```` [Source](https://github.com/ows4444/tui/tree/code/examples/probe) ### faces `faces` is a gallery for the faces package. Run: `go run ./examples/faces` ````text faces #1 of 50 ⣠⣾⣿⣿⣷⣄ ⣿⣧⣿⣿⣼⣿ ⠙⢿⣯⣽⡿⠋ Pip · blink · 1/8 click a face to play · ←/→ change · s size · w wall · space loop · q quit ```` [Source](https://github.com/ows4444/tui/tree/code/examples/faces) ## Forms and input Text fields, sign-in and multi-step flows, focus and the cursor. ### loginflow `loginflow` is the canonical reference for a login-shaped screen composed entirely from the existing widget catalog — widgets.BigText for the title, widgets.Banner for an announcement, and picker.Model configured as a numbered-select account menu. Run: `go run ./examples/loginflow` ````text █ ██ ███ ███ █ █ █ █ █ █ █ ██ █ █ █ █ █ ██ █ █ █ █ █ █ █ █ █ █ █ ██ ████ ██ ███ ███ █ █ ℹ Pick an account to continue — no password requir > Continue as Ada Lovelace Continue as guest Quit enter select ctrl+c quit ```` [Source](https://github.com/ows4444/tui/tree/code/examples/loginflow) ### setupflow `setupflow` follows the same composition-only shape as examples/welcomescreen and examples/loginflow: widgets.BigText for the title, widgets.Alert for a status message, and picker.Model as a numbered-select menu of setup steps. Run: `go run ./examples/setupflow` ````text ███ ████ █████ █ █ ███ █ █ █ █ █ █ █ ██ ███ █ █ █ ███ █ █ █ █ █ █ ███ ████ █ ██ █ ┌────────────────────────────────────────────────┐ │ │ │ ℹ No step applied yet. │ │ │ └────────────────────────────────────────────────┘ > Configure workspace Configure notifications Finish enter apply ctrl+c quit ```` [Source](https://github.com/ows4444/tui/tree/code/examples/setupflow) ### login `login` is a credential sign-in screen built on package form, with a live design-system panel beside it for trying every theming control the library has. Run: `go run ./examples/login` ````text ┌──────────────────────────────────────────────┐ │ │ │ Sign in │ │ │ │ demo: ada / lovelace │ │ │ │ Username: ada │ │ Password: │ │ [ ] Remember me │ │ │ │ [tab] next [enter] sign in [ctrl+t] design │ │ │ └──────────────────────────────────────────────┘ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/login) ### signup `signup` is a small account form built on package form: text, secret, select and checkbox fields with validation, Tab and Shift+Tab to move between them, the arrow keys and Space to change a choice, Enter to submit (Esc quits). Run: `go run ./examples/signup` ````text ┌────────────────────────────────────────────────────┐ │ │ │ Create an account │ │ │ │ Name: Ada Lovelace │ │ Email: ada@example.com │ │ Password: │ │ Plan: < Free > │ │ [ ] Accept terms │ │ │ │ [tab] next [←→/space] change [enter] submit │ │ │ └────────────────────────────────────────────────────┘ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/signup) ### form Run: `go run ./examples/form` ````text ╭──────────────────────────────────────────────╮ │ │ │ Sign up │ │ │ │ ● Edit → ○ Confirm │ │ │ │ Name: Ada Lovelace │ │ Email: ada@example.com │ │ Language: start typing... │ │ │ │ tab next field enter next/confirm esc quit │ │ │ ╰──────────────────────────────────────────────╯ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/form) ### focus `focus` is the canonical, minimal reference for coordinating Focus()/Blur() across mixed widget types in this repo. Run: `go run ./examples/focus` ````text ╭───────────────────────────────────────────╮ │ │ │ Focus/Blur reference │ │ │ │ ╭───────────────────────────────────────╮ │ │ │Name: Ada Lovelace │ │ │ ╰───────────────────────────────────────╯ │ │ ╭───────────────────────────────────────╮ │ │ │Password: │ │ │ ╰───────────────────────────────────────╯ │ │ ╭───────────────────────────────────────╮ │ │ │a few words about you... │ │ │ ╰───────────────────────────────────────╯ │ │ │ │ tab next shift+tab prev esc/ctrl+c quit │ │ │ ╰───────────────────────────────────────────╯ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/focus) ### cursorfield `cursorfield` shows a text input whose cursor is the terminal's real cursor: the model implements tui.CursorPlacer by forwarding textinput.Model.CursorCell, so an IME or a screen magnifier can follow the field. Run: `go run ./examples/cursorfield` ````text Name (Enter quits): > ```` [Source](https://github.com/ows4444/tui/tree/code/examples/cursorfield) ## Lists, tables and navigation Selection, tabs, scrolling and switching screens. ### inspector Run: `go run ./examples/inspector` ````text ┌──────────────────────────────────────────────────────────┐ │ │ │ [Table] JSON │ │ │ │ Name Status CPU │ │ ──────────────────── │ │ > api-1 running 12% │ │ api-2 running 8% │ │ api-3 degraded 41% │ │ │ │ [←→] tabs [↑↓] move [enter] select/toggle [q] quit │ │ │ └──────────────────────────────────────────────────────────┘ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/inspector) ### table `table` is a dedicated, standalone example for datatable.Model's interactive row selection: Up/Down move the cursor and Enter confirms the row under it via SelectedMsg. Run: `go run ./examples/table` ````text Name Role ──────────────────────────────── > Ada Lovelace Engineer Grace Hopper Rear Admiral Margaret Hamilton Director Katherine Johnson Mathematician Selected: (none yet — press Enter) [↑/↓] move [enter] select [q] quit ```` [Source](https://github.com/ows4444/tui/tree/code/examples/table) ### list Run: `go run ./examples/list` ````text ╭─────────────────────────────────────────────────────────╮ ┌───────┐ │ │ │ │ │ Tasks │ │ Done │ │ │ │ │ │ > [ ] Design raw-mode terminal layer │ │ 0 / 6 │ │ [ ] Build ANSI styling package │ │ │ │ [ ] Write escape-sequence key parser │ └───────┘ │ [ ] Implement Elm-architecture event loop │ │ [ ] Add diff-based renderer │ │ [ ] Ship zero-dependency TUI │ │ │ │ up/down move space/click toggle enter confirm q quit │ │ │ ╰─────────────────────────────────────────────────────────╯ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/list) ### settings Run: `go run ./examples/settings` ````text ┌──────────────────────────────────────────────────────┐ │ │ │ [General] Advanced │ │ │ │ > ▸ Display │ │ ▸ Notifications │ │ │ │ [←→/click] tabs [↑↓] move [enter] toggle [q] quit │ │ │ └──────────────────────────────────────────────────────┘ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/settings) ### router `router` is the canonical reference for switching between independent top-level screens in this repo, the way examples/focus is the canonical reference for Focus/Blur coordination. Run: `go run ./examples/router` ````text ╭───────────────────────────╮ │ │ │ Router reference — Menu │ │ │ │ > Settings │ │ About │ │ Quit │ │ │ │ enter select ctrl+c quit │ │ │ ╰───────────────────────────╯ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/router) ### pager Run: `go run ./examples/pager` ````text ╭──────────────────────────────────────────────────────────────╮ │ │ │ Pager │ │ │ │ Paragraph 1 │ │ This from-scratch TUI framework talks to the terminal │ │ directly via raw syscall ioctls, no golang.org/x/term and no │ │ Bubble Tea. │ │ │ │ Paragraph 2 │ │ The event loop follows the Elm Architecture: a Model │ │ implements Init, Update, and View, and Program owns the │ │ terminal, the input parsing, and a line-level diffing │ │ renderer. │ │ │ ╰──────────────────────────────────────────────────────────────╯ up/down/pgup/pgdown/home/end/wheel scroll gg top G bottom q quit 0% ```` [Source](https://github.com/ows4444/tui/tree/code/examples/pager) ## Layout and rendering Composing string and cell children, and the smallest program. ### mixedscreen `mixedscreen` composes a string child and a cell child into one screen with DrawChild and DrawView. Run: `go run ./examples/mixedscreen` ````text Mixed string and cell screen █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ q quits ```` [Source](https://github.com/ows4444/tui/tree/code/examples/mixedscreen) ### counter Run: `go run ./examples/counter` ````text From-scratch TUI — Counter Count: 0 (80x24) ↑/→ increment ↓/← decrement q/ctrl+c quit ```` [Source](https://github.com/ows4444/tui/tree/code/examples/counter) ## Async and streaming Commands that resolve later, child processes and committed output. ### asyncload `asyncload` demonstrates the async-Cmd-resolves-into-a-Msg pattern: a tui.Cmd is just a func() tui.Msg, and Program.dispatch (see program.go) runs each Cmd on its own goroutine and feeds whatever Msg it returns back into the event loop. Run: `go run ./examples/asyncload` ````text ╭────────────────────╮ │ │ │ Loading... │ │ │ │ ⠋ fetching data... │ │ │ ╰────────────────────╯ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/asyncload) ### procstream `procstream` demonstrates streaming a child process's stdout into a scrolling widget (logview.Model) line by line, as it arrives, rather than buffering the whole thing and rendering it once the process exits. Run: `go run ./examples/procstream` ````text procstream — streaming a child process's stdout status: running... (q/esc to quit) ```` [Source](https://github.com/ows4444/tui/tree/code/examples/procstream) ### buildlog `buildlog` demonstrates tui.Println (see cmds.go): a Cmd that commits text permanently to the terminal's real scrollback, above the live-updating region, rather than being part of an ordinary View() repaint. Run: `go run ./examples/buildlog` ````text 0/4 steps complete ⠋ Running: Fetching dependencies... ```` [Source](https://github.com/ows4444/tui/tree/code/examples/buildlog) ### agentshell `agentshell` is a template for an agent CLI. Run: `go run ./examples/agentshell` ````text > Ask the agent (paste is fine, Enter sends) ```` [Source](https://github.com/ows4444/tui/tree/code/examples/agentshell) ## Inline mode Programs that draw in the normal scrollback instead of the alternate screen; their first frame is small by design. ### inlinespinners `inlinespinners` is an inline-mode (no alternate screen) list of tasks: finished tasks show a check, the running one a spinner, the rest wait. Run: `go run ./examples/inlinespinners` ````text ⠋ resolve [ ] fetch [ ] build [ ] verify ```` [Source](https://github.com/ows4444/tui/tree/code/examples/inlinespinners) ### inlinebuild `inlinebuild` is an inline-mode (no alternate screen) build log. Run: `go run ./examples/inlinebuild` ````text building... ```` [Source](https://github.com/ows4444/tui/tree/code/examples/inlinebuild) ### inlinechat `inlinechat` is an inline-mode (no alternate screen) chat prompt. Run: `go run ./examples/inlinechat` ````text > _ ```` [Source](https://github.com/ows4444/tui/tree/code/examples/inlinechat) ### inlinetall `inlinetall` is an inline-mode (no alternate screen) transcript that grows past the terminal height. Run: `go run ./examples/inlinetall` ````text transcript (Enter adds a line, Esc quits) ```` [Source](https://github.com/ows4444/tui/tree/code/examples/inlinetall)