Merge pull request #132 from fiddlerwoaroof/docs/adr-004-rgb-ruling
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled

This commit is contained in:
Ed Langley
2026-09-22 13:19:13 -07:00
committed by GitHub
+83 -30
View File
@@ -1,8 +1,9 @@
# ADR 004 — Faces: appearance as UI-agnostic data
**Status:** accepted (2026-09-22) · **Decided by:** the user, who answered all five open
questions on PR #122 · **Direction from:** the user (improvise-nn4s) · **Governed by:**
`context/design-principles.md` §5
questions on PR #122 and then ruled on the one choice they left ·
**Direction from:** the user (improvise-nn4s) ·
**Governed by:** `context/design-principles.md` §5
> "I think we want to iterate towards having a way to theme the app like emacs does:
> faces, etc. described in a UI-agnostic format that the different frontends can apply
@@ -13,11 +14,11 @@ incremental work cannot paint the repo into a corner. It deliberately does *not*
finished theming system, and what is still deferred is named at the end.
It was written with five questions left open on the grounds that they were the user's
rather than the ADR's, and **the user answered all five on PR #122**. Their answers are
folded into the clauses below and recorded, with the reasoning, in
*[What the user decided](#what-the-user-decided)*. Two of them changed the decision rather
than merely confirming it: `reverse` is in the portable set, and the file format is no
longer deferred.
rather than the ADR's, and **the user answered all five on PR #122** — then ruled on the
one choice those answers left standing. Their answers are folded into the clauses below
and recorded, with the reasoning, in *[What the user decided](#what-the-user-decided)*.
Two of them changed the decision rather than merely confirming it: `reverse` is in the
portable set, and the file format is no longer deferred.
## Context
@@ -182,6 +183,22 @@ two existing `Indexed` greys would keep rendering exactly, and left the fourth a
detail". Those three cannot all hold: folding an index into `Rgb` changes what it *means*
and breaks the rule in the same stroke. Review caught it; the fourth arm is the fix.
**Which spelling does what to a terminal user, stated as a table because the difference is
invisible in a theme file and the wrong one is a thing to stumble into rather than choose:**
| spelling | follows the user's terminal theme? | what it is for |
|---|---|---|
| `Default` | yes — states nothing at all | ordinary text, the terminal's own background |
| `Named(slot)` | **yes**, fully: slots 0–15 are what a terminal theme redefines | almost everything; the shipped default theme is written in these |
| `Indexed(n)` | in principle, in practice no for 16–255 — nothing re-themes the xterm cube | a shade the sixteen cannot express, such as "just off the background" (the two greys today) |
| `Rgb(r,g,b)` | **no** | an exact colour: a brand shade, or a hue outside any palette |
So a theme author reaching for `Rgb` gets exactly what the user ruled they should be able
to have — an exact colour, which is the honest answer for a brand shade or anything the
sixteen slots cannot say — **and pays for it by opting that theme's users out of terminal
palette tracking.** Fine when chosen, bad when stumbled into, which is why the trade is
written here rather than left to be discovered.
Four things follow, and the first is the point of the whole clause.
**A named slot is not a colour, it is an index into the viewer's palette.** A theme that
@@ -590,16 +607,29 @@ silently ends the user's terminal theme applying to improvise, for 123 of 125 li
and the loss would be discovered by a user rather than by a test. Emacs solved this
decades ago with named colours plus tty approximation and there is no reason to relearn it.
**Palette-relative only, no `Rgb` arm at all.** Genuinely tempting, because it makes the
regression *unrepresentable* rather than merely defaulted-against — the strongest shape
this repo has, and the one `CellId` and `Row<T>` both took. It is rejected on one ground
only: a theme author on a truecolor terminal has a legitimate reason to want an exact
colour, and a browser theme is naturally written in RGB, so the arm would be reintroduced
by the first person who needed it. The protection is therefore a property of the *shipped*
theme rather than of the type, which is weaker, and that weakness should be read as a real
cost of this clause and not waved at. **If the user would rather have the stronger
guarantee, dropping `Rgb` is a coherent version of this ADR** — it costs an exact
truecolor theme and buys an invariant.
**Palette-relative only, no `Rgb` arm at all — proposed, and REJECTED BY THE USER.**
Genuinely tempting, because it makes the regression *unrepresentable* rather than merely
defaulted-against, which is the strongest shape this repo has and the one `CellId` and
`Row<T>` both took. The draft kept the arm on its own reasoning — a theme author on a
truecolor terminal has a legitimate reason to want an exact colour, a browser theme is
naturally written in RGB, and the arm would be reintroduced by the first person who needed
it — but flagged the weakness and put the choice to the user rather than settling it.
> **"Keep full color support in themes"** — the user, on `6a4c7dc`
So `Rgb` stays, by decision rather than by default. **The protection against losing the
user's terminal theme is therefore a property of the *shipped theme* and not of the type,
and that is now a chosen cost rather than an unexamined one.** It is the honest trade: a
theme *may* say anything a display can show, and what keeps the terminal palette alive is
that the theme improvise ships declines to.
**One distinction this ruling does not collapse, stated because it is the easy misreading.**
"Full colour support in themes" is about what the *format may express*; the shipped-theme
rule is about what *improvise's own default says*. They are independent, and clause 4
keeps both: the format admits `Rgb`, and the default theme contains none. If the intent
was also that the shipped default use absolute colour, that is a different change and
would reopen the regression clause 4 exists to prevent — so it is worth saying out loud
rather than inferring.
**A `Style` struct on the wire instead of a face name.** i.e. the server resolves the
theme and projects concrete attributes. Rejected by clause 7: the theme would then have to
@@ -617,10 +647,11 @@ document data — precisely the cost that sank improvise-9ix's second serializat
## What the user decided
These five were left open on the ground that they were the user's rather than the ADR's,
and were answered as inline comments on PR #122 at `246e8f4a`. Each answer is quoted
verbatim, with what it settled and where it now lives — the questions are kept because a
decision whose alternatives have been deleted reads as though it was never in doubt.
The first five were left open on the ground that they were the user's rather than the
ADR's, and were answered as inline comments on PR #122 at `246e8f4a`; the sixth is the
choice those answers left standing, ruled on at `6a4c7dc`. Each answer is quoted verbatim,
with what it settled and where it now lives — the questions are kept because a decision
whose alternatives have been deleted reads as though it was never in doubt.
**1. Is the `item-add` / `category-add` colour split meaningful?** Green for adding an item
and yellow for adding a category. If it encodes something ("green means this becomes
@@ -705,6 +736,20 @@ the condition is dissolved rather than met and the larger question stays unmade.
is **JSON**, which costs no new dependency anywhere. Clause 9 has all of it, including
what "editable by hand" constrains.
**6. Should the `Rgb` arm be dropped?** Not one of the original five: it is the choice the
other answers left standing, raised because clause 4's protection against losing a user's
terminal theme is a property of the *shipped theme* rather than of the *type*, which is
weaker than the unconstructible-bad-state shape this repo usually reaches for.
> **"Keep full color support in themes"**
Settled: **the arm stays.** A theme may express any colour a display can show, and what
keeps the terminal palette alive is that improvise's own default theme declines to. The
alternatives section carries it as a rejected alternative with the reasoning, rather than
as an open question — and the weakness is now a **chosen** cost, which is the difference
this answer makes. Read narrowly, as clause 4's own note says: this is about what the
format may express, not about what the shipped default says.
## Deferred, with the condition that would settle each
**The file format is no longer here.** It was deferred on the condition that improvise
@@ -728,14 +773,11 @@ the hard part: once `Theme::resolve` exists, a picker is a field and a reload is
re-resolve. Building them before the vocabulary lands would fix the interface around an
unfinished face set, which is the wrong order.
**Dropping `Rgb` — an open choice, and the user has not ruled on it.** Clause 4's
protection against losing the user's terminal theme is a property of the *shipped theme*
(it contains no `Rgb`) rather than of the *type* (which admits one). That is weaker than
this repo usually accepts — `CellId`, `Row<T>` and `Checked` all make the bad state
unconstructible instead — and the alternatives section records why the arm was kept
anyway. It is listed here rather than left in the prose because it is a live choice
someone may want to make later, and the cost of making it then rather than now is one
enum variant and whatever themes have used it.
**Nothing about `Rgb` — the user ruled, and it is not deferred.** The draft left dropping
the `Rgb` arm as a live choice; the answer is "keep full colour support in themes", so the
arm stays and the alternatives section records it as a rejected alternative with its
reasoning rather than an open one. What that fixes permanently is that **clause 4's
protection is a property of the shipped theme rather than of the type** — a chosen cost.
## Consequences
@@ -752,7 +794,18 @@ them — the ink stops being asserted. And a user can have their own theme witho
building a theme *system* first: clause 9's JSON plus the browser's download half is a
small feature, and it works because the vocabulary is the hard part and it is done.
**Bad.** It is indirection where there is currently none: `Color::Yellow` is legible and
**Bad, and this one first because it is the clause most carefully argued.** With `Rgb` in
the vocabulary by decision, **a user's terminal theme is protected by what improvise
ships, not by what the type permits.** A theme may state an absolute colour, and one that
does will not track the terminal's palette — so the guarantee is "the default theme does
not do this" and not "this cannot happen". That is weaker than this repo's usual bar,
where `CellId`, `Row<T>` and `Checked` all make the bad state unconstructible, and it was
said plainly before the ruling and is worth saying just as plainly after: **the ruling
settles what we ship, not what the type prevents, and those are different guarantees.**
What it buys is real — an exact brand colour, or a shade no sixteen-slot palette can
express — and clause 4's table says which spelling costs what.
It is indirection where there is currently none: `Color::Yellow` is legible and
`face_style(Face::GridColumnHeader)` requires a lookup to understand. That cost is real and
is the honest argument for doing nothing. The answer is that the app already pays it three
times (`axis_fg`, `field_kind_fg`, `active_color`) and nobody has objected — the indirection