Merge: one keymap table, shared by the TUI and the browser (improvise-bpq2 successor)
The browser's hand-written key classifier was a second spelling of default_keymaps(), held honest by a lockstep test over a hand-written list -- doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)'s exact shape. So the TABLE moved to improvise-protocol and improvise-command keeps only what needs a CmdRegistry. Evidence is binding_dump_diff, not the suite: 4021 resolved (mode, key, modifier) lookups over sixteen modes, BYTE-IDENTICAL, with the dump kept as an opt-in test and proved discriminating by renaming one binding. The boundary that did NOT move is the better one: key -> intent is shared, intent -> Command stays client-side, and the named list of what still needs the host is now a list of COMMAND NAMES rather than keys -- so a rebinding no longer invalidates it. AND A LOCKSTEP TEST FOUND A LIVE BUG, BY A COMMA. CellDisplay::text has been through format_f64; App::display_value_at_cursor has not. Seeding an editor from the drawn text and committing turns a stored 1200 into the string '1,200' -- display rounding fed back into the model, which design-principles 4 forbids outright. CellDisplay::edit_text puts the unformatted value on the wire, and it repairs web/app.js's click-to-edit, which had been seeding its input from td.textContent since it was written. That bug is on the live page today for any value with a thousands separator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
This commit is contained in:
+20
-5
@@ -84,6 +84,7 @@ const el = {
|
||||
grid: document.getElementById("grid"),
|
||||
categories: document.getElementById("categories"),
|
||||
mode: document.getElementById("mode"),
|
||||
hint: document.getElementById("hint"),
|
||||
status: document.getElementById("status"),
|
||||
viewName: document.getElementById("view-name"),
|
||||
};
|
||||
@@ -233,6 +234,9 @@ 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`.
|
||||
el.hint.textContent = model.hint;
|
||||
modelStatus = model.status;
|
||||
paintStatus();
|
||||
el.viewName.textContent = model.view_name;
|
||||
@@ -261,15 +265,23 @@ function gridTable(model) {
|
||||
|
||||
row.cells.forEach((cell, c) => {
|
||||
const td = tr.insertCell();
|
||||
td.textContent = cell.text;
|
||||
td.classList.toggle("numeric", cell.numeric);
|
||||
// A keyboard edit is open on the selected cell when `model.editing` is
|
||||
// a string; it holds what has been typed so far. Both facts are
|
||||
// decided in Rust (`RenderModel::editing`) — the page does not know
|
||||
// which modes are editing modes or which buffer they type into.
|
||||
// Without this the mode indicator would say INSERT while every
|
||||
// keystroke vanished into a buffer nobody drew.
|
||||
const typing = cell.selected && typeof model.editing === "string";
|
||||
td.textContent = typing ? model.editing + "\u258f" : cell.text;
|
||||
td.classList.toggle("numeric", cell.numeric && !typing);
|
||||
td.classList.toggle("error", cell.error);
|
||||
td.classList.toggle("selected", cell.selected);
|
||||
td.classList.toggle("typing", typing);
|
||||
// `editable` is the server's answer relayed through Rust: a stored
|
||||
// cell. An aggregate is a drill target and drill is not on the wire
|
||||
// yet, so it is simply not offered here.
|
||||
td.classList.toggle("editable", cell.editable);
|
||||
if (cell.editable) td.addEventListener("click", () => edit(td, r, c));
|
||||
if (cell.editable) td.addEventListener("click", () => edit(td, r, c, cell.edit_text));
|
||||
});
|
||||
});
|
||||
return table;
|
||||
@@ -277,10 +289,13 @@ function gridTable(model) {
|
||||
|
||||
/** Open an inline editor. Committing sends a frame; the value that comes
|
||||
* back is the server's, never the string typed here. */
|
||||
function edit(td, row, col) {
|
||||
function edit(td, row, col, seed) {
|
||||
if (editing) return;
|
||||
editing = { row, col };
|
||||
const original = td.textContent;
|
||||
// `seed` is `RenderCell::edit_text` -- the STORED value, not the drawn
|
||||
// one. `td.textContent` has been through the number format, so a stored
|
||||
// 1200 reads "1,200" and committing that stores the string "1,200".
|
||||
const original = seed;
|
||||
const input = document.createElement("input");
|
||||
input.value = original;
|
||||
td.replaceChildren(input);
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<h1>improvise</h1>
|
||||
<span id="view-name"></span>
|
||||
<span id="mode"></span>
|
||||
<span id="hint"></span>
|
||||
<span id="status" role="status"></span>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -89,6 +89,18 @@ td.selected {
|
||||
outline: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
#hint {
|
||||
opacity: 0.7;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* A keyboard edit in progress. Left-aligned even in a numeric column,
|
||||
because what is drawn is the text being typed rather than a value. */
|
||||
td.typing {
|
||||
text-align: left;
|
||||
outline: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
#grid {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
Reference in New Issue
Block a user