返回 DeepSeek-Reasonix
keys.ts
根目录 / desktop / electron / src / main / browser / keys.ts
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
110 lines TYPESCRIPT