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
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
<div id="grid" tabindex="0" aria-label="Grid"></div>
|
||||
</main>
|
||||
|
||||
<div id="help" role="dialog" aria-label="Help" hidden></div>
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -172,3 +172,62 @@ td input {
|
||||
background: color-mix(in srgb, var(--line) 45%, transparent);
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
/* The help overlay. Content is improvise_protocol::help's; this is paint. */
|
||||
#help {
|
||||
position: fixed;
|
||||
inset: 2rem;
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--accent);
|
||||
background: var(--bg);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.help-tabs {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.help-tab.active {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-body {
|
||||
display: grid;
|
||||
grid-template-columns: 16rem 1fr;
|
||||
gap: 0 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.help-body dt,
|
||||
.help-body dd {
|
||||
margin: 0;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
|
||||
.help-heading {
|
||||
grid-column: 1 / -1;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.help-banner {
|
||||
grid-column: 1 / -1;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.help-text,
|
||||
.help-accent,
|
||||
.help-blank {
|
||||
grid-column: 1 / -1;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.help-dim {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user