Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132/** 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<string>([ '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<string, string> = { 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<Modifier, string> = 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('+'); }}