Open History to see the last commit
1import { isMacPlatform, US_SHIFTED_TO_UNSHIFTED_KEYS } from "@t3tools/shared/keybindings";
2
3// This order mirrors GhosttyKey in ghostty/vt/key/event.h. The values are
4// intentionally derived from the official W3C-aligned enum instead of
5// maintaining a second keyboard protocol.
6const ghosttyKeyboardCodes = [
7 "Unidentified",
8 "Backquote",
9 "Backslash",
10 "BracketLeft",
11 "BracketRight",
12 "Comma",
13 "Digit0",
14 "Digit1",
15 "Digit2",
16 "Digit3",
17 "Digit4",
18 "Digit5",
19 "Digit6",
20 "Digit7",
21 "Digit8",
22 "Digit9",
23 "Equal",
24 "IntlBackslash",
25 "IntlRo",
26 "IntlYen",
27 "KeyA",
28 "KeyB",
29 "KeyC",
30 "KeyD",
31 "KeyE",
32 "KeyF",
33 "KeyG",
34 "KeyH",
35 "KeyI",
36 "KeyJ",
37 "KeyK",
38 "KeyL",
39 "KeyM",
40 "KeyN",
41 "KeyO",
42 "KeyP",
43 "KeyQ",
44 "KeyR",
45 "KeyS",
46 "KeyT",
47 "KeyU",
48 "KeyV",
49 "KeyW",
50 "KeyX",
51 "KeyY",
52 "KeyZ",
53 "Minus",
54 "Period",
55 "Quote",
56 "Semicolon",
57 "Slash",
58 "AltLeft",
59 "AltRight",
60 "Backspace",
61 "CapsLock",
62 "ContextMenu",
63 "ControlLeft",
64 "ControlRight",
65 "Enter",
66 "MetaLeft",
67 "MetaRight",
68 "ShiftLeft",
69 "ShiftRight",
70 "Space",
71 "Tab",
72 "Convert",
73 "KanaMode",
74 "NonConvert",
75 "Delete",
76 "End",
77 "Help",
78 "Home",
79 "Insert",
80 "PageDown",
81 "PageUp",
82 "ArrowDown",
83 "ArrowLeft",
84 "ArrowRight",
85 "ArrowUp",
86 "NumLock",
87 "Numpad0",
88 "Numpad1",
89 "Numpad2",
90 "Numpad3",
91 "Numpad4",
92 "Numpad5",
93 "Numpad6",
94 "Numpad7",
95 "Numpad8",
96 "Numpad9",
97 "NumpadAdd",
98 "NumpadBackspace",
99 "NumpadClear",
100 "NumpadClearEntry",
101 "NumpadComma",
102 "NumpadDecimal",
103 "NumpadDivide",
104 "NumpadEnter",
105 "NumpadEqual",
106 "NumpadMemoryAdd",
107 "NumpadMemoryClear",
108 "NumpadMemoryRecall",
109 "NumpadMemoryStore",
110 "NumpadMemorySubtract",
111 "NumpadMultiply",
112 "NumpadParenLeft",
113 "NumpadParenRight",
114 "NumpadSubtract",
115 "NumpadSeparator",
116 "NumpadArrowUp",
117 "NumpadArrowDown",
118 "NumpadArrowRight",
119 "NumpadArrowLeft",
120 "NumpadBegin",
121 "NumpadHome",
122 "NumpadEnd",
123 "NumpadInsert",
124 "NumpadDelete",
125 "NumpadPageUp",
126 "NumpadPageDown",
127 "Escape",
128 "F1",
129 "F2",
130 "F3",
131 "F4",
132 "F5",
133 "F6",
134 "F7",
135 "F8",
136 "F9",
137 "F10",
138 "F11",
139 "F12",
140 "F13",
141 "F14",
142 "F15",
143 "F16",
144 "F17",
145 "F18",
146 "F19",
147 "F20",
148 "F21",
149 "F22",
150 "F23",
151 "F24",
152 "F25",
153 "Fn",
154 "FnLock",
155 "PrintScreen",
156 "ScrollLock",
157 "Pause",
158 "BrowserBack",
159 "BrowserFavorites",
160 "BrowserForward",
161 "BrowserHome",
162 "BrowserRefresh",
163 "BrowserSearch",
164 "BrowserStop",
165 "Eject",
166 "LaunchApp1",
167 "LaunchApp2",
168 "LaunchMail",
169 "MediaPlayPause",
170 "MediaSelect",
171 "MediaStop",
172 "MediaTrackNext",
173 "MediaTrackPrevious",
174 "Power",
175 "Sleep",
176 "AudioVolumeDown",
177 "AudioVolumeMute",
178 "AudioVolumeUp",
179 "WakeUp",
180 "Copy",
181 "Cut",
182 "Paste",
183] as const;
184
185const codeToGhosttyKey = new Map<string, number>(
186 ghosttyKeyboardCodes.map((code, index) => [code, index]),
187);
188
189export function ghosttyKeyForCode(code: string): number {
190 return codeToGhosttyKey.get(code) ?? 0;
191}
192
193// With NumLock off, keypad digits report navigation keys; Ghostty encodes those as keypad variants.
194const NUMPAD_NAVIGATION_KEYS = new Map<string, number>(
195 (
196 [
197 ["ArrowDown", "NumpadArrowDown"],
198 ["ArrowLeft", "NumpadArrowLeft"],
199 ["ArrowRight", "NumpadArrowRight"],
200 ["ArrowUp", "NumpadArrowUp"],
201 ["Clear", "NumpadBegin"],
202 ["Delete", "NumpadDelete"],
203 ["End", "NumpadEnd"],
204 ["Home", "NumpadHome"],
205 ["Insert", "NumpadInsert"],
206 ["PageDown", "NumpadPageDown"],
207 ["PageUp", "NumpadPageUp"],
208 ] as const
209 ).map(([key, code]) => [key, ghosttyKeyForCode(code)]),
210);
211
212export function ghosttyKeyForEvent(event: Pick<KeyboardEvent, "code" | "key">): number {
213 return (
214 (event.code.startsWith("Numpad") && NUMPAD_NAVIGATION_KEYS.get(event.key)) ||
215 ghosttyKeyForCode(event.code) ||
216 // Android browsers can omit the physical code for keys such as Backspace.
217 ghosttyKeyForCode(event.key)
218 );
219}
220
221export interface GhosttyKeyboardLayoutMap {
222 get(code: string): string | undefined;
223}
224
225let keyboardLayoutMapPromise: Promise<GhosttyKeyboardLayoutMap | undefined> | undefined;
226
227export function loadGhosttyKeyboardLayoutMap(): Promise<GhosttyKeyboardLayoutMap | undefined> {
228 if (keyboardLayoutMapPromise) return keyboardLayoutMapPromise;
229 const browserNavigator = globalThis.navigator as
230 | (Navigator & {
231 readonly keyboard?: {
232 getLayoutMap(): Promise<GhosttyKeyboardLayoutMap>;
233 };
234 })
235 | undefined;
236 const keyboard = browserNavigator?.keyboard;
237 const promise = keyboard?.getLayoutMap().catch(() => undefined) ?? Promise.resolve(undefined);
238 keyboardLayoutMapPromise = promise;
239 return promise;
240}
241
242// Browsers omit consumed modifiers; macOS Option composes layout characters too.
243export function ghosttyConsumedMods(
244 event: Pick<KeyboardEvent, "altKey" | "ctrlKey" | "key" | "metaKey" | "shiftKey">,
245 platform = navigator.platform,
246): number {
247 if (event.ctrlKey || event.metaKey || [...event.key].length !== 1) return 0;
248 if (event.altKey && !isMacPlatform(platform)) return 0;
249 return (event.shiftKey ? 1 : 0) | (event.altKey ? 1 << 2 : 0);
250}
251
252export function ghosttyUnshiftedCodepoint(
253 event: Pick<KeyboardEvent, "code" | "key" | "shiftKey">,
254 layoutMap?: GhosttyKeyboardLayoutMap,
255): number {
256 if ([...event.key].length !== 1) return 0;
257 const layoutCharacter = layoutMap?.get(event.code);
258 if (layoutCharacter && [...layoutCharacter].length === 1) {
259 return layoutCharacter.codePointAt(0) ?? 0;
260 }
261 if (/^[A-Z]$/u.test(event.key)) return event.key.charCodeAt(0) + 32;
262 if (event.shiftKey) {
263 const unshiftedCharacter = US_SHIFTED_TO_UNSHIFTED_KEYS.get(event.key);
264 if (unshiftedCharacter) return unshiftedCharacter.codePointAt(0) ?? 0;
265 const lowercase = event.key.toLowerCase();
266 if (lowercase !== event.key && [...lowercase].length === 1) {
267 return lowercase.codePointAt(0) ?? 0;
268 }
269 // Without layout data, the unshifted character is unknown; guessing corrupts Kitty alternate keys.
270 return 0;
271 }
272 return event.key.codePointAt(0) ?? 0;
273}