docs(faces): where faces live and what the first two steps settled (improvise-nn4s)

context/code/protocol.md gains section 10 and tui.md's colour section now
describes face_style; the pointers to axis_fg, minibuf_fg and AxisColor
elsewhere follow. Evidence for the new tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Claude (agent-faces)
2026-09-24 22:05:24 -07:00
committed by Edward Langley
parent f7d2730736
commit 92f8ac2113
9 changed files with 196 additions and 59 deletions
+82 -3
View File
@@ -1311,8 +1311,8 @@ re-derived from `kind`, so the two ends cannot disagree about what "Measure
rather than re-exported, because this crate must not depend on `improvise-io`
(the wizard is an io type, gotcha 30) and a client that draws the wizard must
still be able to name the step it is on. `WizardFieldKind` is the semantic
half of a colour decision — the terminal mapping lives in `draw.rs`, exactly
as `AxisColor`/`axis_fg` does (gotcha 26).
half of a colour decision: it indexes the `wizard.field.*` faces (§10), and the
terminal mapping is `draw.rs::face_style`.
### `Projection` carries no multi-widget variant
@@ -2353,7 +2353,7 @@ deserializes now carries no English at all.
`Hint { keys, command, caption }` is the unit: `keys` come off
`Keymaps::default_keymaps()`, `command` is the semantic value a localized or
non-terminal front end keys off, and `caption` is the default English — exactly
the `AxisColor`/`draw.rs::axis_fg` shape, with the decision shared and only the
the shape faces now have (`Face` → `draw.rs::face_style`, §10), with the decision shared and only the
last translation local (`invariant(semantic_palette_at_the_core_boundary)`).
### What that bought, and what it cost
@@ -2441,3 +2441,82 @@ the mode, the same shape `col_groups` uses for "no groups". `web/app.js` paints
rows it does not author; `src/ui/help.rs` keeps its ratatui styles and lost
~310 lines of content that is now shared. Neither end restates what the keys
are.
## 10. `face.rs` — appearance named by role (ADR 004)
ADR 004 (`context/adr/004-faces-and-themes.md`) is the decision; this section
is where its first two migration steps landed and what they settled that the
ADR left open (improvise-nn4s.1, improvise-nn4s.2).
### What is here
- `Face`: one variant per role. Where a domain enum already exists the face is
**parameterised by it** — `Face::Axis(Axis)`, `Face::WizardField(WizardFieldKind)`,
`Face::ModePanel(Panel)`, `Face::ModeTextEntryOf(TextEntry)`,
`Face::MinibufferOf(TextEntry)` — so adding an arm to that enum adds a face by
construction (clause 2). `Face::name` spells each one as a theme file keys it
(`axis.row`, `mode.text-entry.command`, `minibuffer.category-add`), and those
names are a compatibility surface once a theme can be downloaded (clause 9).
- `Face::parent`: the inheritance, **fixed in the vocabulary rather than stated
by the theme**. A theme that omits a face must still get the face's parent's
values ("a theme written today keeps working when a face is added tomorrow",
clause 9), and that is only true if the chain does not depend on what the
theme says.
- `Colour` (`Default | Named(AnsiSlot) | Indexed(u8) | Rgb`), `Weight`, `Slant`,
and `FaceSpec`, whose six attributes are all `Option`: `None` inherits, and
`Some(Colour::Default)` states "the surface" and stops inheritance. The two
are different answers and the type keeps them apart.
- `Theme { faces }` and `Theme::resolve(Face) -> ResolvedFace`, which walks the
chain once and returns every attribute concrete. **No front end walks the
chain** (clause 3). `resolve` takes a face and nothing else.
- `Theme::shipped()`: today's terminal rendering, palette-relative throughout
(clause 4). `the_shipped_theme_states_no_absolute_colour` holds that line.
- `Face::of_mode(&AppMode)` and `Face::of_minibuffer(&AppMode)`, both
exhaustive over `AppMode`, so a new mode fails the build here as it used to
in `draw.rs::mode_style` (improvise-zx8t).
### The JSON shape
Face names are the keys and a colour is spelled for a person: `"default"`, a
slot name (`"green"`, `"bright-black"`), a bare number for an index (`235`),
`"#rrggbb"` for `Rgb`. A misspelt face or attribute is **refused**, not
ignored (`deny_unknown_fields`, and `Face`'s `Deserialize` rejects an unknown
name): a file the user edits by hand should say where it is wrong rather than
silently draw the default. The faces map is a `BTreeMap` keyed on `Face`'s
derived order, so a re-download after an edit is a reviewable diff (clause 9).
The protocol crate derives and the front ends encode, so `serde_json` stays a
dev-dependency here.
### Choices the ADR did not make, made here
- **`minibuffer.*` inherits a `minibuffer` base face, not `mode.text-entry`.**
The ADR says the minibuffer faces "inherit `mode.text-entry` directly". But
the mode indicator and the minibuffer paint opposite halves of the cell — the
indicator is Black ink on an accent background, the minibuffer is accent ink
on `Indexed(235)` — so inheriting one from the other makes every attribute an
override and the chain decorative. The `minibuffer` face carries only what
the strip has in common (its background and bold), and encodes nothing about
green and yellow, which is the property the ADR's sentence was protecting.
- **`mode.tile-select` hangs directly off `mode`.** The ADR's decomposition
covers the thirteen DarkGray modes and the text-entry chain, and does not
place `TileSelect`.
- **The mode faces still state `Named(Black)` ink.** Clause 5 wants the twelve
`fg(Black).bg(accent)` sites to become `reverse` over the accent, but that is
not a no-op on a `TestBackend` buffer, and clause 8 names only one exception.
So it waits for improvise-nn4s.6 and a ruling.
- **No slot tables yet.** `Theme` has no `slots`/`dark`/`default_scheme`: they
are the browser's substitute for a terminal palette (clause 4), and the first
consumer is improvise-nn4s.5. Adding them then is a field with a serde
default, not a format break.
- **Only the faces some call site draws with exist.** The ADR's full vocabulary
(`emphasis`, `error`, `selection`, `grid.*`, `panel.*`) arrives with the
call sites that use it, because a face with no caller is a published name
with no evidence behind it.
### The category-add exception
`minibuffer.category-add` resolves to green in the shipped theme, where the old
literal was yellow: the user ruled the split an accident (ADR 004 Q1), and it
is clause 8's one declared exception. `AppMode::category_add` now carries
`MinibufColor::Green` too, so the browser, which still reads the wire colour
until improvise-nn4s.4, agrees with the TUI rather than drifting from it.