import type { Chord, KeybindingBlock, ParsedBinding, ParsedKeystroke, } from './types.js' /** * Parse a keystroke string like "ctrl+shift+k" into a ParsedKeystroke. * Supports various modifier aliases (ctrl/control, alt/opt/option/meta, * cmd/command/super/win). */ export function parseKeystroke(input: string): ParsedKeystroke { const parts = input.split('+') const keystroke: ParsedKeystroke = { key: '', ctrl: false, alt: false, shift: false, meta: false, super: false, } for (const part of parts) { const lower = part.toLowerCase() switch (lower) { case 'ctrl': case 'control': keystroke.ctrl = true break case 'alt': case 'opt': case 'option': keystroke.alt = true break case 'shift': keystroke.shift = true break case 'meta': keystroke.meta = true break case 'cmd': case 'command': case 'super': case 'win': keystroke.super = true break case 'esc': keystroke.key = 'escape' break case 'return': keystroke.key = 'enter' break case 'space': keystroke.key = ' ' break case '↑': keystroke.key = 'up' break case '↓': keystroke.key = 'down' break case '←': keystroke.key = 'left' break case '→': keystroke.key = 'right' break default: keystroke.key = lower break } } return keystroke } /** * Parse a chord string like "ctrl+k ctrl+s" into an array of ParsedKeystrokes. */ export function parseChord(input: string): Chord { // A lone space character IS the space key binding, not a separator if (input === ' ') return [parseKeystroke('space')] return input.trim().split(/\s+/).map(parseKeystroke) } /** * Convert a ParsedKeystroke to its canonical string representation for display. */ export function keystrokeToString(ks: ParsedKeystroke): string { const parts: string[] = [] if (ks.ctrl) parts.push('ctrl') if (ks.alt) parts.push('alt') if (ks.shift) parts.push('shift') if (ks.meta) parts.push('meta') if (ks.super) parts.push('cmd') // Use readable names for display const displayKey = keyToDisplayName(ks.key) parts.push(displayKey) return parts.join('+') } /** * Map internal key names to human-readable display names. */ function keyToDisplayName(key: string): string { switch (key) { case 'escape': return 'Esc' case ' ': return 'Space' case 'tab': return 'tab' case 'enter': return 'Enter' case 'backspace': return 'Backspace' case 'delete': return 'Delete' case 'up': return '↑' case 'down': return '↓' case 'left': return '←' case 'right': return '→' case 'pageup': return 'PageUp' case 'pagedown': return 'PageDown' case 'home': return 'Home' case 'end': return 'End' default: return key } } /** * Convert a Chord to its canonical string representation for display. */ export function chordToString(chord: Chord): string { return chord.map(keystrokeToString).join(' ') } /** * Display platform type - a subset of Platform that we care about for display. * WSL and unknown are treated as linux for display purposes. */ type DisplayPlatform = 'macos' | 'windows' | 'linux' | 'wsl' | 'unknown' /** * Convert a ParsedKeystroke to a platform-appropriate display string. * Uses "opt" for alt on macOS, "alt" elsewhere. */ export function keystrokeToDisplayString( ks: ParsedKeystroke, platform: DisplayPlatform = 'linux', ): string { const parts: string[] = [] if (ks.ctrl) parts.push('ctrl') // Alt/meta are equivalent in terminals, show platform-appropriate name if (ks.alt || ks.meta) { // Only macOS uses "opt", all other platforms use "alt" parts.push(platform === 'macos' ? 'opt' : 'alt') } if (ks.shift) parts.push('shift') if (ks.super) { parts.push(platform === 'macos' ? 'cmd' : 'super') } // Use readable names for display const displayKey = keyToDisplayName(ks.key) parts.push(displayKey) return parts.join('+') } /** * Convert a Chord to a platform-appropriate display string. */ export function chordToDisplayString( chord: Chord, platform: DisplayPlatform = 'linux', ): string { return chord.map(ks => keystrokeToDisplayString(ks, platform)).join(' ') } /** * Parse keybinding blocks (from JSON config) into a flat list of ParsedBindings. */ export function parseBindings(blocks: KeybindingBlock[]): ParsedBinding[] { const bindings: ParsedBinding[] = [] for (const block of blocks) { for (const [key, action] of Object.entries(block.bindings)) { bindings.push({ chord: parseChord(key), action, context: block.context, }) } } return bindings }