Guides
Data display
Choosing between List, Table, Tree and Pager, and the traps each one has.
Data display
Four data widgets: List, Table,
Tree, Pager. They share a
virtualization engine and a focus contract, and each has a trap.
Choosing
| Your data | Widget |
|---|---|
| One string per row | List |
| Rows in columns, with a header | Table |
| Nested, with expandable nodes | Tree |
| Long text to read, with search | Pager |
If two rows apply, pick on this order: is it nested? → Tree. Does it have more
than one field? → Table. Is it prose? → Pager. Otherwise → List.
The shared contract
All four are Focusable on the same terms: keys are consumed only while
focused, and a press inside the rectangle takes focus as well as selecting, so
a click is a complete interaction without you writing a click handler.
All four render only the visible rows. See Virtualization.
List
cmd/capture through
widgettest.Capture: colour at 3 widths
(40, 80, 120 columns), and the exact plain-text cell grid at
the widest of them.
MinSize() returns 10 × 3 cells.
That is the whole widget including its own chrome, and it is the
widget's assertion about itself — below it, Draw clips rather
than blanks, and what to do about that is the application's decision.
40 columns × 11 rows
╭ packages ────────────────────────────╮ │ input │ │ layout │ │> render █│ │ term █│ │ virtual █│ │ widgets/basic █│ │ widgets/block █│ │ widgets/data │ │ widgets/form │ ╰──────────────────────────────────────╯
80 columns × 11 rows
╭ packages ────────────────────────────────────────────────────────────────────╮ │ input │ │ layout │ │> render █│ │ term █│ │ virtual █│ │ widgets/basic █│ │ widgets/block █│ │ widgets/data │ │ widgets/form │ ╰──────────────────────────────────────────────────────────────────────────────╯
120 columns × 11 rows
╭ packages ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ input │ │ layout │ │> render █│ │ term █│ │ virtual █│ │ widgets/basic █│ │ widgets/block █│ │ widgets/data │ │ widgets/form │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Plain text — the exact cell grid, trimmed per line. This is the honest form: Braille and block-element glyphs can take a different advance width in a web font than a terminal gives them, which breaks the alignment they depend on.
# List — A scrollable, selectable list with a marker gutter and a scrollbar. # package: widgets/data # constructor: data.NewList(r buffer.Rect, items ...data.ListItem) *data.List # 120 columns x 11 rows, rendered through widgettest.Capture ╭ packages ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ input │ │ layout │ │> render █│ │ term █│ │ virtual █│ │ widgets/basic █│ │ widgets/block █│ │ widgets/data │ │ widgets/form │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
What you are looking at. This is the cell grid the renderer produced, in a web page. It is not a screenshot of a terminal, and it cannot show interaction, resize or animation — a capture is one settled frame after N frames of settling. Braille and Block Elements can take a different advance width in your browser's font than in a terminal, so if a gauge or a sparkline above looks wrong, the plain-text form below it is the exact output and the colour one is the decoration. The colour frames are at 40, 80, 120 columns; the plain-text frame is at 120. More on this.
The trap: a ListItem is one string. Two or more columns is a Table, and
bolting a fixed-width formatter onto list items is how you get a table that cannot
scroll horizontally and misaligns on a wide glyph.
The number that matters, and it is measured:
| Items | List |
Table |
|---|---|---|
| 10,000 | 13,320 ns | 16,801 ns |
| 100,000 | 14,242 ns | 17,885 ns |
Ten times the data for seven percent more time, at zero allocations. But it is per-frame cost: mutating the item list invalidates, and doing that every frame is a different cost from drawing.
Table
cmd/capture through
widgettest.Capture: colour at 3 widths
(40, 80, 120 columns), and the exact plain-text cell grid at
the widest of them.
MinSize() returns 12 × 4 cells.
That is the whole widget including its own chrome, and it is the
widget's assertion about itself — below it, Draw clips rather
than blanks, and what to do about that is the application's decision.
40 columns × 10 rows
╭ modules ─────────────────────────────╮ │ module size state │ │ buffer 8.2 kB stable │ │ geometry 3.1 kB stable │ │› headless 11.4 kB beta │ │ layout 6.7 kB stable │ │ render 14.9 kB beta │ │ virtual 5.0 kB alpha │ │ │ ╰──────────────────────────────────────╯
80 columns × 10 rows
╭ modules ─────────────────────────────────────────────────────────────────────╮ │ module size state │ │ buffer 8.2 kB stable │ │ geometry 3.1 kB stable │ │› headless 11.4 kB beta │ │ layout 6.7 kB stable │ │ render 14.9 kB beta │ │ virtual 5.0 kB alpha │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯
120 columns × 10 rows
╭ modules ─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ module size state │ │ buffer 8.2 kB stable │ │ geometry 3.1 kB stable │ │› headless 11.4 kB beta │ │ layout 6.7 kB stable │ │ render 14.9 kB beta │ │ virtual 5.0 kB alpha │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Plain text — the exact cell grid, trimmed per line. This is the honest form: Braille and block-element glyphs can take a different advance width in a web font than a terminal gives them, which breaks the alignment they depend on.
# Table — Rows in columns, with a header, a selection and horizontal scrolling. # package: widgets/data # constructor: data.NewTable(r buffer.Rect, cols ...data.Column) *data.Table # 120 columns x 10 rows, rendered through widgettest.Capture ╭ modules ─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ module size state │ │ buffer 8.2 kB stable │ │ geometry 3.1 kB stable │ │› headless 11.4 kB beta │ │ layout 6.7 kB stable │ │ render 14.9 kB beta │ │ virtual 5.0 kB alpha │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
What you are looking at. This is the cell grid the renderer produced, in a web page. It is not a screenshot of a terminal, and it cannot show interaction, resize or animation — a capture is one settled frame after N frames of settling. Braille and Block Elements can take a different advance width in your browser's font than in a terminal, so if a gauge or a sparkline above looks wrong, the plain-text form below it is the exact output and the colour one is the decoration. The colour frames are at 40, 80, 120 columns; the plain-text frame is at 120. More on this.
Three traps, all of which will look like bugs:
- Column widths are computed from content and available space. A column holding a widget, a multi-line cell, or anything with its own alignment is a composition problem — render that cell yourself and pass a string.
- Scrolling to the end leaves a column partially visible.
Tablekeeps two horizontal offsets, andclampColOffset’s ceiling is the content’s right-hand edge, which is generally not a column start. This is deliberate and tested. A resize that changescontentWortotalWre-clamps onto the same ceiling, so the partial column can appear with no scrolling key pressed at all. - There is no column selection in v0.3.0. Selection is one row.
One amendment trap: Header is a plain field, and toggling it invalidates.
That matters because a widget that caches column widths on Bounds() and is
handed Header = true would render the old layout permanently — nothing will
produce a different rect to repair it. Table obeys the rule; if you write a
similar widget, see Responsiveness.
Tree
cmd/capture through
widgettest.Capture: colour at 3 widths
(40, 80, 120 columns), and the exact plain-text cell grid at
the widest of them.
MinSize() returns 14 × 4 cells.
That is the whole widget including its own chrome, and it is the
widget's assertion about itself — below it, Draw clips rather
than blanks, and what to do about that is the application's decision.
40 columns × 13 rows
╭ repository ──────────────────────────╮ │ - termmosaic │ │ - buffer │ │ cell.go │ │› style.go │ │ wrap.go │ │ - render │ │ renderer.go │ │ diff.go │ │ widgets │ │ headless │ │ - examples │ ╰──────────────────────────────────────╯
80 columns × 13 rows
╭ repository ──────────────────────────────────────────────────────────────────╮ │ - termmosaic │ │ - buffer │ │ cell.go │ │› style.go │ │ wrap.go │ │ - render │ │ renderer.go │ │ diff.go │ │ widgets │ │ headless │ │ - examples │ ╰──────────────────────────────────────────────────────────────────────────────╯
120 columns × 13 rows
╭ repository ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ - termmosaic │ │ - buffer │ │ cell.go │ │› style.go │ │ wrap.go │ │ - render │ │ renderer.go │ │ diff.go │ │ widgets │ │ headless │ │ - examples │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Plain text — the exact cell grid, trimmed per line. This is the honest form: Braille and block-element glyphs can take a different advance width in a web font than a terminal gives them, which breaks the alignment they depend on.
# Tree — A hierarchy with expandable nodes, keyboard navigation and lazy row rendering. # package: widgets/data # constructor: data.NewTree(r buffer.Rect, nodes ...data.Node) *data.Tree # 120 columns x 13 rows, rendered through widgettest.Capture ╭ repository ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ - termmosaic │ │ - buffer │ │ cell.go │ │› style.go │ │ wrap.go │ │ - render │ │ renderer.go │ │ diff.go │ │ widgets │ │ headless │ │ - examples │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
What you are looking at. This is the cell grid the renderer produced, in a web page. It is not a screenshot of a terminal, and it cannot show interaction, resize or animation — a capture is one settled frame after N frames of settling. Braille and Block Elements can take a different advance width in your browser's font than in a terminal, so if a gauge or a sparkline above looks wrong, the plain-text form below it is the exact output and the colour one is the decoration. The colour frames are at 40, 80, 120 columns; the plain-text frame is at 120. More on this.
Two traps:
- Laziness is about rendering, not about your data. Only visible rows are rendered; the nodes and the open/closed flags are yours. A tree over a filesystem you have not walked is a tree over nothing.
- Expansion state is yours. There is no tree controller holding it.
And the documentation trap: expansion is state, and a still frame shows it only as twisties and indentation. The capture above cannot show you what expanding a node looks like. That is why the plain-text capture sits beside every colour one — see Limitations.
Pager
cmd/capture through
widgettest.Capture: colour at 3 widths
(40, 80, 120 columns), and the exact plain-text cell grid at
the widest of them.
MinSize() returns 10 × 4 cells.
That is the whole widget including its own chrome, and it is the
widget's assertion about itself — below it, Draw clips rather
than blanks, and what to do about that is the application's decision.
40 columns × 11 rows
╭ docs/ARCHITECTURE.md ────────────────╮ │Ln 9/46 >diff │ │cannot │ │ read the terminal, cannot sleep, │ │cannot write a byte. A widget that │ │needs │ │ input from the environment is a │ │widget that cannot be rendered │ │headlessly, │ │ and this framework refuses to │ ╰──────────────────────────────────────╯
80 columns × 11 rows
╭ docs/ARCHITECTURE.md ────────────────────────────────────────────────────────╮ │Ln 15/46 >diff │ │ Frame pacing, the dirty-rect computation and the two-tier diff all live in│ │ render. A widget never decides when it repaints; it declares what changed │ │ and the renderer works out the smallest rectangle that covers it. │ │ │ │ 3. The diff owns bytes │ │ A frame is turned into SGR sequences and cursor moves by a two-tier diff: │ │ a cell-level diff that finds the changed rectangle, and a byte-level diff │ │ within it. The encoder emits what changed and nothing else. │ ╰──────────────────────────────────────────────────────────────────────────────╯
120 columns × 11 rows
╭ docs/ARCHITECTURE.md ────────────────────────────────────────────────────────────────────────────────────────────────╮ │Ln 15/46 >diff │ │ Frame pacing, the dirty-rect computation and the two-tier diff all live in │ │ render. A widget never decides when it repaints; it declares what changed │ │ and the renderer works out the smallest rectangle that covers it. │ │ │ │ 3. The diff owns bytes │ │ A frame is turned into SGR sequences and cursor moves by a two-tier diff: │ │ a cell-level diff that finds the changed rectangle, and a byte-level diff │ │ within it. The encoder emits what changed and nothing else. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Plain text — the exact cell grid, trimmed per line. This is the honest form: Braille and block-element glyphs can take a different advance width in a web font than a terminal gives them, which breaks the alignment they depend on.
# Pager — A scrollable long text with search, match highlighting and a position readout. # package: widgets/data # constructor: data.NewPager(r buffer.Rect) *data.Pager # 120 columns x 11 rows, rendered through widgettest.Capture ╭ docs/ARCHITECTURE.md ────────────────────────────────────────────────────────────────────────────────────────────────╮ │Ln 15/46 >diff │ │ Frame pacing, the dirty-rect computation and the two-tier diff all live in │ │ render. A widget never decides when it repaints; it declares what changed │ │ and the renderer works out the smallest rectangle that covers it. │ │ │ │ 3. The diff owns bytes │ │ A frame is turned into SGR sequences and cursor moves by a two-tier diff: │ │ a cell-level diff that finds the changed rectangle, and a byte-level diff │ │ within it. The encoder emits what changed and nothing else. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
What you are looking at. This is the cell grid the renderer produced, in a web page. It is not a screenshot of a terminal, and it cannot show interaction, resize or animation — a capture is one settled frame after N frames of settling. Braille and Block Elements can take a different advance width in your browser's font than in a terminal, so if a gauge or a sparkline above looks wrong, the plain-text form below it is the exact output and the colour one is the decoration. The colour frames are at 40, 80, 120 columns; the plain-text frame is at 120. More on this.
It is read-only by construction. It cannot be edited, deliberately: a pager
that accepted keystrokes would need a caret, a selection and an undo history to
be worth having. Editing long text is TextArea, with the
caveat that it has no rendered selection in v0.3.0.
Search is a key contract, not a live filter. You set the query, the pager highlights matches and reports the count in its status line, and moving between them is a key. Search-as-you-type means building it yourself.
There is no pager selection in v0.3.0. It shows and searches; it hands nothing back. If you need the user to pick a line, that is a different design and you should expect to build it.
Master/detail
Two panes with a list driving a detail view is Split:
split.New(layout.DirectionRow, list, detail)
Both panes are focusable, Tab moves between them, and the focused pane is the one
that takes keys. The Split capture shows the focused pane’s marker in the second
pane’s title — and note that a still frame cannot show the Tab that got you
there.
cmd/capture through
widgettest.Capture: colour at 3 widths
(40, 80, 120 columns), and the exact plain-text cell grid at
the widest of them.
MinSize() returns 3 × 1 cells.
That is the whole widget including its own chrome, and it is the
widget's assertion about itself — below it, Draw clips rather
than blanks, and what to do about that is the application's decision.
40 columns × 11 rows
╭ modules ────────────╮ ╭ packages ────╮ │ module size │ │ input │ │ buffer 8.2 … │ │ layout │ │ geometry 3.1 … │ │> render █│ │› headless 11.4… │ │ term █│ │ layout 6.7 … │ │ virtual █│ │ render 14.9… │ │ widgets/b…█│ │ virtual 5.0 … │ │ widgets/b…█│ │ │ │ widgets/d… │ │ │ │ widgets/f… │ ╰─────────────────────╯ ╰──────────────╯
80 columns × 11 rows
╭ modules ────────────────────────────────────╮ ╭ packages ────────────────────╮ │ module size state │ │ input │ │ buffer 8.2 kB stable │ │ layout │ │ geometry 3.1 kB stable │ │> render █│ │› headless 11.4 kB beta │ │ term █│ │ layout 6.7 kB stable │ │ virtual █│ │ render 14.9 kB beta │ │ widgets/basic █│ │ virtual 5.0 kB alpha │ │ widgets/block █│ │ │ │ widgets/data │ │ │ │ widgets/form │ ╰─────────────────────────────────────────────╯ ╰──────────────────────────────╯
120 columns × 11 rows
╭ modules ────────────────────────────────────────────────────────────╮ ╭ packages ────────────────────────────────────╮ │ module size state │ │ input │ │ buffer 8.2 kB stable │ │ layout │ │ geometry 3.1 kB stable │ │> render █│ │› headless 11.4 kB beta │ │ term █│ │ layout 6.7 kB stable │ │ virtual █│ │ render 14.9 kB beta │ │ widgets/basic █│ │ virtual 5.0 kB alpha │ │ widgets/block █│ │ │ │ widgets/data │ │ │ │ widgets/form │ ╰─────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────╯
Plain text — the exact cell grid, trimmed per line. This is the honest form: Braille and block-element glyphs can take a different advance width in a web font than a terminal gives them, which breaks the alignment they depend on.
# Split — A container that divides its rectangle among focusable panes. # package: widgets/split # constructor: split.New(d layout.Direction, panes ...termmosaic.Widget) *split.Split # 120 columns x 11 rows, rendered through widgettest.Capture ╭ modules ────────────────────────────────────────────────────────────╮ ╭ packages ────────────────────────────────────╮ │ module size state │ │ input │ │ buffer 8.2 kB stable │ │ layout │ │ geometry 3.1 kB stable │ │> render █│ │› headless 11.4 kB beta │ │ term █│ │ layout 6.7 kB stable │ │ virtual █│ │ render 14.9 kB beta │ │ widgets/basic █│ │ virtual 5.0 kB alpha │ │ widgets/block █│ │ │ │ widgets/data │ │ │ │ widgets/form │ ╰─────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────╯
What you are looking at. This is the cell grid the renderer produced, in a web page. It is not a screenshot of a terminal, and it cannot show interaction, resize or animation — a capture is one settled frame after N frames of settling. Braille and Block Elements can take a different advance width in your browser's font than in a terminal, so if a gauge or a sparkline above looks wrong, the plain-text form below it is the exact output and the colour one is the decoration. The colour frames are at 40, 80, 120 columns; the plain-text frame is at 120. More on this.
Numbers beside names
When the number is the point, print it. Every widget here truncates or marks a
value too wide rather than letting it bleed into the frame, and
BarChart puts each value beside each bar precisely
because bar colour alone is not a reading.
See Dashboards for putting several of these on one screen.
Reading next
- Virtualization — what the flat-cost claim does and does not cover.
- Dashboards — composing these into a screen.
List,Table,Tree,Pager— each with its full key contract and its “when not to use it”.