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:
committed by
Edward Langley
parent
f7d2730736
commit
92f8ac2113
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user