Files
airship/scripts/gen-controls.d.mts
T
Nayan 45261f147e feat(overlay): declare every command once, and generate the reference from it
Chords were string literals at each `keys.bind` call site, so the only thing
that knew a command existed was the line that bound it. Twenty-seven of the
thirty-three shortcuts appeared nowhere in the product or the docs, five menu
rows showed Mac glyphs to Windows users, and one advertised `⌘Z` for a feature
`⌘Z` has never run. The only reference was six hand-written rows in `README.md`,
and its copy under `apps/cli/` had already drifted.

`keys/catalog.ts` is the declaration: chord, title, sentence, mode, surface, and
where a scoped command applies. A binding supplies only `run`, `when` and
`within`. `CommandId` is a union, so a mistyped id is a compile error rather
than a shortcut that quietly never fires.

Three surfaces read it. `keys/shortcuts-panel.ts` is the `?` sheet, grouped and
marking what is live right now; `keys/palette.ts` is `⌘K`; and `MenuItem.command`
renders a menu row's chord so nothing spells one by hand — `catalog.test.ts`
fails any `hint:` literal that looks like a keystroke.

`scripts/gen-controls.mjs` is the fourth reader. It imports the `.ts` catalog
directly under `--experimental-strip-types`, which is why that module may
contain no value imports, and writes `CONTROLS.md` plus the short table in
`README.md`. `keys/controls-doc.test.ts` byte-compares the committed files
against the same renderers, so drift fails the suite even on a Node that cannot
strip types.

The tooltip chip moves with it. It used to be found by matching the tip's own
*text* against a binding's label — elegant until someone reworded a tip, at
which point the chip vanished with nothing failing. A control now names its
command with `data-key`.
2026-08-16 13:05:37 +05:30

56 lines
1.9 KiB
TypeScript

/**
* Types for the two renderers `gen-controls.mjs` exports.
*
* Hand-written and deliberately structural. The script is plain JavaScript
* because every generator in this repo is — it runs from a Makefile target and
* a CI step, neither of which builds anything first — but
* `packages/overlay/src/keys/controls-doc.test.ts` imports the same two
* functions so that the drift gate and the writer cannot diverge, and that test
* is TypeScript.
*
* The parameter shapes are the fields the renderers actually read, not the full
* `CommandSpec` and `GestureSpec` — importing those from `packages/overlay`
* would point a repo-root script at a workspace package's internals for no gain,
* and the real definitions are checked where they live.
*/
interface RenderedCommand {
readonly display?: string;
readonly doc: string;
readonly essential?: boolean;
readonly group: string;
readonly keys: readonly string[];
readonly mode: string;
readonly primary?: readonly string[];
readonly surface: string;
readonly title: string;
readonly where?: string;
}
interface RenderedGesture {
readonly doc: string;
readonly essential?: boolean;
readonly input: string;
/** The Windows/Linux spelling, when it differs from `input`. */
readonly inputPc?: string;
readonly mode: string;
readonly surface: string;
readonly title: string;
}
interface RenderInput {
readonly commands: readonly RenderedCommand[];
readonly displayChord: (chord: string, platform: "mac" | "pc") => string;
readonly gestures: readonly RenderedGesture[];
readonly groups: readonly string[];
readonly notes: readonly string[];
}
/** The whole of `CONTROLS.md`. */
export function renderControls(input: RenderInput): string;
/** The short table between the markers in `README.md`. */
export function renderEssentials(
input: Omit<RenderInput, "groups" | "notes">
): string;