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.mdlists as a written non-goal (“no WASM build”). - No animation or timing. Frame pacing, the 30–60 fps budget and
render.Pacerare 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. Widgetis 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.mdrequires one per widget. PR #17 closed that gap with 74func Examplefunctions covering all 24 catalog widgets, in the eightwidgets/*/example_test.gofiles, each rendered throughwidgets/widgettestso its// Outputcomment 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), andexamples/dashboard.examples/dashboardoverlapsexamples/marketsheavily and whether to keep it or retire it is undecided. It has not been removed; the documentation points new readers atmarketsand 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.Quantiserhook. What was wrong:rmean/256and(255-rmean)/256divide to zero inuint8arithmetic, so both weights were identically 2 and “redmean” was in practice the fixed2*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 — withmarkets.downcollapsing to grey at the 16 rung and colliding withmarkets.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.sgrandhello_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/widgettestis excluded from the stability promise — decided 2026-10-06. The package is public — all 74func Examplefunctions inwidgets/*/example_test.goimport 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 underinternal/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’sdocs/STATUS.mdandCHANGELOG.md([Unreleased]).- The Windows CI test leg was dropped; eleven checks are required now.
test (windows-2025)ran the full-racesuite 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 sixcross-compilelegs (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=1closes a hole where a manual re-run could serve a cached PASS instead of re-executing tests, andcache: truewas added to the setup-go steps that did not declare it. deleteBranchOnMergeis 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 onWidget.Handleand is the designed outcome: aScopeFocusbinding is the fix, and it goes inert when focus moves. The registry is told a widget’s bounds and its published chords, never what itsHandledoes — soWarningscannot 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+Kpalette in scope and explicitly not in that ADR.Describeis the discoverability data for one; the palette is the UI, and it is not built. CommandableandClickableare 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.Registryhas noUnregister, 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, notScopeFocus— and that is a finding, not a simplification. No catalog widget implementskeymap.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 thingCommandableexists to stop being necessary for.Enabledis documented as “an unavailable command is not run by a key press”, andDispatchskips it and keeps looking, so a screen-scoped arrow binding withEnabledfalse while the table has focus is simply not claimed and the event falls through to the tree. Registry.SetFocusis missing, soDescribe(ScopeFocus)is over-inclusive, not incomplete.inScopereturns 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/searchworked 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, onceSetFocusexists. Deferred to v1.1.
Also, if you build a screen this shape:
- A key bound anywhere outranks a focused widget’s own key.
qhas 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, becauseCtrl-Carrives asCtrl+'c': a printable rune with a modifier. HomeandEndare 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 areCtrl+Home/Ctrl+End, which neither widget consumes.data.Tablehas noAsciiflag 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.TextInputdoes 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 rowUpmoves nothing and the way back isShift-Tab. A ring whose arrows worked both ways would need the table to declineUpat 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
Dialogchoice label now renders.ChoiceFocusStylereached 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 renderedattr=noneon anattr=reverserow, which is unreadable dark-on-dark on exactly the row the reader is meant to look at.drawActionsalready had this right via a second cached rendition;drawChoicesnow does the same. A focused choice looks different from v0.4.x because in v0.4.x it was unreadable. - A focused or disabled
Buttonlabel now renders in the right style.FocusStyleandDisabledStylereached 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’sCellStyle.SelectedStyleandItemStylefilled 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, matchingListandTree. A cell carrying several spans keeps its own styles, asdrawCelldocuments — 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.HeaderStyleis documented as “patched withItemStyle” and never was, so — becauseHeadingStyleis attribute-only — the header text resolved to the terminal background inside a row just filled withItemStyle. It is patched now, andPatchtakes FG as well as BG, so the header text also picks upItemStyle’s foreground. That isPatch’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 setItemStylewith 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 inItemStyleon 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 carriedstyles.focus, which inheritsSelectedStyle’sAttrReverse, 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’saction.ymldeclaresusing: 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, namelytest (ubuntu-24.04),test (macos-15),test (windows-2025),gofmt,golangci-lint,zero-allocation diff, and the sixcross-compilelegs forlinux/amd64,linux/arm64,darwin/amd64,darwin/arm64,windows/amd64andwindows/arm64. The post-merge run onmainwas 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-15andwindows-2025are 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-racerun 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/amd64andwindows/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
TextInputproduces 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 reservesEventComposeand aComposepayload 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/marketsopts 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.Selectandform.Radioused 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 itsBounds(), and the fix is in the sharedoptionlisthelper 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/marketskeeps 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. TextAreahas 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.ClampCountandgeometry.Budgetcount 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.Quantiserhook. 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/256and(255-rmean)/256divide to zero inuint8arithmetic, so both weights were identically 2 and the formula was fixed2*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 withmarkets.flat— the up/flat/down trichotomy is green/grey/red again — and#9b3228brick red lands on a red rather than on olive. Caps.Unicodeis 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_COLORis 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
Formcontainer widget, by decision. ADR 0004’s solver pluslayoutcovers composition, and aFormtype would have been a second way to do the same thing. See Forms. TextAreahas no rendered selection. It tracks and moves a caret and supports editing, but the selected range is not drawn.TextInputrenders its selection;TextAreadoes not. Half a selection is worse than none.BarChart’s horizontal category labels were all drawn at absolute column 0 until v0.2.0, becauseadaptnever setaxisRow. InsideBoundsonly 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 whyDrawmust stay insideBoundsat 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
Pagershows and searches but hands nothing back. - No keyboard column selection in
Table, no multi-select anywhere, and no clipboard integration. Treeexpansion 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 —
Renderflushed 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
Drawand asserts the next frame is byte-identical, andwidgets/cacheauditfails 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
Stylefields 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
-racesuite. A further reduction has been discussed but not done — do not read the Windows drop as a trend. deleteBranchOnMergeis 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/dashboardoverlapsexamples/marketsand 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’smanifest.json, godoc extracted from the Go source, and hand-written prose indata/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.