Examples¶
The repository ships a launcher hosting every demo as a child screen:
git clone https://github.com/jsdrews/tuilib.git
cd tuilib
task examples # or: go run ./examples/launcher
Select a demo and press enter to open it; esc returns to the menu. Each
demo's about pane names the directory its code lives in. Filtering the menu
matches the group as well as the name, so /patterns narrows to one section.
The launcher turns on mouse support, the output console (o) and the action
menu (a) for the whole suite.
task demo does the same against a local demo API server (an HTTP service in
demoapi/ shaped like the APIs the demos model), so the remote, eventlog and
activity demos run over real HTTP. See the demo API design note.
examples/quickstart is the standalone program from the
Quick start: go run ./examples/quickstart.
Every demo is a package at examples/<area>/<name>/ exporting
New(theme.Theme) screen.Screen. To start a new screen, copy the closest one
and delete what you don't need.
examples/components/ — one per component, in isolation¶
| Entry | Demonstrates |
|---|---|
| Pane | Border styles, title positions, and slot-bracket variants in one 2×2 grid. Every interactive component owns one of these internally, which is why none of them need a wrapper |
| List | A filterable list.Model with 372 numbered rows, so every scroll affordance is reachable: drag the thumb, click the track, wheel over an unfocused pane, g/G, ctrl+u/d, double-click to open |
| Table | table.Model with a sticky header and all three column sizing modes side by side (City Flex:1 + MaxWidth:28, Region/Population fixed, Status content-auto). [/] step the sort column, s toggles direction, Population sorts numerically via a custom Less that parses "8.3M". Status uses ansi.CellColor so the selected-row background passes through colored cells; Wiki wraps each URL in ansi.Hyperlink so shift-click launches the full link even when the column truncates it |
| Tree | Synthetic project tree with cursor, expand/collapse (space), /-search, and \ filter mode that hides non-matching subtrees while keeping ancestors. Leaves carry colored status icons so the row highlight survives ANSI segments |
| Inspector | Two-column label/value viewer over a synthetic k8s-pod payload, fed through inspector.FromAny — the typical json.Unmarshal → FromMap path. Sibling labels auto-align per group; / searches labels and values |
| TextView | Two documents (README + git diff) cycled with d. /-search with n/N, w toggles wrap, g/G and ctrl+u/d scroll. No follow, no MaxLines — the read-static-text counterpart to logview |
| LogView | Streaming log tail with /-search, n/N jump, \ filter-mode toggle, current-line highlight, and a MaxLines cap so an open-ended producer can't grow memory without limit |
| Form | form.Model with Text / Select / Confirm fields + submit. Validation lives in the form, not the screen: errors render on the field's own border so nothing reflows while you correct it |
| Metrics | pkg/metrics inline primitives in a live deployments table — Ratio for replica counts, Badge for pod-state breakdown, Bar+percent for CPU, Spark for CPU history. The point is that they drop into existing components as ordinary cells: no new screens, no custom layout. Polls every 2s with a keyed cursor |
examples/shell/ — what pkg/app owns¶
| Entry | Demonstrates |
|---|---|
| Layouts | One sub-screen per layout primitive: HStack+Fixed/Flex, nested stacks, ZStack modal, and more — the answer to every "how do I leave room for a bar" question that would otherwise be written as m.h - 2 |
| Stack | Navigation plus all three directions data moves: parent→child through the constructor, child→parent through Pop(result) landing in OnEnter, and self→self through Replace. r on either screen swaps it for a fresh instance — filter and visit counter reset, depth stays put, and the screen underneath is never reactivated |
| Tabs | Three sub-screens (filterable list / streaming logview / counter) behind one strip; switch via shift+arrows, 1/2/3, or a click on a label. Each body keeps its own state, and the logs keep streaming while you are on another tab |
| Status | Screens emit app.Info / app.Error / app.ClearStatus as tea.Cmds and the shell paints the statusbar's center slot. Auto-clears on the next keypress, which is why anything worth reading twice belongs in the console |
| Output | The app-wide console: app.Info / app.ErrorDetail / app.ErrorOf, plus runner.Capture streaming a live subprocess. o opens it, c clears, x kills a running capture, w exports. Note the OnEnter guard on app.OutputClosed — closing the console must not look like a fresh activation |
| Themes | Live palette picker — moving the cursor re-skins the whole app, enter shows a theme's field palette. Also the package every other example's theme list comes from |
| Chrome | The two border-shape slots a Theme carries, one for ordinary components and one for overlays. Pick a shape and every component on screen is rebuilt from the modified theme. Set both pickers the same to see why the overlay slot is separate: the modal flattens into the content behind it |
| Prescreen | The "log in before you can use this" shape — a root screen pushes a child from OnEnter, takes its result on Pop, and can re-push it later (L logs out) without the child living permanently on the stack |
examples/patterns/ — composition idioms¶
| Entry | Demonstrates |
|---|---|
| Focus | input + list + toggle behind one focus.Group: tab cycles, only the focused component takes keys, and a clicked component gets focus because the Group sees the request it sends |
| Filters | A list and a table, each with its own filter, on one screen — the focus states a single filterable pane can't reach: one region highlighted at a time, clicking a body taking input back from its filter, switching panes clearing the filter you left, tab completing a key:value term instead of cycling panes |
| Mouse | Click to focus, click a row to select, double-click to open, click a table header to sort or a tree ▸ to expand, wheel over any pane focused or not, drag a scrollbar. Every rect comes from pkg/layout — nothing is injected into the rendered string |
| Loading | list, logview, and tree all start in SetLoading(true); staggered tea.Tick delays simulate fetches that resolve at different times. r refetches, and the spinner replaces the previous result rather than overlaying it |
| Drilldown | Master-detail with async fetches at every level. Enter on either pane "opens the focused selection": left-enter loads the detail (reqID-tagged so stale results drop) and shifts focus right, right-enter pushes a child screen |
| Poll | pkg/poll drives a 2s tick that mutates a synthetic job list; SetKeyedItems keeps the cursor on the same job ID across every refresh even as statuses flip and the list reorders. p pauses, r refreshes now, +/- adjust cadence |
| Remote | The whole windowed-source loop: pkg/source coordinating a table in FilterRemote/SortRemote over a simulated 5,000-row API that answers one 100-row page at a time, with latency L cycles from 250ms to 8s. Scroll faster than it answers and you see the · placeholders; the cursor stays put and data arrives under it. Commit a filter or sort and the rows stay on screen, dimmed, with the border naming the query they answer and the one loading; [/]/s wait for a quiet moment before asking, superseded requests are cancelled, and every query's outcome lands in the output console |
| Eventlog | pkg/eventlog bound with pkg/remote to demoapi's AWX-shaped job-events endpoints over HTTP — a running job: counter ranges, holes from late-saved events that fill in, follow with a new-events count, f to filter on the server (failed, host:web-1), / + n/N to find past what is loaded, enter to open an event, L to cycle latency |
| Eventlog (finished job) | The same over a finished AWX job of 8,000 events — the troubleshooting case: opens at the first event, nothing polls, and / + n/N find matches across the whole job, asking AWX for the next one past what is loaded |
| Anchored | pkg/eventlog bound with pkg/remote to demoapi's Elasticsearch-shaped _search over HTTP — search_after with the _shard_doc tiebreaker, a span grown at whichever edge you scroll toward, a tail that rewinds to catch late-ingested documents, and a search hit past the span that re-anchors around it |
| Pod logs | A fake pod log in pkg/logview with pkg/resume: exact reconnects at one-second resolution, marked reconnects and restarts, O to load older lines from a longer tail, P for the previous run |
| Actions | The verb menu. a or right-click opens action.Menu, sized to its widest row and anchored where you asked. Single-target on purpose. Start a Restart and reopen the menu to see the Exclusive gate; Delete confirms first |
| Multi-select | table.Model with Options.Markable + pkg/action: x marks, X (or shift+click) extends a range in either direction, A marks everything the filter shows, D drops. Marks are held by Key, so they survive filtering and a theme swap. The menu is titled with what it will act on ("3 items"), and the action that did not declare Multi dims itself with a reason |
| Tree actions | pkg/tree marking + pkg/action on a cluster hierarchy. Marking a branch marks that branch alone; the screen resolves it to the pods a verb should touch with a prefix test on the path, which is why Restart cascades into a marked namespace and Describe does not |
| Modals | The three weights of feedback on one list of operations: app.Info for a success, pkg/confirm for a destructive op, pkg/alert for a failure the user must acknowledge. "Force push main" goes through both modals in sequence. Note the two hosting shapes — fixed-size inside layout.Center vs. an autosizing alert that centers itself |
| Runner | Handing the terminal to an interactive subprocess: the TUI suspends, $EDITOR / less / man / htop gets the real TTY, and it resumes on exit. The last entry uses RunWithNotice to print "connecting…" during the handoff |
| Capture | The counterpart: runner.Capture streams a subprocess into an on-screen logview while the TUI stays live. The shell chains the reads and forwards every message, so the screen just matches CaptureStarted / CapturedLine / Captured — no io.Pipe, no goroutine, no scanner |