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.

Widgets widgets/data

Pager

A scrollable long text with search, match highlighting and a position readout.

A scrollable long text with search, match highlighting and a position readout.

Package widgets/data. API reference: pkg.go.dev/widgets/data.

Pager is a read-only view over a large text, wrapped to its width, with search.

It is Focusable: keys are consumed only while it has focus. It cannot be edited — that is what TextArea in widgets/form is for, and a pager that accepted keystrokes would need a caret, a selection and an undo history to be worth having.

Rendered output

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.

Package context

Package data provides the catalog’s scrolling data widgets: List, Table, Tree and Pager.

  • Its space is Bounds(), never buf.Size() (ADR 0007 §1 rule 1). The only place a screen dimension is read is inside a test.
  • It repaints its whole Bounds() before drawing content (rule 3). Every widget here does that by composing widgets/block.Block, whose Draw fills the rect in Background before it draws anything else — the sanctioned way to express it (ADR 0008 §2). No widget in this package draws a border, a corner or a title itself.
  • Its Draw is allocation-free (ADR 0008 §4). Anything derived from a widget’s own size — which columns are visible, which chrome survives the budget, which rows map to which items — is computed in the size-change check, cached against the rect it was computed for, and only read in Draw.
  • Draw is total. Every rect including 0x0 and 1x1 is defined, and a widget below its own MinSize draws its minimum layout clipped rather than blanking itself (ADR 0007 §4).
  • Colour is never the only signal. Selection is carried by a marker column as well as by SelectedStyle, the tree’s expansion state by a glyph, and the pager’s matches by a rendition attribute rather than a hue.

Constructing it

data.NewPager(r buffer.Rect) *data.Pager

NewPager takes only a rect; the text arrives through SetText. It is read-only by construction.

  • Status — bool, via SetStatus(on) — draw the status line: position readout and match count. It was an exported field until v0.5.0; the setter drops the layout cache, which is what makes toggling it safe at an unchanged rect. Read it back with Status().
  • MatchStyle — buffer.Style — a search match. The match is also counted and reported in the status line, so it is not colour-only.

Key contract

Consumed only while focused.

Keys Effect
up / down scroll one visual row
page up/down scroll one screen
home / end first / last screen
n jump to the next match of the current query
N (shift+n) jump to the previous match
wheel up/down scroll WITHOUT moving anything else
press move the caret to the pressed position — a pager has none, so a press is consumed and does nothing but take focus

The QUERY ITSELF IS THE APPLICATION’S: SetQuery takes it, because a read-only widget has no way to receive typed text without becoming an editor. KeyTab is NOT consumed.

What it costs

O(visible cells) per frame: the wrap of the visible lines is recomputed each frame into a scratch buffer the pager owns, so there is no cache to invalidate on a resize and nothing to allocate. Text can be any size; TestPagerScrollsAHugeDocument is the evidence.

When not to use it

Do not use it for text the user must edit. 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.

Do not use it for short text. A paragraph that fits does not need a scrollbar, a search field and a status line.

Do not use it as the only view of something the user needs to select from. There is no pager selection in v0.3.0. It shows and searches; it does not hand back a line.

Do not expect search to be interactive as you type. Search is a key contract, not a live filter: you set the query, the pager highlights matches and reports the count, and moving between them is a key. Designing around a search-as-you-type expectation means building it yourself.

Instead: Paragraph when it fits, TextArea to edit it.