Library rules¶
tuilib is opinionated. These rules keep apps built on it consistent with each
other and with the library's own components. The full text, with the reasoning
and the anti-patterns each rule prevents, is in the repository's
CLAUDE.md, which
is written for people and coding agents alike.
Structure¶
- Default to
pkg/app+pkg/screen+pkg/layout. A tuilib app is a root screen handed toapp.New. Drop to a baretea.Modelonly when you need something the shell's shape can't express. - Describe layout declaratively.
VStack/HStack/ZStackwithFixed/Flex. Neverheight - 2. - Start from a theme builder.
t.List(),t.Table(), …; set only instance-specific fields. Don't setWidth/Heighton components placed withlayout.Sized. - Rebuild in
SetTheme, carryingState()/Restore(). Never restore field by field. - Gate global keys with
IsCapturingKeys(). True while any embedded field or filter owns input, or a modal is up. - Forward every message. Keys and mouse go to the top screen; everything else reaches every screen on the stack. In a focus group, keys go to the focused component and mouse to all.
- Pass data through the stack. Constructor parameters down,
screen.Pop(result)→OnEnter(result)up. - Make interaction menu-driven. Lists and
enter, or the action menu, for per-screen verbs. Single letters for app-wide affordances only. - Components own their pane. Label with
Title; never wrap a component in another pane or put a label line above it. Filters live inside their pane. - Expose
Help()andHelpSections(). Groups are named by function from the shared vocabulary, never after their owner.
Interaction¶
- Arrows and
hjklare for scrolling, library-wide, including the mouse wheel. No component binds them to anything else. entermeans "open the focused selection"; double-click is the same verb. Route both throughIsActivate.tab/shift+tabcycle focus; tabs switch withshift+←/→and1–9.- Click focuses and selects; it's always safe. Only double-click opens.
- Override keys through
Options.Keys, never a package-level keymap. - Compose focus with
focus.Group, never an int index. - Mouse comes from rects, never markers injected into the output.
Feedback¶
app.Info/app.Errorfor passing notices; they clear on the next key.- Verbose output goes to the console (
app.ErrorOf,InfoDetail,runner.Capture), never into the statusbar. pkg/confirmfor yes/no,pkg/alertfor acknowledge. Don't roll your own modal.- Ignore
app.OutputClosedinOnEnter. - Severity comes from exit status, never from stderr.
Data¶
SetLoadingwhile a whole body is in flight; drop stale data first.- Let the pane truncate long lines; pre-wrap only prose.
- Color table cells with
ansi.CellColor, and links withansi.Hyperlink. - Auto-refresh with
pkg/polland keyed rows, so the cursor and marks follow keys, not positions. - Cap streaming buffers. Use
-1only for bounded producers. - Remote data: bind with
pkg/remote; scrolling is the pagination. No page keys, noSetRowsper page, noSetLoadingorSetCursor(0)in the loop, no query per keystroke. - Choose the component by the data's shape (Record / Event / Line × Seekable / Anchored / Streamed). Events never go in a table; search jumps, filter narrows.
- Multi-select needs keyed data; read
Selection(), notMarks(). - Row activity follows the server. Pick
ObservedorHeldby who knows when the work ends; switch on whatDonereturns; no hold timers, no outcome glyphs.
Subprocesses¶
runner.Runhands over the terminal;runner.Capturekeeps the TUI live. Don't calltea.ExecProcessdirectly or hand-roll a pipe reader.- Stream other producers through a chained
tea.Cmd: one goroutine owns the source, a command reads one item,Updatere-dispatches. No goroutine touches a model.