// THE LEDGER, ASSERTED. `GROUPS` in client/components/ShortcutsHelp.tsx is the // one place a keyboard binding exists; this file is the arithmetic on it that // does not need a browser, and it is the same arithmetic `npm run check-keymap` // runs — one implementation in client/keymap.ts, driven from two doors, so a // gate that passes and a suite that passes can never disagree. // // What it is guarding against is a bug with no symptom at the crash site: two // rows claiming one keystroke. One handler answers, the other never sees the // event, and neither knows the other exists — so it surfaces weeks later as // "Ctrl+B does nothing", on one platform, from one reader. There is nothing to // grep for, because nothing is wrong with either binding. // // The grammar half matters as much as the collision half. A row's `keys` array // is PRINTED as chips and READ here as a chord, and those two only stay // the same thing while every row spells a chord the same way: modifiers in one // canonical order, one key token, `↑ / ↓` for a pair. A row that spells itself // differently still renders — it just stops being comparable, which is how a // collision hides from the gate that exists to find it. // // tests/shortcuts.test.ts is the other half of the keyboard story: this file // asks whether a binding is UNIQUE, that one asks whether it can be REACHED // from a keyboard that types no Latin letters. A new binding needs both. import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { describe, it } from "node:test"; import { RESOLVED, parseGroups, parseKeymapDoc, parseKeys, scopeOverlap, } from "../client/keymap.ts"; const read = (rel: string) => readFileSync(new URL(rel, import.meta.url).pathname, "utf8"); const ledger = parseGroups(read("../client/components/ShortcutsHelp.tsx")); const doc = parseKeymapDoc(read("../docs/keymap.md")); describe("the keymap ledger parses", () => { it("reads GROUPS out of the source with no complaints", () => { assert.deepEqual(ledger.errors, []); // A parser that quietly matches nothing reports a table with no // collisions, which is exactly what a broken parser and a healthy table // look like from the outside. assert.ok(ledger.rows.length > 30, `only ${ledger.rows.length} rows parsed`); assert.ok(new Set(ledger.rows.map((r) => r.group)).size > 3); }); it("gives every row an answer — a key, or the surface that carries it", () => { for (const row of ledger.rows) { assert.ok( row.keys !== null || row.via !== null, `${row.label} (ShortcutsHelp.tsx:${row.line}) prints a label and an empty key column`, ); } }); it("spells every row's keys in the shape the gate expects", () => { for (const row of ledger.rows) { if (row.keys === null) continue; const { chords, error } = parseKeys(row.keys); assert.equal(error, null, `${row.label} (ShortcutsHelp.tsx:${row.line}): ${error}`); assert.ok(chords.length > 0, `${row.label} parsed to no chord at all`); for (const chord of chords) { assert.equal(chord.id, [...chord.mods, chord.key].join("+")); } } }); }); describe("no two bindings are the same keystroke", () => { /** chord id → the rows claiming it. */ const claims = new Map(); for (const row of ledger.rows) { if (row.keys === null) continue; for (const chord of parseKeys(row.keys).chords) { const at = claims.get(chord.id) ?? []; at.push(row); claims.set(chord.id, at); } } const pairs: Array<{ id: string; a: string; b: string }> = []; for (const [id, rows] of claims) { for (let i = 0; i < rows.length; i++) { for (let j = i + 1; j < rows.length; j++) { if (scopeOverlap(rows[i], rows[j]) !== null) { pairs.push({ id, a: rows[i].label, b: rows[j].label }); } } } } it("has no undeclared collision", () => { const declared = new Set(RESOLVED.map((r) => `${r.chord} ${[...r.rows].sort().join(" ")}`)); const undeclared = pairs .filter((p) => !declared.has(`${p.id} ${[p.a, p.b].sort().join(" ")}`)) .map((p) => `${p.id}: ${p.a} ⊗ ${p.b}`); assert.deepEqual(undeclared, []); }); it("declares no collision that has stopped happening", () => { // Dead exceptions are the reason check-i18n fails on a dead dictionary // key: an argued-out paragraph about two rows that no longer meet is a // claim the next reader will believe. const live = new Set(pairs.map((p) => `${p.id} ${[p.a, p.b].sort().join(" ")}`)); for (const entry of RESOLVED) { const token = `${entry.chord} ${[...entry.rows].sort().join(" ")}`; assert.ok(live.has(token), `RESOLVED still names ${entry.rows.join(" ⊗ ")} on ${entry.chord}`); } }); it("every declared collision says where the tie is broken", () => { for (const entry of RESOLVED) { assert.ok(entry.why.length > 80, `${entry.chord}: "${entry.why}" is a shrug, not a rule`); } }); it("counts a shell and a runtime as scope, and admin as neither", () => { const app = { shell: "app" as const, desktop: false }; const blog = { shell: "blog" as const, desktop: false }; const both = { shell: null, desktop: false }; const only = { shell: null, desktop: true }; assert.equal(scopeOverlap(app, blog), null); assert.deepEqual(scopeOverlap(app, both)?.shells, ["app"]); assert.deepEqual(scopeOverlap(both, only)?.runtimes, ["desktop"]); // An admin session sees the visitor's rows AND its own, so `admin` can // never keep two bindings apart — the reader with the most keys is // exactly the reader a collision reaches. assert.notEqual(scopeOverlap({ ...both }, { ...both }), null); }); }); describe("docs/keymap.md is a rendering of the ledger", () => { it("parses, between its markers", () => { assert.deepEqual(doc.errors, []); assert.ok(doc.chords.length > 0); }); it("claims exactly the chords GROUPS binds", () => { const bound = new Set( ledger.rows.flatMap((r) => (r.keys === null ? [] : parseKeys(r.keys).chords.map((c) => c.id))), ); const written = new Set(doc.chords.map((d) => d.chord.id)); assert.deepEqual([...bound].filter((id) => !written.has(id)), [], "bound, undocumented"); assert.deepEqual([...written].filter((id) => !bound.has(id)), [], "documented, unbound"); }); }); describe("the chord grammar", () => { it("reads a plain chord", () => { assert.deepEqual(parseKeys(["Ctrl/Cmd", "Alt", "Shift", "B"]).chords.map((c) => c.id), [ "Mod+Alt+Shift+b", ]); }); it("expands the alternatives a single row prints", () => { // `Ctrl/Cmd` `↑ / ↓` is ONE row because move-line-up and move-line-down // are one thing to learn, and two chords because they are two keystrokes. assert.deepEqual(parseKeys(["Ctrl/Cmd", "↑ / ↓"]).chords.map((c) => c.id), [ "Mod+Up", "Mod+Down", ]); assert.deepEqual(parseKeys(["← / →"]).chords.map((c) => c.id), ["Left", "Right"]); }); it("keeps the slash that is a KEY out of the slash that is a separator", () => { // Ctrl/Cmd+/ opens the sheet this whole ledger lives in. Splitting its // key token on a bare "/" made it two empty alternatives — the binding // most likely to be pressed first, parsed into nothing. assert.deepEqual(parseKeys(["Ctrl/Cmd", "/"]).chords.map((c) => c.id), ["Mod+/"]); }); it("refuses a spelling that would hide a collision", () => { assert.match(parseKeys(["Ctrl/Cmd", "Shift", "Alt", "T"]).error ?? "", /out of order/); assert.match(parseKeys(["Cmd", "T"]).error ?? "", /not a modifier/); assert.match(parseKeys(["Ctrl/Cmd", "Shift"]).error ?? "", /is a modifier, not a key/); assert.match(parseKeys(["Ctrl/Cmd", "Meta"]).error ?? "", /not a key/); assert.match(parseKeys([]).error ?? "", /empty/); }); }); // THE WAY BACK's chord: the twin's plus Shift, a keystroke of its own, bound // in the global listener, for an admin, in both shells. describe("the chrome-language chord", () => { const row = ledger.rows.find((r) => r.label === "scChromeLang"); it("is in the ledger as Ctrl/Cmd Alt Shift L, for an admin, in both shells", () => { assert.ok(row, "no scChromeLang row"); assert.deepEqual(parseKeys(row.keys ?? []).chords.map((c) => c.id), ["Mod+Alt+Shift+l"]); assert.equal(row.admin, true); assert.equal(row.shell ?? null, null); }); it("is not the twin's chord", () => { const twin = ledger.rows.find((r) => r.label === "cmdTwinSwitch"); assert.ok(twin); assert.notDeepEqual(parseKeys(twin.keys ?? []).chords.map((c) => c.id), parseKeys(row?.keys ?? []).chords.map((c) => c.id)); }); it("is answered in the global listener, which keeps Shift away from the twin", () => { const src = read("../client/globalKeys.ts"); assert.match(src, /keymap: scChromeLang/); assert.match(src, /e\.altKey && e\.shiftKey && isKey\(e, "l"\)[^\n]*\n[^\n]*keymap: scChromeLang[\s\S]{0,120}toggleChromeLang\(\)/); assert.match(src, /key === "l" && e\.altKey && !e\.shiftKey/); }); });