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.

TermMosaic

A terminal UI framework for Go: a cell-buffer renderer with two-tier diffing and 24 ready-to-use widgets. v1.0.0.

A terminal UI framework for Go

TermMosaic is a cell-buffer renderer plus a catalog of 24 widgets. The renderer double-buffers cells, diffs at two tiers, and tracks dirty rectangles; the catalog is the part that is the point — including the five measurement widgets that comparable Go TUIs do not ship.

v1.0.0 is the first release that makes a stability promise. The public API freezes there and Semantic Versioning applies in earnest — a behaviour change means a minor, not a quiet patch — with one documented exception: widgets/widgettest, the test harness, is excluded from that promise, because it must evolve with the framework. Everything on this site is accurate as of v1.0.0 (released 2026-10-06). Read Limitations before you rely on any of it — the honest list is short, specific, and load-bearing, and it names what is still open at this release.

go get github.com/serkanalgur/termmosaic@v1.0.0

Then read the quickstart, or run the example:

go run github.com/serkanalgur/termmosaic/examples/markets@v1.0.0

What’s new in v1.0.0

The first release that makes a stability promise — and the reason it is v1.0.0 rather than v0.8.0 is a behaviour change below.

  • The public API freezes here. From v1.0.0 the project follows Semantic Versioning in earnest: a behaviour change means a minor, not a quiet patch — with one documented exception: widgets/widgettest, the test harness, is excluded from the promise, because it must evolve with the framework. Every release before this was a pre-release under a break-without-notice policy, and v1.0.0 retires that paragraph. One honesty note the release carries itself: the gate’s SemVer criterion is recorded PARTIALLY MET, because v0.5.1, v0.5.2 and v0.6.1 were patch numbers that carried behaviour changes. The promise starts at v1.0.0; it is not retroactive.
  • The colour quantiser was replaced, and that is a behaviour change. PR #18 audited the 256/16-colour selection with CIEDE2000 and found the “redmean” mapping inert — rmean/256 and (255-rmean)/256 divide to zero in uint8 arithmetic, so both weights were identically 2 and the formula was fixed 2*dr²+4*dg²+2*db² in gamma-space RGB, flipping the hue of plausible UI colours. Measured selection error before: 256 rung max 21.201 (19.35% above the just-noticeable difference), 16 rung max 36.821 (37.50% above), with markets.down collapsing to grey at the 16 rung and colliding with markets.flat. PR #19 replaced the quantiser with Lab-space (CIEDE2000) selection through the existing buffer.Quantiser hook: selection error is 0.000 on both rungs, 0 of 281,216 colour-rungs regressed, and the frame path went 224.8 → 6.6 ns/op at 0 allocs. The bytes a program emits at the 256 and 16 colour rungs change. The colour model moved PROPOSED → DECIDED on measurement — decided because it was measured, not asserted.
  • Every widget now has a runnable example. 74 func Example functions covering all 24 catalog widgets, in the eight widgets/*/example_test.go files — each rendered through widgets/widgettest, so its // Output comment is the cell grid the renderer produced. This closes the framework’s largest unmet criterion (“every widget has a runnable example”). Test-only: no behaviour change.
  • Platform claim narrowed to Linux and macOS (ADR 0001). Windows is a deliberate loud-error stub, out of scope for v1.0.0 — every console operation fails loudly instead of half-working. The Windows CI test leg was dropped: the full test suite on a Windows runner spent minutes asserting only that the stub returns its documented error. CI is now 11 required checks: test (ubuntu-24.04), test (macos-15), gofmt, golangci-lint, zero-allocation diff, and six cross-compile legs (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64) — Windows is still build-verified, just not test-run.
  • Two CI correctness fixes: -count=1 on test runs (a manual re-run could otherwise serve a cached PASS instead of re-executing) and cache: true on the setup-go steps that did not declare it.

Open at this release — decided by nobody. v1.0.0 does not close these, and reading the release as closure would be reading the maintainer’s mind: the macOS test leg still runs and a further reduction has been discussed but not done; and deleteBranchOnMerge is false at repo level. One item that was undecided here has since been settled — widgets/widgettest is excluded from the v1.0.0 stability promise (decided 2026-10-06), because a test harness must evolve with the framework. The full list is on Limitations.

What’s new in v0.7.0

A minor bump, and the reason is a fourth example: examples/search — search and results on real Wikipedia data, no API key and nothing to sign up for. A form.TextInput query field, a data.Table of results, and a detail pane fed by the article-summary endpoint. --offline runs the whole screen on a transcribed capture, so it needs no network at all.

  • It is the first example with a focusable widget in the focus ring, and that is what makes it worth reading rather than just running. hello and markets compose screens out of widgets that hold no keyboard focus — their focus ring is an integer the screen owns. Here TextInput and Table both implement Focusable and both have key contracts of their own, so this is the first place the catalog and keymap meet under real conditions.
  • Table over List, deliberately. Word counts span two orders of magnitude, and a right-aligned fixed column lets the eye find the longest article by shape; a List renders one string per item and would have had the spacing built in by hand.
  • Nine golden files and 74 tests, all asserting on cells through the headless harness. No escape-sequence assertion anywhere.
  • No capture on this site moved, and that is expected: the search example is not in the widget catalog, so no widget’s rendering changed.

Two findings worth knowing if you build a screen with real focus: context-dependence is expressed with Command.Enabled rather than ScopeFocus (no catalog widget implements Commandable, so a focus-scoped binding would mean the application declaring keys on a widget’s behalf); and because Registry.SetFocus still does not exist, Describe(ScopeFocus) is over-inclusive rather than incomplete — fine for a help screen, wrong for a hint line. Both are in full on the Limitations page.

What’s new in v0.6.1

A minor bump, and the reason is that the first application to actually use keymap found a shape the specification had not considered.

  • keymap.Registry.DescribeGrouped(scope) — one Entry per command, carrying every chord in scope for it. Describe stays one per chord, which is what ADR 0009 §9 specifies for a command palette, where a row consumes Chords[0]. The difference is the consumer: form.KeyHint.SetEntries joins an entry’s chords into one label, so a hint line fed Describe printed a three-chord command’s description three times. examples/hello worked around it with its own merge; that is now a framework function.
  • examples/hello dispatches through keymap — six commands, twelve chords, and both the pinned hint line and the ? overlay render from the registry. A binding and its description are written once; the hand-written hint string and the test that checked it against Handle are both gone. This retires risk 5 of ADR 0009.

Two visible consequences, if you took hello as near-enough-correct. The pinned hint line’s text changed — it now reads [Q q Esc Ctrl+c] quit · [?] toggle the keys — and the ? help overlay went from two rows to three, because Describe spells every chord in full rather than saying “arrows”. The key contract did not otherwise change: same keys, same behaviour, and Widget.Handle claims nothing.

What’s new in v0.6.0

A minor bump, and the reason is the longest-deferred item in the project: keymap shipped, after slipping v0.3.0 and v0.4.0.

If you are on v0.5.x, the upgrade is additive. Widget, Event, Key and Mouse are unchanged, and Widget.Handle’s interface is byte-identical — only its doc comment changed, to state that events reach a widget after your keymap has declined them. There is no migration to perform, and a program that never touches keymap behaves exactly as before.

  • Named commands, and a key is one way to invoke one. A new package holding Command, CommandID, Binding, Entry, Ctx and a 16-byte comparable Chord, with ParseChord/ChordOf as the single notation function in both directions.
  • Resolution by context specificity: focus, then screen, then global, with no numeric priority. Dispatch walks a pre-built candidate slice in rank order, which is what lets both Enabled and Run decline and fall through. Ties break on registration order.
  • Dispatch is 0 allocs/op on every event kind, measured and pinned across all eleven paths in ADR 0009 §2’s table — miss, match, Enabled nil, Enabled non-nil, Run declining, paste, resize and each mouse case. 91 ns/op for a hit, 29.5 ns/op for a miss, both zero-alloc.
  • Describe as the single source of discoverability data, plus Chords, and KeyHint.SetEntries so help renders from the registry rather than from a hand-maintained second list.
  • ADR 0009 gained five corrections where its code did not compile or contradicted itself. The two worth naming: Ctx is 152 bytes, not the 128 the prose claimed, pinned by a test with the arithmetic in the comment; and the precedence table contradicted itself on overrides, where specificity wins and an override is the within-scope tiebreak — the only reading under which a user’s global Esc does not steal a dialog’s.

What this release does not do, plainly: there is no palette. ADR 0009 §9 puts a Ctrl+K palette in scope and explicitly not in that ADR. There is also no catalog widget implementing the optional Commandable or Clickable, and a key the keymap consumes shadows a widget’s own switch without the registry being able to report it — it is told a widget’s bounds and published chords, never what its Handle does. The full list is on Limitations.

What’s new in v0.5.2

A minor bump, and the reason is a decision with a number attached: ADR 0010 settles who receives a mouse event — and the answer exposed three widgets that were getting it wrong.

  • form.Tabs, form.Select and form.Radio consumed a wheel notch regardless of where the pointer was. Tabs.Handle tested the wheel before the switch ev.Kind, so a notch never reached the bounds check its click path already used; Select and Radio reached the same place through the shared optionlist helper. A tab row, a select or a radio group took the wheel from whatever sat beneath it. Per ADR 0010, a widget handles a pointer event only when the pointer is inside its Bounds() — with two stated exemptions: a release ends a drag wherever the pointer is, and a drag continues outside Bounds once a press has claimed it.
  • examples/markets behaviour changes. Its tab row is first in the focus ring, so it swallowed every notch in the app. A notch over a KPI tile, which no widget in the ring owns, is now consumed by nobody rather than scrolling the tab row by three.

The ADR also states which widgets decline a wheel outright: Button, Checkbox, Toggle and Split have nothing to scroll, and TextInput/TextArea decline by decision — TextArea wheel-to-scroll is plausible, but adding it under a routing ADR would answer a different question.

What’s new in v0.5.1

A minor bump, and the reason is seven defects: the style a widget computes for one cell was stopping where two code paths diverged, and never reaching the text beside it. Three of the five releases before this existed because of this one class.

  • Dialog: ChoiceFocusStyle never reached the choice label — the row was filled and marked in the focus style while its text was written in the unfocused style. With default styles the focused choice rendered attr=none on an attr=reverse row: unreadable dark-on-dark, on exactly the row the reader is meant to look at.
  • Button: FocusStyle and DisabledStyle reached the ring and the fill but not the label, so a disabled button rendered blue brackets around default-coloured text. The field’s own documentation already promised otherwise.
  • Table: SelectedStyle and ItemStyle filled the row but never reached the cell text, and HeaderStyle was documented as “patched with ItemStyle” and never was. Both now do what their docs said. The trade, stated plainly: on the selected row a cell’s own Style and its column’s CellStyle no longer show, because the row style now overrides a single-span cell — a multi-span cell keeps its own. And the header now carries ItemStyle’s foreground as well as its background, because Patch takes both.
  • Menu: the check glyph and the submenu arrow were drawn in ItemStyle on the selected row, where the row is reversed — the one unreadable thing on it.
  • Radio: the focus gutter was styled as a focus mark on every row, so an unfocused group painted a reverse-video stripe down its left edge. The column still exists on every row so labels align; only the rendition was wrong.
  • BarChart: axis labels overran their column — a computed centring offset was discarded with _ = lx, so a label wider than its column overwrote the next category’s.

Every one of these renders differently from v0.4.x because it now renders correctly. If your app looks different after upgrading, that is this release working. The full detail is on the Limitations page.

What’s new in v0.5.0

A minor bump, and the reason is the first item: five exported fields are now private, so a program that assigns them will not compile. Start here if you are arriving at v0.5.x from v0.4.x.

Breaking — Pager.Status, Split.Spacing, Meter.ShowValue, ProgressBar.Label and ProgressBar.Percentage are no longer exported fields. Each already had a working setter, so migration is mechanical:

Before After
p.Status = on p.SetStatus(on)
s.Spacing = n s.SetSpacing(n)
m.ShowValue = on m.SetShowValue(on)
p.Label = s p.SetLabel(s, st)
p.Percentage = on p.SetPercentage(on)

Read-only accessors exist too: Status(), Spacing(), ShowValue(), Percentage(), Label(). The reason is in ADR 0007 §3: a widget caches its derived layout keyed on Bounds(), so a field that changes without an Invalidate() produces a stale layout that nothing ever repairs. A doc comment saying “call Invalidate after assigning” is a rule with no enforcement, no compile error and no reminder.

  • Eight widgets kept a stale layout cache after a documented setter — found by the new cache-audit mode, not by review: Pager.SetStatus, Select.SetMarker, BarChart.SetData, Meter.SetShowValue, ProgressBar.SetLabel/SetLabelSpans/SetPercentage, Sparkline.SetValues, and Split.Spacing via direct assignment. Each now drops the cached derivation.
  • A cache-audit mode, specified as ADR 0007 §3’s deferred “expensive half”: it corrupts a widget’s cached derivation after a Draw and asserts the next frame is byte-identical, so this defect class is caught mechanically. Opt-in via render.Config.CacheAudit, and zero-allocation when disabled.
  • widgets/cacheaudit now fails the build when a widget in the catalog is flagged, on all three platforms via the existing go test ./... -race job.

The gate covers the transitions it names, so the exported raw fields that remain — Select.Marker, Gauge.ShowValue, BarChart.ShowValue/Vertical/Data, Sparkline.Values/Braille, TextInput.Placeholder, Checkbox.TriState — are the same shape but produce no finding today. See Limitations.

What’s new in v0.4.1

A patch, and a dependency-only one: golang.org/x/term v0.27.0 → v0.29.0 and golang.org/x/sys v0.28.0 → v0.30.0. No library code changed, so there is nothing to adapt to, and the Go 1.23 floor is preserved — the pin still keeps x/term off the releases that would raise it. That is the whole release; it answers framework issue #4.

What’s new in v0.4.0

A minor bump, and the reason is one fix: styling you set on a Tree used to be silently discarded, and is now honoured. That is a behavioural change, so it is a minor bump and not a patch.

If your tree looks different after upgrading, this is why — and if you had worked around the old behaviour by compensating in your own styles, remove that compensation now, because it is double-counting.

  • Tree ignored per-node and selected-row styling on the label. The row painter computed the right style for each node — the node’s Style, falling back to ItemStyle, and SelectedStyle outright when the row was selected — and then applied it only to the expander glyph, writing the label through a call that took no style at all. So the label was drawn with whatever the terminal default happened to be, and only the little marker beside it was styled. All three now apply to the label, and the selected style wins on the selected row. A deliberately multi-span label keeps its per-span styles, and the fill covers the label region only — it cannot reach the marker or the indent.

This closes the defect class rather than opening one. v0.3.0 fixed List and Table; Tree was missed because its row painter computed the style for one cell and then took a different call for the text beside it, so the computed style stopped exactly where the two code paths diverged.

The rest of this release is CI and tooling, not the library: every GitHub Actions action moved to a Node 24 major (checkout v4→v7, setup-go v5→v7, golangci-lint-action v7→v9, github-script v7→v9) and every runner image is pinned by name instead of tracking -latest (ubuntu-24.04, macos-15, windows-2025). All twelve checks then ran green on the v0.4.0 pull request, and the post-merge run on main was green as well. What that run still does not cover is recorded on the Limitations page rather than glossed.

No capture on this site moved, and that is expected: the generator’s tree entry sets no styles, so the fix is inert there. The full rationale is on the Limitations page.

What’s new in v0.3.0

A minor bump, and the reason is the two fixes below: styling you set on a List or a Table used to be silently discarded, and is now honoured. That is a behavioural change, so it is a minor bump and not a patch.

If your widgets look different after upgrading, this is why — and if you had worked around the old behaviour by compensating in your own styles, remove that compensation now, because it is double-counting.

  • List ignored per-item and selected-row styling. The style computed for each row was never passed to the paint call, so it was computed and thrown away. SelectedStyle, documented as the selected row’s rendition, reached only the background fill and never the glyphs. ItemStyle and SelectedStyle now both apply, with the selected style winning on the selected row. A deliberately multi-span row keeps its per-span styles — flattening one would mean allocating on the frame path.
  • Table discarded every cell style. The sentinel meaning “write these spans verbatim” was a resolved style rather than an unset one, so the guard never fired and each cell’s own Style was overwritten with the terminal default, along with every column’s CellStyle. Both now render as given. HeadingStyle was never affected and still works.

Both were documented as taking effect already, so these are fixes to behaviour that contradicted the documentation. The full rationale is on the Limitations page, and the captures on this site have been regenerated to show the corrected rendering.

What’s new in v0.2.0

A minor bump, and the reason is the framework fix below: Renderer.Post now wakes the frame pacer, which is a behavioural change. If you are on v0.1.0 and your app updated the screen from Post, it was almost certainly frozen.

Fixed

  • Renderer.Post never woke the pacer, so async apps froze. Post queued work, callbacks only ran inside Render, and Render was gated on NeedsFrame() — which cannot become true until the callback runs. An app that updates the screen from Post, which ADR 0003 documents as the safe way to mutate widget state, painted one frame and idled forever. examples/markets hit it: live runs painted one empty frame and stopped. A regression test now covers it.
  • The diff emitted a cursor move before every wide glyph. Run suppression assumed one cell per rune, but a wide glyph advances the cursor by two. On 6,000 wide glyphs that was 6,000 cursor moves against the narrow scene’s 30, and 68,832 bytes against 6,233 — about 11× — for identical output. It now advances by the glyph’s cell width: 60 moves, 19,443 bytes, 3.12×. The ASCII path is unchanged at ~7,200 ns/op, 0 allocs.
  • BarChart drew every horizontal category label at absolute column 0 — a widget writing outside its own rectangle.

Added

  • Menu — a navigable tree with submenus to arbitrary depth.
  • Dialog — a modal with info, confirm and choice variants that traps keys and restores focus on close.
  • examples/markets — a live Grafana-style finance dashboard: ECB FX from Frankfurter and crypto from CoinGecko, no API key, and --offline for bundled sample data.
  • Keyboard and mouse in both examples. hello gains a focus ring and a ? help overlay; markets gains a two-entry focus ring, per-panel key routing, wheel and click, pause, and a help overlay.
  • ADR 0009 — the command and keymap layer. Specified, not implemented: there is no keymap package and no command palette yet, and widgets still dispatch their own keys. (Accurate as of v0.2.0. The package shipped in v0.6.0; the palette still does not exist.)

Changed

  • examples/hello is genuinely responsive. It used Max(46) inside two Fill(1)s, so it shrank but never grew — a 200×60 terminal still drew a 46×9 block floating in the middle. It now spans the terminal, re-arranges across four bands, and below 38×8 says so in one line rather than clipping.
  • The catalog is 24 widgets, up from 22.

Full detail, including the Known Limitations section, is in the framework’s CHANGELOG.md.

What is measured, and what is not

These are the only performance figures on this site. They come from the framework’s own benchmarks and its docs/STATUS.md, and nothing here is an estimate.

Property Measured Source
Two-tier diff vs a full repaint, 200×60 scene that is 99% static chrome 141 bytes written where a full repaint writes 19,979 — about 141× fewer, at ~7,133 ns/op, 0 allocs/op ADR 0001, ADR 0002
Allocations on the frame path 0 per frame, asserted by a test rather than benchmarked ADR 0008
List render cost, 10,000 items → 100,000 items 13,320 ns → 14,242 ns — 7% for ten times the data, both at zero allocations widgets/data benchmarks
Table render cost, 10,000 items → 100,000 items 16,801 ns → 17,885 ns, same shape widgets/data benchmarks
The key-decoding path 0 allocations, pinned by test ADR 0005
tcell’s flush, same one-row-dirty workload 280,814 ns/op against our 7,133 ns/op — the measurement that settled ADR 0001 ADR 0001
Colour-quantiser selection, steady state (v1.0.0) Nearest256 224.8 → 6.611 ns/op, Nearest16 16.02 → 7.126 ns/op, both 0 allocs — the Lab/CIEDE2000 replacement selects through a per-colour memo, so only the first use of a colour pays for the exhaustive search Framework buffer benchmarks, recorded in the v1.0.0 release notes

There are no benchmarks on this site for the widgets’ visual output, for frame pacing under load, or for drag-resize. The resize costs in ADR 0007 are derived from existing code and from ADR 0002/0003’s numbers; no resize has ever been observed against a real terminal being dragged. docs/STATUS.md says so itself and this site does not paper over it.

BarChart 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 8 × 6 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

╭ allocations per frame ───────────────╮
│  412     968     1284   2140    356  │
│                        █             │
│                        █             │
│                ▃       █             │
│        ▂       █       █             │
│        █       █       █             │
│▄       █       █       █      ▂      │
│█       █       █       █      █      │
│-buffer--render-widgets-headle…-input-│
╰──────────────────────────────────────╯

80 columns × 11 rows

╭ allocations per frame ───────────────────────────────────────────────────────╮
│      412             968             1284           2140            356      │
│                                                █                             │
│                                                █                             │
│                                ▃               █                             │
│                ▂               █               █                             │
│                █               █               █                             │
│▄               █               █               █              ▂              │
│█               █               █               █              █              │
│-----buffer----------render---------widgets--------headless---------input-----│
╰──────────────────────────────────────────────────────────────────────────────╯

120 columns × 11 rows

╭ allocations per frame ───────────────────────────────────────────────────────────────────────────────────────────────╮
│          412                     968                     1284                   2140                    356          │
│                                                                        █                                             │
│                                                                        █                                             │
│                                                ▃                       █                                             │
│                        ▂                       █                       █                                             │
│                        █                       █                       █                                             │
│▄                       █                       █                       █                      ▂                      │
│█                       █                       █                       █                      █                      │
│---------buffer------------------render-----------------widgets----------------headless-----------------input---------│
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

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.

# BarChart — Categorical magnitudes as horizontal or vertical bars, with an axis and per-bar values.
# package: widgets/viz
# constructor: viz.NewBarChart(r buffer.Rect) *viz.BarChart
# 120 columns x 11 rows, rendered through widgettest.Capture

╭ allocations per frame ───────────────────────────────────────────────────────────────────────────────────────────────╮
│          412                     968                     1284                   2140                    356          │
│                                                                        █                                             │
│                                                                        █                                             │
│                                                ▃                       █                                             │
│                        ▂                       █                       █                                             │
│                        █                       █                       █                                             │
│▄                       █                       █                       █                      ▂                      │
│█                       █                       █                       █                      █                      │
│---------buffer------------------render-----------------widgets----------------headless-----------------input---------│
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

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.

That is a BarChart at three widths, rendered by the framework’s own capture tool from the cells the renderer produced.

About the pictures on this site — read this once

Every capture on this site is a cell grid, not a screenshot of anyone’s terminal. It is produced by cmd/capture in the framework repo, which runs each widget through widgettest.Capture — the same path the 1,186 top-level test functions assert on — and converts MemorySink.Cells() to HTML. That is what makes it trustworthy: the docs cannot show something no test pins.

It is also what it is, and the limits are not closable:

  • No interaction. A capture is one settled frame after N frames of settling. It cannot show a keypress, a selection moving, a cell flickering, a pager scrolling or a tree expanding. Every widget page says so.
  • No animation, no timing, no frame pacing. render.Pacer and the 30–60 fps budget are invisible in a picture.
  • No live resize. Instead, every widget page shows three fixed widths — 40, 80 and 120 columns — side by side. That is more informative than a resize demo, because each frame is exact and the widths are named.
  • No terminal fidelity. You are not seeing your font, your colour scheme, your background, your terminal’s cell aspect ratio, or its wide-glyph and ligature behaviour.
  • Braille and Block Elements may misalign in a web font. Gauge and Sparkline are Braille (U+2800–28FF); ProgressBar, Meter, Gauge and BarChart use Block Elements (U+2580–259F). If those glyphs take a different advance width in your browser than in a terminal, the alignment those five widgets depend on is destroyed. That is exactly why the plain-text capture sits beside every colour capture on every widget page. The plain-text form is exact; the colour form is the persuasive one.

Full statement: Captures are cell grids, not terminal screenshots.

The four examples

Every one runs with go run github.com/serkanalgur/termmosaic/examples/<name>. All four are keyboard- and mouse-driven, and all four take --offline where they have a network path.

Example What it demonstrates
markets A real dashboard: three reflowing bands, live ECB FX from Frankfurter and crypto from CoinGecko, no API key, a two-entry focus ring and per-panel key routing. Walkthrough
hello The smallest complete program: a bordered panel that survives a resize, a focus ring, and a ? help overlay — now both rendered from a keymap registry rather than hand-written strings.
search Search and results on real Wikipedia data, no API key: a TextInput query field, a Table of results and a detail pane. The first example with a focusable widget in the focus ring, which is why it is the one to read if you want the catalog and keymap to meet under real conditions.
dashboard An older program that overlaps markets heavily. Whether to keep it or retire it is undecided, so this site points new readers at markets and does not present the two as equally recommended. It has not been removed.

The 24 widgets

Group Widgets
Core Block Text Paragraph Split
Forms TextInput TextArea Select Checkbox Radio Toggle Tabs Button KeyHint
Data List Table Tree Pager
Visualization ProgressBar Gauge Meter Sparkline BarChart
Navigation & modality Menu Dialog

That is 24, counted: every exported type with a New… constructor that satisfies termmosaic.Widget. buffer.Buffer is deliberately not on the list — it has Invalidate() but no Bounds, Draw or Handle, so it is not a widget, it is what widgets draw into. widgets/form/optionlist.go is an unexported helper behind Select, Tabs and KeyHint, not a widget of its own.

There is no Form container widget, on purpose. A Form type would be a second way to do what ADR 0004’s constraint solver and the layout package already do. See Forms.

Start here

Why it exists

The Go TUI ecosystem has one dominant framework and it leaves room:

  • OpenTUI is a strong engineering artifact with a deliberately unfinished surface. At 13.4k stars and running in production, it ships no progressbar, gauge, meter, sparkline or bar chart, and no list, tree, pager or virtual scroll.
  • Ink clears and repaints the whole screen on each update, which shows up as input lag once the application gets large.

The bet is narrow: the widget catalog is the product. A framework nobody can build a real dashboard on is a toy, however elegant its renderer.

Honest status, in one paragraph

The renderer, the input layer, the layout solver, the full 24-widget catalog and the keymap package are built and tested: 28 packages, 1,186 top-level test functions, a zero-allocation frame path. Alongside that: Windows is a stub that returns a loud error from every console operation, there is still no command palette, no catalog widget implements the optional Commandable/Clickable, a key the keymap consumes shadows a widget’s own switch and the registry cannot report it, there is no IME or preedit, tmux DCS passthrough is missing, TextArea has no rendered selection, and there is no theme — by decision, argued in ADR 0008, not by omission. One decision made at v1.0.0 is recorded as open rather than settled: whether the macOS test leg should also go. (widgets/widgettest’s compatibility promise was in that list too; it has since been decided — the package is excluded from the v1.0.0 stability promise, 2026-10-06.) Everything in that list is on Limitations with the reason.

Elsewhere

MIT licensed. Informed by Ratatui, Bubble Tea, Textual and OpenTUI.

Pages in this section

  • Getting started Three pages: install it, run a program in one file, then build a real application end to end.
  • Concepts The twelve ideas TermMosaic is built from, and why each one was decided rather than defaulted into.
  • Widgets All 24 widgets, with a captured frame at three widths for each.
  • Guides Composition, forms, data display, dashboards, performance and migration — task-shaped rather than widget-shaped.
  • Reference Generated from the framework's own capture manifest — nothing here is hand-maintained.
  • Architecture decisions The ten ADRs, verbatim — what was decided, what was rejected, and why.
  • FAQ The questions a reader arrives with, answered without hedging into uselessness.
  • Search Full-text search over every page of this site. Indexed by Pagefind from the built HTML.
  • Limitations What TermMosaic does not do, every item traceable to the framework's own status document. Read this before you rely on anything.