| 1 | import type { KeyboardInputEvent } from "electron"; |
| 2 | |
| 3 | type Modifier = "shift" | "control" | "alt" | "meta"; |
| 4 | |
| 5 | export interface Chord { |
| 6 | keyCode: string; |
| 7 | modifiers: Modifier[]; |
| 8 | char: string | null; |
| 9 | } |
| 10 | |
| 11 | const MODIFIERS: Record<string, Modifier> = { |
| 12 | shift: "shift", |
| 13 | control: "control", |
| 14 | ctrl: "control", |
| 15 | alt: "alt", |
| 16 | option: "alt", |
| 17 | meta: "meta", |
| 18 | command: "meta", |
| 19 | cmd: "meta", |
| 20 | super: "meta", |
| 21 | }; |
| 22 | |
| 23 | // DOM key names to Electron accelerator key codes where they differ. |
| 24 | const KEY_CODES: Record<string, string> = { |
| 25 | enter: "Return", |
| 26 | return: "Return", |
| 27 | escape: "Escape", |
| 28 | esc: "Escape", |
| 29 | arrowup: "Up", |
| 30 | arrowdown: "Down", |
| 31 | arrowleft: "Left", |
| 32 | arrowright: "Right", |
| 33 | up: "Up", |
| 34 | down: "Down", |
| 35 | left: "Left", |
| 36 | right: "Right", |
| 37 | space: "Space", |
| 38 | " ": "Space", |
| 39 | tab: "Tab", |
| 40 | backspace: "Backspace", |
| 41 | delete: "Delete", |
| 42 | del: "Delete", |
| 43 | insert: "Insert", |
| 44 | home: "Home", |
| 45 | end: "End", |
| 46 | pageup: "PageUp", |
| 47 | pagedown: "PageDown", |
| 48 | plus: "Plus", |
| 49 | "+": "Plus", |
| 50 | }; |
| 51 | |
| 52 | const CHAR_KEYS: Record<string, string> = { Return: "\r", Tab: "\t", Space: " " }; |
| 53 | |
| 54 | export function parseChord(input: string): Chord { |
| 55 | const raw = input.trim(); |
| 56 | if (raw === "") throw new Error("empty key chord"); |
| 57 | const parts = raw.split("+").map((part) => part.trim()); |
| 58 | // A literal "+" key ("Shift++", "+") ends the split with an empty element. |
| 59 | if (parts.length > 1 && parts[parts.length - 1] === "") { |
| 60 | parts.pop(); |
| 61 | if (parts[parts.length - 1] !== "") throw new Error(`malformed key chord "${input}"`); |
| 62 | parts[parts.length - 1] = "+"; |
| 63 | } |
| 64 | const modifiers: Modifier[] = []; |
| 65 | let key = ""; |
| 66 | for (const part of parts) { |
| 67 | if (part === "") throw new Error(`malformed key chord "${input}"`); |
| 68 | const modifier = MODIFIERS[part.toLowerCase()]; |
| 69 | if (modifier && !modifiers.includes(modifier)) { |
| 70 | modifiers.push(modifier); |
| 71 | continue; |
| 72 | } |
| 73 | if (key !== "") throw new Error(`key chord "${input}" names more than one key`); |
| 74 | key = part; |
| 75 | } |
| 76 | if (key === "") throw new Error(`key chord "${input}" has no key`); |
| 77 | const keyCode = keyCodeFor(key); |
| 78 | return { keyCode, modifiers, char: charFor(key, keyCode, modifiers) }; |
| 79 | } |
| 80 | |
| 81 | function keyCodeFor(key: string): string { |
| 82 | const mapped = KEY_CODES[key.toLowerCase()]; |
| 83 | if (mapped) return mapped; |
| 84 | if (/^f([1-9]|1\d|2[0-4])$/i.test(key)) return key.toUpperCase(); |
| 85 | if (key.length === 1) return key; |
| 86 | return key.charAt(0).toUpperCase() + key.slice(1); |
| 87 | } |
| 88 | |
| 89 | function charFor(key: string, keyCode: string, modifiers: Modifier[]): string | null { |
| 90 | if (modifiers.includes("control") || modifiers.includes("meta") || modifiers.includes("alt")) return null; |
| 91 | const special = CHAR_KEYS[keyCode]; |
| 92 | if (special) return special; |
| 93 | if (key.length !== 1) return null; |
| 94 | return modifiers.includes("shift") ? key.toUpperCase() : key; |
| 95 | } |
| 96 | |
| 97 | export function chordEvents(chord: Chord): KeyboardInputEvent[] { |
| 98 | const modifiers = chord.modifiers.length ? chord.modifiers : undefined; |
| 99 | const events: KeyboardInputEvent[] = [{ type: "keyDown", keyCode: chord.keyCode, modifiers }]; |
| 100 | if (chord.char !== null) events.push({ type: "char", keyCode: chord.char, modifiers }); |
| 101 | events.push({ type: "keyUp", keyCode: chord.keyCode, modifiers }); |
| 102 | return events; |
| 103 | } |
| 104 | |
| 105 | // "Control+a Enter" presses two chords in sequence; a chord never contains |
| 106 | // whitespace, so splitting on it is unambiguous. |
| 107 | export function parseKeySequence(keys: string): Chord[] { |
| 108 | return keys.split(/\s+/).filter((part) => part !== "").map(parseChord); |
| 109 | } |
| 110 |