1import type { KeybindingShortcut } from "@t3tools/contracts";
2
3// Matching only: the desktop preview guest bundles this file without the rest of the
4// keybindings module, so it stays free of parsing, defaults, and contracts runtime code.
5
6export interface ShortcutEventLike {
7 getModifierState?: (key: "AltGraph") => boolean;
8 type?: string;
9 code?: string;
10 key: string;
11 repeat?: boolean;
12 metaKey: boolean;
13 ctrlKey: boolean;
14 shiftKey: boolean;
15 altKey: boolean;
16}
17
18export interface ShortcutModifierStateLike {
19 metaKey: boolean;
20 ctrlKey: boolean;
21 shiftKey: boolean;
22 altKey: boolean;
23}
24
25export const EVENT_CODE_SHORTCUT_KEYS: Readonly<Record<string, string>> = {
26 Backquote: "`",
27 Backslash: "\\",
28 BracketLeft: "[",
29 BracketRight: "]",
30 Comma: ",",
31 Digit0: "0",
32 Digit1: "1",
33 Digit2: "2",
34 Digit3: "3",
35 Digit4: "4",
36 Digit5: "5",
37 Digit6: "6",
38 Digit7: "7",
39 Digit8: "8",
40 Digit9: "9",
41 Equal: "=",
42 Minus: "-",
43 Period: ".",
44 Quote: "'",
45 Semicolon: ";",
46 Slash: "/",
47};
48
49export function isMacPlatform(platform: string): boolean {
50 return /mac|iphone|ipad|ipod/i.test(platform);
51}
52
53export function normalizeEventKey(key: string): string {
54 const normalized = key.toLowerCase();
55 if (normalized === "esc") return "escape";
56 return normalized;
57}
58
59export function shortcutKeyFromEvent(
60 event: Pick<ShortcutEventLike, "key" | "code" | "shiftKey" | "altKey">,
61): string {
62 const layoutKey = normalizeEventKey(event.key);
63 if (/^[a-z]$/.test(layoutKey)) return layoutKey;
64 // An unshifted "+" is a dedicated zoom key (German, Spanish, Nordic); Czech types it on Digit1.
65 const isZoomPlus = layoutKey === "+" && !event.shiftKey && !event.altKey;
66 if (isZoomPlus && !event.code?.startsWith("Digit")) return layoutKey;
67 const physicalKey = event.code ? EVENT_CODE_SHORTCUT_KEYS[event.code] : undefined;
68 return physicalKey ?? layoutKey;
69}
70
71export function resolveEventKeys(event: ShortcutEventLike): Set<string> {
72 const layoutKey = normalizeEventKey(event.key);
73 const keys = new Set([layoutKey]);
74 const letterCode = event.code?.match(/^Key([A-Z])$/)?.[1];
75 if (letterCode && !/^[a-z]$/.test(layoutKey)) {
76 keys.add(letterCode.toLowerCase());
77 }
78 keys.add(shortcutKeyFromEvent(event));
79 return keys;
80}
81
82export function matchesKeybindingShortcutModifiers(
83 event: ShortcutModifierStateLike,
84 shortcut: KeybindingShortcut,
85 platform: string,
86): boolean {
87 const useMetaForMod = isMacPlatform(platform);
88 const expectedMeta = shortcut.metaKey || (shortcut.modKey && useMetaForMod);
89 const expectedCtrl = shortcut.ctrlKey || (shortcut.modKey && !useMetaForMod);
90 return (
91 event.metaKey === expectedMeta &&
92 event.ctrlKey === expectedCtrl &&
93 event.shiftKey === shortcut.shiftKey &&
94 event.altKey === shortcut.altKey
95 );
96}
97
98export function matchesKeybindingShortcut(
99 event: ShortcutEventLike,
100 shortcut: KeybindingShortcut,
101 platform: string,
102): boolean {
103 if (
104 !isMacPlatform(platform) &&
105 event.getModifierState?.("AltGraph") &&
106 !/^[a-z0-9]$/i.test(event.key)
107 )
108 return false;
109 if (!matchesKeybindingShortcutModifiers(event, shortcut, platform)) return false;
110 return resolveEventKeys(event).has(shortcut.key);
111}