import { commands as defaultCommands, type ShellCommand } from "../commands.ts"; import type { Unsubscribe, Workbench } from "../workbench.ts"; /** The command tables bound on this page, so a view that reads every key can leave theirs alone. */ const bound = new Set(); const isMac = () => /Mac|iPhone|iPad/.test(navigator.platform); /** Runs shell commands from their keybindings. */ export function bindKeys(target: Window, workbench: Workbench, commands: readonly ShellCommand[] = defaultCommands): Unsubscribe { const mac = isMac(); const onKey = (event: KeyboardEvent) => { const match = commands.find((c) => c.keys && matches(c.keys, event, mac)); if (!match) return; event.preventDefault(); match.run(workbench); }; target.addEventListener("keydown", onKey); bound.add(commands); return () => { target.removeEventListener("keydown", onKey); bound.delete(commands); }; } /** Whether `event` is a bound command's keybinding. */ export function isBound(event: KeyboardEvent): boolean { const mac = isMac(); for (const commands of bound) if (commands.some((c) => c.keys && matches(c.keys, event, mac))) return true; return false; } /** Matches `Mod+Shift+E` against a key event by physical key, so Alt and Shift don't change the letter. */ export function matches(keys: string, event: Pick, mac: boolean): boolean { const parts = keys.split("+"); const key = parts.pop()!; const mods = new Set(parts.map((m) => (m === "Mod" ? (mac ? "Meta" : "Ctrl") : m))); if (mods.has("Meta") !== event.metaKey || mods.has("Ctrl") !== event.ctrlKey) return false; if (mods.has("Alt") !== event.altKey || mods.has("Shift") !== event.shiftKey) return false; return event.code === codeOf(key); } function codeOf(key: string): string { if (/^[A-Z]$/i.test(key)) return `Key${key.toUpperCase()}`; if (/^\d$/.test(key)) return `Digit${key}`; return { "\\": "Backslash", "`": "Backquote", "/": "Slash", ",": "Comma", ".": "Period" }[key] ?? key; }