feat(protocol): one home for UI strings (improvise-x7sw, ktv1, k099)
Three tickets asked one question — where do UI strings live relative to a
serde wire enum — and answering it three times is the disease this unit exists
to cure. One answer, in `improvise-protocol/src/help.rs`:
**A UI string is keyed by a command name, never by a key, and never hangs off
the enum that crosses the wire.** `AppMode::label`/`hint_text` are gone;
`help::mode_label`/`help::hint_line` replace them, so the enum a client
deserializes carries no English at all (improvise-k099). **And wherever a
string names a key, the key is looked up rather than spelled**:
`Hint { keys, command, caption }` takes `keys` off `Keymaps::default_keymaps()`,
`command` is the semantic value a localized front end keys off, and `caption`
is the default English — the `AxisColor`/`axis_fg` shape.
- **improvise-x7sw**: `help::which_key` is the which-key popup's rows, keys and
captions together. The client already holds the keymaps, which is the answer
the ticket preferred, so `ViewState::transient_prefix` stays an address.
- **improvise-ktv1**: the help pages are data — `help::pages()` — with every
key row derived. `src/ui/help.rs` keeps its ratatui styles and loses ~310
lines of content; `web/app.js` paints rows it does not author, and
`harness/tests/shell.spec.js` asserts that `?` really draws them.
**What this deletes is the measure of it.**
`every_hint_key_resolves_in_its_mode` and its 60-line hint tokenizer are gone:
they existed to hold a hand-written string honest against the keymap, and the
string is derived now, so the property is true by construction
(`doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)`).
`Keymap::binding_hints` goes with them — `which_key` is the one walker, and the
binding dump reads it. What replaced the test checks the *other* direction, a
mode advertising a command nothing binds, which was never checkable before.
Two defects the derivation exposed, both fixed here: `key_label` dropped
modifiers, so Ctrl-arrows read as bare arrows, and `Char(' ')` printed a literal
space instead of `Space`.
**What is deliberately not derived**, and says so: the import wizard's keys
(its whole keymap is one `Any` binding into `HandleWizardKey`, which decides
per step inside the command — a real hole, left visible), `gg`/`yy`/`ZZ` and
the ex-commands (`HelpLine::Literal`), and the captions, which no test can
judge. `context/code/protocol.md` §8 has the argument.
Cost, stated: the hints changed shape. They are longer, they name arrow keys
beside `hjkl`, and two sub-lists on the Panels page lost their visual indent.
That is the price of the keys being true.
Evidence: 1321 tests pass, and the 7 Chromium shell tests pass against the
assembled site. Two faults were injected into the help overlay — dropping the
`paintHelp` call, and removing the `mode != Help` guard so it is never hidden —
and each fails the new spec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+43
-2
@@ -31,6 +31,7 @@ const el = {
|
||||
categories: document.getElementById("categories"),
|
||||
mode: document.getElementById("mode"),
|
||||
hint: document.getElementById("hint"),
|
||||
help: document.getElementById("help"),
|
||||
status: document.getElementById("status"),
|
||||
viewName: document.getElementById("view-name"),
|
||||
};
|
||||
@@ -177,9 +178,10 @@ function repaint() {
|
||||
painted = model.revision;
|
||||
|
||||
el.mode.textContent = model.mode;
|
||||
// The mode's own hint, off `AppMode::hint_text()`. The page spells no
|
||||
// keys itself; see `RenderModel::hint`.
|
||||
// The mode's own hint, off `help::hint_line`. The page spells no keys
|
||||
// itself; see `RenderModel::hint`.
|
||||
el.hint.textContent = model.hint;
|
||||
paintHelp(model);
|
||||
modelStatus = model.status;
|
||||
paintStatus();
|
||||
el.viewName.textContent = model.view_name;
|
||||
@@ -197,6 +199,45 @@ function repaint() {
|
||||
if (changed) sendViewport();
|
||||
}
|
||||
|
||||
/** The help overlay. An empty `help` is what says it is closed -- the page
|
||||
* asks no question about the mode, and writes none of the content: the rows,
|
||||
* their keys and the page titles are `improvise_protocol::help`'s, the same
|
||||
* ones the TUI draws (improvise-ktv1). */
|
||||
function paintHelp(model) {
|
||||
el.help.hidden = model.help.length === 0;
|
||||
if (el.help.hidden) {
|
||||
el.help.replaceChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "help-tabs";
|
||||
model.help_titles.forEach((title, i) => {
|
||||
const tab = document.createElement("span");
|
||||
tab.textContent = title;
|
||||
tab.className = i === model.help_page ? "help-tab active" : "help-tab";
|
||||
tabs.append(tab);
|
||||
});
|
||||
|
||||
const body = document.createElement("dl");
|
||||
body.className = "help-body";
|
||||
for (const row of model.help) {
|
||||
const keys = document.createElement("dt");
|
||||
keys.textContent = row.keys;
|
||||
keys.className = `help-${row.kind}`;
|
||||
body.append(keys);
|
||||
// A prose row has no description and spans both columns (its class says
|
||||
// so); emitting an empty `dd` beside it would leave a blank grid cell.
|
||||
if (row.description) {
|
||||
const desc = document.createElement("dd");
|
||||
desc.textContent = row.description;
|
||||
body.append(desc);
|
||||
}
|
||||
}
|
||||
|
||||
el.help.replaceChildren(tabs, body);
|
||||
}
|
||||
|
||||
/** The grid, painted from a model in which every frame decision is already
|
||||
* made (improvise-anr9). The page decides nothing here: which labels repeat
|
||||
* and are therefore blank, how many header levels there are, which columns
|
||||
|
||||
Reference in New Issue
Block a user