| 1 | import { |
| 2 | isModifierPairShortcut, |
| 3 | snapShotModifierPairLabel, |
| 4 | snapShotShortcutModifierPair, |
| 5 | type KeybindingShortcut, |
| 6 | type SnapShotModifier, |
| 7 | type SnapShotShortcut, |
| 8 | } from "@t3tools/contracts"; |
| 9 | import { parseKeybindingShortcut } from "@t3tools/shared/keybindings"; |
| 10 | |
| 11 | import { formatShortcutKeyLabel, formatShortcutLabel, shortcutConflictKey } from "../keybindings"; |
| 12 | import { isMacPlatform, isWindowsPlatform } from "./utils"; |
| 13 | |
| 14 | const DESKTOP_KEY_ALIASES: Readonly<Record<string, string>> = { |
| 15 | return: "enter", |
| 16 | page_up: "pageup", |
| 17 | page_down: "pagedown", |
| 18 | left: "arrowleft", |
| 19 | right: "arrowright", |
| 20 | up: "arrowup", |
| 21 | down: "arrowdown", |
| 22 | plus: "+", |
| 23 | minus: "-", |
| 24 | }; |
| 25 | |
| 26 | /** Portal labels are descriptions, not a keybinding protocol. Only parse known key notation. */ |
| 27 | export function parseDesktopSnapShotShortcut(label: string): KeybindingShortcut | null { |
| 28 | const text = label.trim().replace(/^Press\s+/i, ""); |
| 29 | if (!text) return null; |
| 30 | const gtk = text.match(/^((?:<[^<>]+>)+)([^<>]+)$/); |
| 31 | const input = (gtk ? gtk[1]!.replace(/<([^<>]+)>/g, "$1+") + gtk[2]! : text).replace( |
| 32 | /\b(super|win|primary)\b/gi, |
| 33 | (modifier) => (modifier.toLowerCase() === "primary" ? "ctrl" : "meta"), |
| 34 | ); |
| 35 | if (input !== "+" && input.endsWith("+") && !input.endsWith("++")) return null; |
| 36 | const shortcut = parseKeybindingShortcut(input); |
| 37 | if (!shortcut) return null; |
| 38 | const key = DESKTOP_KEY_ALIASES[shortcut.key] ?? shortcut.key; |
| 39 | if ( |
| 40 | key !== " " && |
| 41 | !/^[^\s<>]$/u.test(key) && |
| 42 | !/^(?:f(?:[1-9]|1\d|2[0-4])|enter|tab|escape|backspace|delete|insert|home|end|pageup|pagedown|arrow(?:left|right|up|down)|pause|printscreen)$/i.test( |
| 43 | key, |
| 44 | ) |
| 45 | ) { |
| 46 | return null; |
| 47 | } |
| 48 | return { ...shortcut, key }; |
| 49 | } |
| 50 | |
| 51 | export function formatSnapShotShortcutLabel( |
| 52 | shortcut: SnapShotShortcut, |
| 53 | platform = navigator.platform, |
| 54 | ): string { |
| 55 | return isModifierPairShortcut(shortcut) |
| 56 | ? snapShotModifierPairLabel(snapShotShortcutModifierPair(shortcut), isMacPlatform(platform)) |
| 57 | : formatShortcutLabel(shortcut, platform); |
| 58 | } |
| 59 | |
| 60 | function modifierKeyLabel(modifier: SnapShotModifier, platform: string): string { |
| 61 | if (modifier === "shift") return "⇧"; |
| 62 | if (isMacPlatform(platform)) { |
| 63 | if (modifier === "meta") return "⌘"; |
| 64 | if (modifier === "control") return "⌃"; |
| 65 | return "⌥"; |
| 66 | } |
| 67 | if (modifier === "meta") return isWindowsPlatform(platform) ? "⊞" : "Super"; |
| 68 | return modifier === "control" ? "Ctrl" : "Alt"; |
| 69 | } |
| 70 | |
| 71 | export function snapShotShortcutKeyLabels( |
| 72 | shortcut: SnapShotShortcut, |
| 73 | platform = navigator.platform, |
| 74 | ): readonly string[] { |
| 75 | if (isModifierPairShortcut(shortcut)) { |
| 76 | const label = modifierKeyLabel(snapShotShortcutModifierPair(shortcut), platform); |
| 77 | return [label, label]; |
| 78 | } |
| 79 | |
| 80 | const useMetaForMod = isMacPlatform(platform); |
| 81 | const modifiers: readonly [boolean, SnapShotModifier][] = [ |
| 82 | [shortcut.ctrlKey || (shortcut.modKey && !useMetaForMod), "control"], |
| 83 | [shortcut.altKey, "alt"], |
| 84 | [shortcut.shiftKey, "shift"], |
| 85 | [shortcut.metaKey || (shortcut.modKey && useMetaForMod), "meta"], |
| 86 | ]; |
| 87 | return [ |
| 88 | ...modifiers |
| 89 | .filter(([enabled]) => enabled) |
| 90 | .map(([, modifier]) => modifierKeyLabel(modifier, platform)), |
| 91 | formatShortcutKeyLabel(shortcut.key), |
| 92 | ]; |
| 93 | } |
| 94 | |
| 95 | export function sameSnapShotShortcut( |
| 96 | left: SnapShotShortcut, |
| 97 | right: SnapShotShortcut, |
| 98 | platform = navigator.platform, |
| 99 | ): boolean { |
| 100 | if (isModifierPairShortcut(left) || isModifierPairShortcut(right)) { |
| 101 | return ( |
| 102 | isModifierPairShortcut(left) && |
| 103 | isModifierPairShortcut(right) && |
| 104 | snapShotShortcutModifierPair(left) === snapShotShortcutModifierPair(right) |
| 105 | ); |
| 106 | } |
| 107 | return shortcutConflictKey(left, platform) === shortcutConflictKey(right, platform); |
| 108 | } |
| 109 | |
| 110 | export function snapShotKeybindingConflict<Command extends string>( |
| 111 | shortcut: SnapShotShortcut, |
| 112 | keybindings: ReadonlyArray<{ readonly command: Command; readonly shortcut: KeybindingShortcut }>, |
| 113 | platform = navigator.platform, |
| 114 | ): Command | null { |
| 115 | if (isModifierPairShortcut(shortcut)) return null; |
| 116 | const key = shortcutConflictKey(shortcut, platform); |
| 117 | return ( |
| 118 | keybindings.find((binding) => shortcutConflictKey(binding.shortcut, platform) === key) |
| 119 | ?.command ?? null |
| 120 | ); |
| 121 | } |