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.

Limitations

What TermMosaic does not do, every item traceable to the framework's own status document. Read this before you rely on anything.

Limitations

This page is not an appendix. It is linked from the landing page, from every widget page’s footer, and from the FAQ, and every item on it is traceable to the framework’s docs/STATUS.md or CHANGELOG.md at v1.0.0. If something is missing here and you find it in the repository, that is a bug in this page — open an issue.

Captures are cell grids, not terminal screenshots

Every picture on this site is the cell grid the renderer produced, not a screenshot of anyone’s terminal. It comes from cmd/capture in the framework repo, which renders each widget through widgettest.Capture — the same path the tests assert on — and translates MemorySink.Cells() into HTML.

That makes the captures trustworthy in one specific way: the documentation cannot show something no test pins. It does not make them terminal screenshots. What is not shown:

  • 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. This is the largest gap and it is not closable without a browser build of the runtime, which docs/ARCHITECTURE.md lists as a written non-goal (“no WASM build”).
  • No animation or timing. Frame pacing, the 30–60 fps budget and render.Pacer are invisible.
  • No live resize. Each widget page shows three fixed widths instead — 40, 80 and 120 columns — which is more informative than a resize demo because each frame is exact and the widths are named.
  • No terminal fidelity. You are not seeing the reader’s font or its cell aspect ratio, their colour scheme, their background, their terminal’s wide-glyph behaviour, or any terminal-side ligature or shaping.
  • No cursor blinking, and the cursor mark is a convention. The capture tool marks the cursor as a hollow, dotted-underlined cell, because a terminal cursor sits under a glyph rather than on top of it and a static frame cannot blink.

Braille and Block Elements may misalign

Gauge and Sparkline use Braille (U+2800–28FF); ProgressBar, Meter, Gauge and BarChart use Block Elements (U+2580–259F). In a web font these can render at a different advance width than a terminal gives them, and that destroys the alignment those five widgets depend on.

That is why every widget page shows the plain-text capture beside the colour one, and why the plain-text form is the exact MemorySink.String() output — the same thing the golden files compare against. The plain-text capture is truthful. The colour capture is persuasive. If a gauge looks wrong in your browser, read the text.

One asymmetry, stated rather than hidden: the colour captures come at all three widths (40, 80, 120) but the plain-text capture comes at the widest one only — 120 columns. The 40- and 80-column frames are available in colour only. So the cross-width comparison on a widget page is a colour comparison, and the exact-cell reference beside it is a 120-column reference.

Do not mistake a captured frame for a live terminal. A reader who does will draw the wrong conclusion about how a widget looks on their own setup, and that is the failure mode this page exists to prevent.

What a capture can be trusted for

  • Which glyphs are drawn, and in which cells — the plain-text form is exact.
  • How a widget degrades across 40, 80 and 120 columns.
  • Which states a widget has distinct renderings for, because a capture of each state is a separate file.
  • MinSize(), which was called, not parsed, when the manifest was generated.

Project stage

  • v1.0.0 is the first release that makes a stability promise. The public API freezes there and Semantic Versioning applies in earnest, with one documented exception: widgets/widgettest, the test harness, is excluded from that promise — it must evolve with the framework. A behaviour change means a minor, not a quiet patch. Every release before v1.0.0 was a pre-release under a break-without-notice policy. 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.
  • Widget is the one thing held fixed. Four methods — Bounds, Draw, Invalidate, Handle — unchanged across all ten architecture decisions.
  • Anything marked PROPOSED may change or be reversed. See the decision table in docs/STATUS.md.
  • There is no version selector on this site. The plan records this as a decision rather than an oversight: the widget pages are generated from the current source, so a version selector would have to render from a checkout, not from a site.
  • Every widget has a runnable example — as of v1.0.0. CONTRIBUTING.md requires one per widget. PR #17 closed that gap with 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. There are still only four runnable programs: examples/markets (the flagship — a live finance dashboard, keyboard and mouse driven), examples/hello (a responsive panel with a focus ring and a ? help overlay), examples/search (search and results on real Wikipedia data, and the first example with a focusable widget in the focus ring), and examples/dashboard. examples/dashboard overlaps examples/markets heavily and whether to keep it or retire it is undecided. It has not been removed; the documentation points new readers at markets and does not present the two as equally recommended.

v1.0.0 — the stability promise, a replaced quantiser, and eleven checks

v1.0.0 (2026-10-06) is the first release that makes a stability promise. The public API freezes there and Semantic Versioning applies in earnest — with one documented exception: widgets/widgettest, the test harness, is excluded from the promise, because it must evolve with the framework. The reason it is v1.0.0 rather than v0.8.0 is the quantiser replacement below, which is a behaviour change and therefore a minor-level change under the project’s own policy — and the policy says the first release to promise stability is 1.0.0.

  • The 256/16-colour quantiser changed behaviour, and the bytes a program emits at those rungs change. The old “redmean” mapping was replaced with Lab-space (CIEDE2000) selection through the existing buffer.Quantiser hook. What was wrong: rmean/256 and (255-rmean)/256 divide to zero in uint8 arithmetic, so both weights were identically 2 and “redmean” was in practice the fixed 2*dr²+4*dg²+2*db² in gamma-space RGB. The CIEDE2000 audit (PR #18) measured selection error of 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. After the replacement (PR #19): selection error 0.000 on both rungs, 0 of 281,216 colour-rungs regressed, frame path 224.8 → 6.6 ns/op at 0 allocs. Two goldens moved in the framework (examples/hello/testdata/hello_256.sgr and hello_16.sgr, the title accent only), each verified better by the audit’s own metric. If you diff output at a degraded rung across the upgrade, this is why it differs.
  • widgets/widgettest is excluded from the stability promise — decided 2026-10-06. The package is public — all 74 func Example functions in widgets/*/example_test.go import it — so v1.0.0’s promise would have frozen it by default, making every future helper a breaking change. The decision excludes it explicitly instead: it is a test harness whose value depends on evolving alongside the framework, and freezing it would either prevent useful helpers from being added or force a major bump every time one is — it serves nobody. The window to relocate it under internal/ has closed, because v1.0.0 is tagged and published, so the exclusion is documented rather than the freeze pretended workable. The package is not moved, not renamed, and unchanged in code. Recorded in the framework’s docs/STATUS.md and CHANGELOG.md ([Unreleased]).
  • The Windows CI test leg was dropped; eleven checks are required now. test (windows-2025) ran the full -race suite on a Windows runner to assert that a deliberate stub returns its documented error — the most expensive check gating a platform that is out of scope. ADR 0001 scopes v1.0.0 to Linux and macOS; Windows stays a loud-error stub by decision. The six cross-compile legs (including windows/amd64 and windows/arm64) are untouched, so “it builds for Windows” is still verified; “it runs on Windows” is not claimed. The macOS test leg still runs — a further reduction has been discussed but not done, so do not read the Windows drop as a trend.
  • CI test runs bypass the test-result cache now. -count=1 closes a hole where a manual re-run could serve a cached PASS instead of re-executing tests, and cache: true was added to the setup-go steps that did not declare it.
  • deleteBranchOnMerge is false at repo level. Merged branches persist on the remote. No decision has been made about changing it.

v0.6.0 keymap — a new package, and nothing you wrote breaks

keymap shipped in v0.6.0, after slipping v0.3.0 and v0.4.0. It is a new package — Command, CommandID, Binding, Entry, Ctx, and a 16-byte comparable Chord with ParseChord/ChordOf as the one notation function in both directions. If you do not adopt it, nothing about your program changes.

No migration, and this is worth stating rather than leaving you to check. Widget, Event, Key and Mouse are unchanged. Widget.Handle’s doc comment now states the precedence — events reach a widget only after the application’s keymap has declined them — but no method was added, changed or deprecated, and the interface is byte-identical. A widget that knows nothing about keymap is fully supported.

Four things this release does not do:

  • A key the keymap consumes shadows a widget’s own switch, and the registry cannot tell you. This is documented on Widget.Handle and is the designed outcome: a ScopeFocus binding is the fix, and it goes inert when focus moves. The registry is told a widget’s bounds and its published chords, never what its Handle does — so Warnings cannot report the overlap. Not “does not yet”; cannot, on the current interface. A test asserts the silence deliberately, so the limit is recorded rather than implied.
  • No command palette. ADR 0009 §9 puts a Ctrl+K palette in scope and explicitly not in that ADR. Describe is the discoverability data for one; the palette is the UI, and it is not built.
  • Commandable and Clickable are optional interfaces, and no catalog widget implements either. The deferred half of ADR 0009 §8. A widget implementing neither is fully supported, so this is a limit on how much the registry can learn, not on what your program can do.
  • Registry has no Unregister, so a command renamed at runtime leaves a chordless row in help. Visible rather than silent.

Also unchanged from ADR 0009’s own risk list: no real terminal has met Chord’s folding rules (Ctrl+k and Ctrl+K are two chords, because a kitty terminal reports them as two gestures), and three scopes may be too coarse.

examples/markets and examples/dashboard still dispatch by their own switch. Only examples/hello and examples/search route through the registry, so the mixed-mechanism risk ADR 0009 names is live in two of four examples. markets still hand-rolls a bindings() function plus a hand-written help overlay; the command layer would make that declarative, and there it does not yet.

v0.6.1 DescribeGrouped — and examples/hello’s hint text changed

Describe returns one Entry per chord. DescribeGrouped returns one per command, carrying every chord in scope for it. That is the ordering ADR 0009 §9 specifies for a command palette, where a row consumes Chords[0]. Both orderings exist because the consumer differs: 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.

Pick by what you are rendering. Describe for a palette row; DescribeGrouped for a hint line or any merged display.

examples/hello’s pinned hint line text changed, and if you took that example as near-enough-correct, it no longer matches. It now reads:

[Q q Esc Ctrl+c] quit  ·  [?] toggle the keys

where it used to read press q to quit · ? keys · arrows move focus. The ? help overlay also went from two rows to three, because Describe spells every chord in full rather than saying “arrows” — twelve chords is about eighty cells of bracketed labels before a single description, which needs three rows at the narrowest interior the panel draws at.

That change is the visible half of what v0.6.1 actually did: examples/hello now dispatches through keymap, and both the 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. The example’s key contract did not otherwise change: same keys, same behaviour, and Widget.Handle claims nothing.

Registry has no SetFocus, so Describe(ScopeFocus) cannot answer “what can the focused widget do right now” — the registry only learns what is focused by dispatching. Deferred to v1.1. examples/hello avoids it by using ScopeScreen. One more consequence worth knowing: Attach must be called even when no widget implements Commandable, purely so a scope-bound owner is IsAttached — a registry that reports no Warnings requires it.

v0.7.0 examples/search — the first example with something actually focused

examples/search shipped in v0.7.0, with nine golden files and 74 tests, all asserting on cells through the headless harness.

It is a search-and-results screen on real Wikipedia data — no API key — with a form.TextInput query field, a data.Table of results and a detail pane. --offline runs the whole screen on a transcribed capture. It is the first example with a focusable widget in the focus ring, and that is what makes it interesting to read rather than merely runnable.

What it establishes, and the two limits it exposed:

  • Context-dependence is expressed with Command.Enabled, not ScopeFocus — and that is a finding, not a simplification. No catalog widget implements keymap.Commandable, so a focus-scoped binding would mean the application declaring keys on a widget’s behalf with an owner it picked, which is the one thing Commandable exists to stop being necessary for. Enabled is documented as “an unavailable command is not run by a key press”, and Dispatch skips it and keeps looking, so a screen-scoped arrow binding with Enabled false while the table has focus is simply not claimed and the event falls through to the tree.
  • Registry.SetFocus is missing, so Describe(ScopeFocus) is over-inclusive, not incomplete. inScope returns true for an exact-scope match without consulting liveness, so a focus query reports every focus-scoped binding whatever has focus. Harmless for a help screen, which arguably wants the superset; wrong for a hint line. examples/search worked around it by tracking focus itself, and a test pins the gap so neither the workaround nor the gap can be quietly forgotten. One line, once SetFocus exists. Deferred to v1.1.

Also, if you build a screen this shape:

  • A key bound anywhere outranks a focused widget’s own key. q has to decline explicitly or you have a search box you cannot type “quit” into — and the decline must check that the field has focus and that the chord is an unmodified printable, because Ctrl-C arrives as Ctrl+'c': a printable rune with a modifier.
  • Home and End are deliberately left unbound. Both widgets claim the bare forms — the field moves the caret, the table selects first and last — and a screen binding outranks both, so binding them would silently break both. The ring’s ends are Ctrl+Home/Ctrl+End, which neither widget consumes.
  • data.Table has no Ascii flag for its selection marker, so its default › has no ASCII rung and would leak onto a terminal whose caps report no Unicode. The example overrides the marker, making it an application decision.
  • TextInput does not expose its horizontal scroll offset, so the example computes the caret cell from the rune index and clamps — exact for any query shorter than the field, approximate beyond.
  • The arrow step from the field to the table is one-way. The table consumes Up, so at its first row Up moves nothing and the way back is Shift-Tab. A ring whose arrows worked both ways would need the table to decline Up at row 0, which is not something a widget can express.

There is still no command palette

ADR 0009’s command layer is built; its palette is not. keymap.Describe and DescribeGrouped give you the data a palette needs and the ordering to render it in, and there is no Ctrl+K palette UI in the framework. ADR 0009 §9 scopes it out of that ADR deliberately, so this is a stated omission rather than an unfinished implementation.

An application that wants one writes it, and the registry is what makes that cheap: one Entry per command with its chords, rendered from data the application already owns rather than a second hand-maintained list. That is exactly what examples/hello and examples/search do for their hint lines.

The v0.5.0 field-to-setter migration breaks compilation — read this before upgrading

This is the only change in the v0.5.x line that stops a program compiling. Pager.Status, Split.Spacing, Meter.ShowValue, ProgressBar.Label and ProgressBar.Percentage are no longer exported fields. A program that assigns them gets a compile error, not a silent behaviour change — which is the good way for it to arrive, because the compiler points at every site.

What to change:

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 for all five: Status(), Spacing(), ShowValue(), Percentage(), Label() — so a read does not need a rewrite.

Why the fields went private. 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 — the rect did not change, so the cache keeps hitting. A doc comment saying “call Invalidate after assigning” is a rule with no enforcement, no compile error and no reminder. ProgressBar is the proof: SetLabel already reset the cache correctly and the field was still assignable, so the API taught the wrong lesson by having both. The setters are behaviour-preserving on a widget that has not yet drawn, and strictly better on one that has.

If you were already calling the setters, nothing to do — and if you were compensating for the stale-cache bug by calling Invalidate() by hand after assigning, remove that: the setter does it.

What the cache-audit gate does not cover. It covers the transitions it names, and it now fails the build. The exported raw fields that remain — Select.Marker, Gauge.ShowValue, BarChart.ShowValue/Vertical/Data, Sparkline.Values/Braille, TextInput.Placeholder, Checkbox.TriState, and the Scrollbar/Header fields on List/Table/Tree — are the same shape. None produced a finding, so none is a confirmed defect, and they are not covered because no transition names them. Treat that list as the next place a stale-cache bug would surface, not as a set of bugs.

v0.5.1 style fixes — focused and disabled widgets now render correctly, and look different from v0.4.x

Seven style-application defects, one class: the style a widget computed for one cell stopped where two code paths diverged and never reached the text beside it. Nothing has to change to compile or to run. But if your widgets look different after upgrading, that is this release working.

  • A focused Dialog choice label now renders. ChoiceFocusStyle reached the row fill and the focus marker but not the text — the label cache is built before focus is known. With default styles the focused choice rendered attr=none on an attr=reverse row, which is unreadable dark-on-dark on exactly the row the reader is meant to look at. drawActions already had this right via a second cached rendition; drawChoices now does the same. A focused choice looks different from v0.4.x because in v0.4.x it was unreadable.
  • A focused or disabled Button label now renders in the right style. 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 said the style was “the style of the whole button — background, label and brackets”. Both states look different from v0.4.x because v0.4.x contradicted the field’s own documentation.

On Table, the row style now overrides the cell — read this before you upgrade a table with styled cells.

  • A selected row no longer shows a cell’s own Style, and no longer shows its column’s CellStyle. SelectedStyle and ItemStyle filled the row but never reached the cell text, so a selected row showed terminal-default text on the terminal-default background in the middle of a highlighted row. The row style now overrides a single-span cell, matching List and Tree. A cell carrying several spans keeps its own styles, as drawCell documents — so a cell you built from several styled runs is unaffected, and a plain single-style cell is. That is the trade: row legibility, in exchange for the cell’s own rendition not showing on that one row.
  • The header now carries ItemStyle’s foreground as well as its background. HeaderStyle is documented as “patched with ItemStyle” and never was, so — because HeadingStyle is attribute-only — the header text resolved to the terminal background inside a row just filled with ItemStyle. It is patched now, and Patch takes FG as well as BG, so the header text also picks up ItemStyle’s foreground. That is Patch’s documented semantics and matches the header’s stated intent of sitting on the row background, but it will be visible to a caller who set ItemStyle with a distinct foreground. If your header changed colour and you did not change your styles, this is why.
  • Menu’s check glyph and submenu arrow are no longer drawn in ItemStyle on the selected row, where the row is reversed. The marker gutter already had the right fallback — the source comment names this exact hazard — and the check and arrow did not.
  • Radio’s focus gutter is no longer styled as a focus mark on every row. A blank cell carried styles.focus, which inherits SelectedStyle’s AttrReverse, so an unfocused group painted a reverse-video stripe down its left edge. The column still exists on every row, because that is what keeps the labels aligned; only the rendition was wrong.
  • BarChart’s horizontal axis labels no longer overrun their column. The centring offset was computed and discarded with _ = lx, and the label was capped to the whole axis row rather than its own column, so a label wider than its column overwrote the next category’s. The horizontal path already did this correctly elsewhere; this is the visible consequence.

Why this is a minor bump and not a patch. Changing what a library widget draws is the definition of a behaviour change, and the project’s release policy puts those in a minor — a patch that changed behaviour would itself be a bug in the release.

If you worked around the old behaviour, undo the workaround. A program that compensated in its own styles because the widget’s were ignored is now double-counting, and should remove the compensation.

v0.5.2 mouse routing — Tabs, Select and Radio are hit-tested now, and examples/markets changed behaviour

Until v0.5.1, 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.

ADR 0010 settles the rule: a widget handles a pointer event only when the pointer is inside its Bounds(). Two exemptions are stated rather than left implicit — a release ends a drag wherever the pointer is, and a drag continues outside Bounds once a press has claimed it, because the press is the claim and the drag is the continuation. The fix is in the shared helper, so a future fourth widget on it is correct by construction.

examples/markets behaviour changes, and this is worth knowing if you read the dashboard guide or run the example. Its tab row (d.pair) is a form.Tabs and is first in the focus ring, so it swallowed every wheel notch in the application. It carried an application-level workaround loop, and the loop stays — what it actually buys is a rule no widget can implement alone: the wheel never takes focus. The visible difference is that a wheel 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.

docs/STATUS.md recorded this only for Tabs. It is three widgets, and the fix belongs in the shared helper, which is why it was found by applying a written rule rather than by chasing three symptoms.

Two related things the ADR states rather than leaves to be discovered. split.Split consumes a wheel notch over a pane and moves focus to the pane under the pointer — that is in-bounds behaviour consistent with a click, and a Split has no content of its own to scroll, so it is unchanged. And TextArea wheel-to-scroll is declined by decision, not overlooked: it is a plausible feature, but adding one under a routing ADR would answer a different question. It will be bounds-correct when it lands, because the rule is now written down.

Renderer.Post changed behaviour in v0.2.0 — apps on v0.1.0 should read this

In v0.1.0, Post never woke the frame pacer, and any app driving screen updates from Post painted one frame and idled forever. NeedsFrame() ignored queued callbacks while Pacer.Run gates every frame on NeedsFrame(), and posted callbacks only run inside Render — so the callback could not run until a frame happened, and a frame would not happen until the callback ran.

This is documented in ADR 0003 as the safe way to mutate widget state, precisely because it is safe against a concurrent Draw. So this was not an exotic path: it was the recommended one.

Fixed in v0.2.0, with a regression test. This is why v0.2.0 is a minor bump and not a patch — it is a behavioural change, which the project’s release policy allows for a minor release and forbids for a patch.

If you are moving from v0.1.0 and your screen was not updating, this is why, and upgrading fixes it. The symptom was a live-looking program that simply never changed again — not a hang, a crash or a visible error.

Tree label styling took effect in v0.4.0 — this changes what you see

This is the change in v0.4.0, and it is a behaviour change rather than a new feature: styling you set on a Tree used to be silently discarded on the label, and it is now honoured. Nothing has to change to compile or to run. But if your tree renders differently after upgrading, that is this release working, not a regression.

Plainly: a program whose Tree relied on its labels ignoring Style, ItemStyle and SelectedStyle will now render differently. Before this release, a node’s Style, the ItemStyle fallback and the selected row’s SelectedStyle reached the expander glyph and nothing else — the label was written through a call that took no style, so it came out in whatever the terminal’s own default happened to be. Those three fields are documented as applying to the node, and now they do.

drawRow computed the content style — node Style, falling back to ItemStyle, and SelectedStyle outright when the row was selected — and applied it to a single cell, then wrote the label beside it through a different call. The computed style stopped exactly where the two code paths diverged. The label now goes through the same paintRow path List and Table use, over a rectangle covering the label region alone, so the fill cannot bleed into the expander marker or the indent. Four tests pin it: node style reaching the label, the ItemStyle fallback, a multi-span label keeping its own styles, and the ASCII path.

If you worked around the old behaviour, undo the workaround. A program that compensated in its own styles because the widget’s were ignored is now double-counting, and should remove the compensation.

Why this is a minor bump and not a patch. Changing what a library widget draws is the definition of a behaviour change, and the project’s release policy puts those in a minor — a patch that changed behaviour would itself be a bug in the release. The framework’s CHANGELOG.md records it under Breaking.

Two things in this release did not close.

  • No capture on this site moved, and that is expected. The generator’s tree entry sets no styles, so it never exercised the broken path and the fix is inert for the captures. A widget page can be correct while its picture is merely uninformative.

  • The CI posture change has since been run, and it was green. Every action moved to a Node 24 major and every runner image is pinned by name (ubuntu-24.04, macos-15, windows-2025) rather than tracking -latest. This was originally verified statically — each action’s action.yml declares using: node24 — and not by a run. It has now been executed by a real GitHub Actions run: all twelve checks on the v0.4.0 pull request passed, namely test (ubuntu-24.04), test (macos-15), test (windows-2025), gofmt, golangci-lint, zero-allocation diff, and the six cross-compile legs for linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64 and windows/arm64. The post-merge run on main was green too, as was the v0.4.1 dependency-bump run.

    So the concern the previous release raised is closed: none of the four action major bumps changed an input, a default or a behaviour the project relies on, and macos-15 and windows-2025 are now images this project has actually run on rather than new ones it has only read about. What that green run does not cover is the item below, which no CI configuration has ever covered.

    Dated note (v1.0.0, 2026-10-06): the twelve-check configuration above is history. CI now requires eleven checks — the test (windows-2025) leg was dropped at v1.0.0 because a full -race run on Windows asserted only that a deliberate stub returns its documented error. The six cross-compile legs, including both windows targets, are unchanged. See v1.0.0.

And one long-standing item that this release did not fix. The Windows backend still runs zero tests at runtime. term/terminal_windows_test.go is compile-only verified — GOOS=windows go vet ./... is the only check that has ever covered it — and those tests have never been executed anywhere. It is restated here because it is the item most easily mistaken for coverage: the file exists and is substantial, which reads like Windows is tested, and it is not. See Platform for why the Windows backend is a stub in the first place.

List and Table styling took effect in v0.3.0 — this changes what you see

This is the most visible change in v0.3.0, and it is a behaviour change rather than a new feature: styling you set on a List or a Table used to be silently discarded, and it is now honoured. Nothing has to change to compile or to run. But if your widgets look different after upgrading, that is this release working, not a regression.

Two independent bugs, one visible consequence each.

On List, the style computed for each row was never actually passed to the paint call — it was computed and thrown away. So a List drew each item using whatever styles the item’s own text spans happened to carry, and SelectedStyle, the field documented as the selected row’s rendition, reached only the background fill and never the glyphs. ItemStyle on an unselected row and SelectedStyle on the selected row now both apply, and on the selected row the selected style wins — which is the precedence those two fields have always documented.

On Table, the sentinel passed to mean “write these spans verbatim” was buffer.DefaultStyle, which is a wrong choice for that job: the unset check compares against the zero Style{}, and DefaultStyle is a resolved style, not an unset one. The guard therefore never fired, and every cell’s own Style was overwritten with the terminal’s default colours — along with every column’s CellStyle. Cell spans and column styles now render as given. HeadingStyle was never affected, because it is passed as a real override rather than as the sentinel, and it still works.

Why this is a minor bump and not a patch. Both fields were already documented as taking effect. The code contradicted its own documentation, so this is a fix to behaviour that was already promised — but a patch release that changes what renders would itself be a bug in the release, so the project policy makes it a minor. The framework’s CHANGELOG.md records both under Breaking.

If you worked around the old behaviour, undo the workaround. A program that compensated in its own styles because the widget’s were ignored is now double-counting, and should remove the compensation.

One thing that did not change: a multi-span row keeps its per-span styles. The override only applies when a row is a single span. Flattening a multi-span row would mean building a string on the frame path, which the package’s zero-allocation claim rules out. A row that deliberately carries several styles still renders with them. This limit was equally true before — it was just invisible, because nothing else about row styling worked either.

Platform

  • Linux and macOS are the supported platforms — narrowed to that by the v1.0.0 platform decision (ADR 0001), recorded 2026-10-05. The claim was previously “Linux, macOS and Windows”; it amends to two because Windows is not close and pretending otherwise was the bigger risk.
  • Windows is a stub that returns a loud error from every console operation. The package compiles and cross-compiles cleanly for windows/amd64 and windows/arm64, so the packaging works and the runtime does not. On Windows this framework does not currently draw anything. The Windows CI test leg was dropped at v1.0.0 — running the full test suite there asserted only that the stub returns its documented error — but the cross-compile legs still build both windows targets, so “it builds” remains verified.
  • tmux and GNU screen DCS passthrough is missing. Under tmux on a modern terminal, a TermMosaic program can lose key and mouse reporting, because the sequences TermMosaic emits are not wrapped for the multiplexer. Deferred with a stated trigger: any tmux user reporting broken keys or mouse, or v1.0, whichever comes first. v1.0.0 has shipped and the gap remains — the trigger arrived and nothing was built, which is the honest state: still deferred, still real.

Input

  • No IME or preedit support. This is a deliberate deferral, not an oversight, and the consequence is worth stating plainly: composing Japanese, Chinese or Korean in a TextInput produces wrong behaviour rather than degraded behaviour. On most terminals the committed text arrives as a burst of ordinary key events, which inserts correctly but pollutes the undo stack with one entry per character, so a single Ctrl-Z removes one character instead of the composition. The event model reserves EventCompose and a Compose payload so this can be added later as a feature rather than as a rewrite of every widget. See ADR 0005 §7. Do not read this as parity with a framework that has it, and do not read it as a gap being closed — it is a documented decision.
  • Mouse capture is off by default. Enabling it takes text selection and scrollback copying away from the user’s shell. That is the default on purpose, and the framework has no opinion about your application changing it — examples/markets opts in and restores the previous mode on exit. Nothing in the catalog enables it for you.
  • Focus reporting is off by default, for the same reason.
  • Wheel routing is hit-tested, and it was not until v0.5.2. form.Tabs, form.Select and form.Radio used to consume every wheel notch whether or not the pointer was over them, so a tab row early in the focus ring swallowed every notch in the application. That is fixed: ADR 0010 decides that a widget handles a pointer event only when the pointer is inside its Bounds(), and the fix is in the shared optionlist helper so a future widget on it is correct by construction. See v0.5.2 mouse routing.
  • What is still open about the mouse is not the wheel and not hit-testing. Hit-testing is settled. What no widget can do alone is the rule that the wheel never takes focus: a reader scrolling is reading, not committing to a panel, and moving focus under the pointer would rewrite the key hint while they are still looking at the numbers. A click is the gesture that commits. That is why examples/markets keeps an application-level routing loop rather than relying on the widgets, and why an application still needs one: if you route the wheel by focus ring order, you will re-create the v0.5.1 defect, because a wheel notch must not reach a widget whose rectangle the pointer is not inside.
  • TextArea has no wheel-to-scroll, by decision and not by oversight. ADR 0010 declines it explicitly: it is a plausible feature, but adding one under a routing ADR would answer a different question. It will be bounds-correct when it lands.

Text and internationalization

  • Wide glyphs and grapheme clusters are handled, but the handling is provisional. A double-width glyph occupies two cells and its continuation cell takes its owning span’s style, so rows do not flicker — and those paths were benchmarked for the first time in v0.1.0 (a full repaint costs 74,139 ns/op against the ASCII scene’s 74,078, all paths 0 allocs/op). What is not done: the cell-width table is hand-written from East Asian Width ranges rather than generated from Unicode data, so newly assigned wide blocks are wrong until the table is updated; and grapheme clusters are not composed, so a flag emoji renders as two cells’ worth of junk and a zero-width-joiner sequence renders as several glyphs.
  • The diff’s wide-glyph cursor-move overhead was fixed in v0.2.0, and the fix is worth stating as a correction to an earlier entry on this page. The old text said the defect was unfixed and out of scope; that was true at v0.1.0 and is no longer. Run suppression had assumed one cell per rune, so every wide glyph was preceded by a cursor-position escape: 6,000 cursor moves and 68,832 bytes against the narrow scene’s 30 moves and 6,233 bytes — about 11× — for identical output. The tracker now advances by the glyph’s cell width, giving 60 moves and 19,443 bytes, 3.12×. The ASCII path is unchanged at ~7,200 ns/op with 0 allocs, and the benchmark that had pinned the defective behaviour now asserts the corrected one.
  • Grapheme clusters are still uncomposed, so a flag emoji still renders as two cells’ worth of junk and a zero-width-joiner sequence as several glyphs.
  • geometry.ClampCount and geometry.Budget count cells, not glyphs, so a row budget computed from them can be one row optimistic once wide characters are in play.

Colour

  • The colour quantiser selects in Lab space (CIEDE2000), decided on measurement — 2026-10-06, at v1.0.0. Truecolor maps to the 256 and 16 colour rungs through an exhaustive CIEDE2000 search behind a per-colour memo, via the existing buffer.Quantiser hook. Selection error is 0.000 on both rungs; every threshold in the audit is pinned at 0, so a non-zero measurement means the metric, the palettes or the wiring changed without a re-audit. History, kept because the record of what was wrong matters: through v0.7.0 this page described the quantiser as unvalidated redmean — “nobody has checked that its output is perceptually acceptable; treat the 256 and 16 rungs as provisional.” That was true when written. The check was then performed (PR #18’s CIEDE2000 audit), it failed: the “redmean” weights were 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 — measuring selection error of 21.201 (256 rung) and 36.821 (16 rung) at worst, 19.35% and 37.50% of the lattice above the just-noticeable difference. The quantiser was then replaced (PR #19). The colour model moved PROPOSED → DECIDED because it was measured, not asserted; reopening it after v1.0.0 is a release-defining re-audit, not a tweak.
  • This is a behaviour change at v1.0.0. The bytes a program emits at the 256 and 16 colour rungs differ from v0.7.x. Visible consequence named in the release: markets.down (#d86a62) no longer collapses to grey at the 16 rung and collides with markets.flat — the up/flat/down trichotomy is green/grey/red again — and #9b3228 brick red lands on a red rather than on olive.
  • Caps.Unicode is a proxy, not a probe. It is the honest one available without querying the terminal out of band, and a terminal configured out of band will disagree with it.
  • NO_COLOR is honoured at encode time, so no widget path consults the environment. Suppressing colour does not suppress attributes — reverse video still works, which is why selection and focus survive it.

Widgets

  • There is no Form container widget, by decision. ADR 0004’s solver plus layout covers composition, and a Form type would have been a second way to do the same thing. See Forms.
  • TextArea has no rendered selection. It tracks and moves a caret and supports editing, but the selected range is not drawn. TextInput renders its selection; TextArea does not. Half a selection is worse than none.
  • BarChart’s horizontal category labels were all drawn at absolute column 0 until v0.2.0, because adapt never set axisRow. Inside Bounds only for a chart at the origin, and a widget writing outside its own rectangle everywhere else. Fixed; named here because it is a good illustration of why Draw must stay inside Bounds at all.
  • There is no redo stack, in either text field.
  • There is no table column selection and no pager selection. Selection is one table row; a Pager shows and searches but hands nothing back.
  • No keyboard column selection in Table, no multi-select anywhere, and no clipboard integration.
  • Tree expansion state is the application’s to hold. Laziness means only visible rows are rendered; the nodes and the open/closed flags are yours.

Layout and responsiveness

  • There are no framework breakpoints and no size classes, by decision in ADR 0007. A size class is a lossy function of two numbers and a product decision in the wrong layer. Each widget’s threshold is a local named constant beside its own Draw.
  • No resize has ever been observed against a real terminal being dragged. ADR 0007’s drag-resize costs are derived from existing code and from ADR 0002/0003’s measurements. Four of ADR 0007 §3’s tests were written for v0.1.0 and they found a real defect — Render flushed the sink even when it wrote nothing — but a scripted resize sweep is not a human dragging a window.
  • The cache-audit mode is built and gates the build, but it only covers the transitions it names. ADR 0007’s expensive half shipped in v0.5.0: it corrupts a widget’s cached derivation after a Draw and asserts the next frame is byte-identical, and widgets/cacheaudit fails the build on a finding. It found eight real stale caches on its first run, so the class is real. What it does not do is audit the exported raw fields no transition names — see the v0.5.0 section for which those are.
  • MinSize() is an assertion, not enforcement. The framework does nothing with it. What to do when the available space is below it is the application’s decision, because only the application knows whether losing a table is acceptable.

Theme

  • There is no theme in v1, and that is a decision with a written trigger, not an omission. Widgets carry Style fields and the framework’s defaults are the terminal’s own colours plus named attribute styles. The trigger for adding a theme is the first style role that two widgets must share. Until then a theme layer would be an abstraction over a concept the catalog does not have. Why, in full.

Still open

  • Whether the macOS test leg should also go. The Windows test leg was dropped at v1.0.0; the macOS leg still runs the full -race suite. A further reduction has been discussed but not done — do not read the Windows drop as a trend.
  • deleteBranchOnMerge is false at repo level. Merged branches persist on the remote. No decision has been made about changing it.
  • Kitty graphics protocol in v1, or stay text-only? Leaning no. Images undermine the grid-of-cells assumption the whole renderer rests on. Still open because “no” has not been formally decided.
  • Headless backend: v1 or v0.5? Largely settled — ADR 0001 makes it a v1 deliverable. The remaining sub-question is its assertion surface: it must expose the cell buffer, not just recorded bytes. It does expose the cell buffer; whether the surface is complete is open.
  • The width table is hand-written. Derived from East Asian Width ranges rather than generated from Unicode data, so newly assigned wide blocks are wrong until it is updated. A benchmark cannot make a table correct.
  • Whether the wheel should ever take focus. Hit-testing is settled — ADR 0010 says a widget handles a pointer event only when the pointer is inside its Bounds(), and the three widgets that broke that rule are fixed. The question that is still open is the one no single widget can answer: in an application with several focusable widgets, which widget should receive a notch that lands on none of them, and should any of them move focus as a side effect. See Input above.
  • examples/dashboard overlaps examples/markets and the decision is open. See Project stage above.

This site

  • It is generated, and partly of it is committed. The widget pages come from scripts/gen_widgets.py, which merges the framework’s manifest.json, godoc extracted from the Go source, and hand-written prose in data/widget_prose.json. The captures themselves are framework artefacts, copied unmodified. The ADRs are byte-identical to the framework’s.
  • Two sections of every widget page cannot be generated — the example and “when not to use it” — and the second is hand-written per widget. A reviewer reading a widget page knows exactly which parts to check.
  • No analytics, no cookies, no third-party requests. Search is Pagefind, indexed from the built HTML, and its script is loaded on one page only.
  • If you find an inaccuracy here, that is a bug in this site, not a matter of opinion. Please report it.