TermMosaic v1.0.0

v1.0.0 is the first release that makes a stability promise: the public API freezes here and Semantic Versioning applies in earnest — with one documented exception: widgets/widgettest is excluded from that promise, because it must evolve with the framework. Read the limitations before you rely on anything here.

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

List rendered by 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

Table rendered by 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:

  1. 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.
  2. Scrolling to the end leaves a column partially visible. Table keeps two horizontal offsets, and clampColOffset’s ceiling is the content’s right-hand edge, which is generally not a column start. This is deliberate and tested. A resize that changes contentW or totalW re-clamps onto the same ceiling, so the partial column can appear with no scrolling key pressed at all.
  3. 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

Tree rendered by 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

Pager rendered by 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.

Split rendered by 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”.