/** Modifier sort order for consistent string representation. */ const MODIFIER_ORDER: Modifier[] = ['Ctrl', 'Meta', 'Alt', 'Shift']; /** Known modifier names for validation. */ const KNOWN_MODIFIERS = new Set([ 'Ctrl', 'Meta', 'Alt', 'Shift', 'Mod', ]); import type { Modifier, Hotkey } from './types'; /** * Detect if the current platform is macOS. */ export function isMacPlatform(): boolean { if (typeof navigator === 'undefined') return false; return /Mac|iPod|iPhone|iPad/.test(navigator.platform); } /** * Normalize a hotkey by expanding 'Mod' to the platform-specific modifier. * Also lowercases the key for consistent matching. */ export function normalizeHotkey(hotkey: Hotkey, isMac: boolean): Hotkey { const modifiers: Modifier[] = hotkey.modifiers.map((mod) => { if (mod === 'Mod') { return isMac ? 'Meta' : 'Ctrl'; } return mod; }); return { modifiers, key: hotkey.key.toLowerCase(), }; } /** * Convert a hotkey to a consistent string representation. * Modifiers are sorted consistently and key is uppercased. * Used as a lookup key in the hotkey registry. */ export function hotkeyToString(hotkey: Hotkey): string { const sortedMods = [...hotkey.modifiers].sort( (a, b) => MODIFIER_ORDER.indexOf(a) - MODIFIER_ORDER.indexOf(b), ); const parts = [...sortedMods, hotkey.key.toUpperCase()]; return parts.join('+'); } /** * Parse a hotkey string back into a Hotkey object. * E.g., "Ctrl+Shift+P" -> { modifiers: ['Ctrl', 'Shift'], key: 'p' } * Unknown modifiers are silently dropped. */ export function parseHotkeyString(str: string): Hotkey { const parts = str.split('+'); const key = parts.pop()!.toLowerCase(); const modifiers = parts.filter((p) => KNOWN_MODIFIERS.has(p)) as Modifier[]; return { modifiers, key }; } /** * Convert a keyboard event to a Hotkey object. */ export function eventToHotkey(event: KeyboardEvent): Hotkey { const modifiers: Modifier[] = []; if (event.ctrlKey) modifiers.push('Ctrl'); if (event.metaKey) modifiers.push('Meta'); if (event.altKey) modifiers.push('Alt'); if (event.shiftKey) modifiers.push('Shift'); return { modifiers, key: event.key.toLowerCase(), }; } /** * Display glyphs for non-printable keys (matched case-insensitively against * `KeyboardEvent.key`). Printable keys fall back to their uppercased value. */ const KEY_DISPLAY_SYMBOLS: Record = { arrowleft: '←', // ← arrowright: '→', // → arrowup: '↑', // ↑ arrowdown: '↓', // ↓ enter: '↵', // ↵ return: '↵', // ↵ tab: '⇥', // ⇥ backspace: '⌫', // ⌫ delete: '⌦', // ⌦ escape: 'Esc', ' ': 'Space', space: 'Space', }; /** * Format a hotkey for display to the user. * Uses platform-appropriate symbols (e.g., Command on Mac, Ctrl on Windows). */ export function formatHotkeyForDisplay(hotkey: Hotkey, isMac: boolean): string { const symbols: Record = isMac ? { Ctrl: '^', Meta: '\u2318', Alt: '\u2325', Shift: '\u21E7', Mod: '\u2318', } : { Ctrl: 'Ctrl', Meta: 'Win', Alt: 'Alt', Shift: 'Shift', Mod: 'Ctrl', }; const mods = hotkey.modifiers.map((m) => symbols[m] ?? m); const key = KEY_DISPLAY_SYMBOLS[hotkey.key.toLowerCase()] ?? hotkey.key.toUpperCase(); if (isMac) { return mods.join('') + key; } else { return [...mods, key].join('+'); } }