/ keybindings / parser.ts
parser.ts
  1  import type {
  2    Chord,
  3    KeybindingBlock,
  4    ParsedBinding,
  5    ParsedKeystroke,
  6  } from './types.js'
  7  
  8  /**
  9   * Parse a keystroke string like "ctrl+shift+k" into a ParsedKeystroke.
 10   * Supports various modifier aliases (ctrl/control, alt/opt/option/meta,
 11   * cmd/command/super/win).
 12   */
 13  export function parseKeystroke(input: string): ParsedKeystroke {
 14    const parts = input.split('+')
 15    const keystroke: ParsedKeystroke = {
 16      key: '',
 17      ctrl: false,
 18      alt: false,
 19      shift: false,
 20      meta: false,
 21      super: false,
 22    }
 23    for (const part of parts) {
 24      const lower = part.toLowerCase()
 25      switch (lower) {
 26        case 'ctrl':
 27        case 'control':
 28          keystroke.ctrl = true
 29          break
 30        case 'alt':
 31        case 'opt':
 32        case 'option':
 33          keystroke.alt = true
 34          break
 35        case 'shift':
 36          keystroke.shift = true
 37          break
 38        case 'meta':
 39          keystroke.meta = true
 40          break
 41        case 'cmd':
 42        case 'command':
 43        case 'super':
 44        case 'win':
 45          keystroke.super = true
 46          break
 47        case 'esc':
 48          keystroke.key = 'escape'
 49          break
 50        case 'return':
 51          keystroke.key = 'enter'
 52          break
 53        case 'space':
 54          keystroke.key = ' '
 55          break
 56        case '↑':
 57          keystroke.key = 'up'
 58          break
 59        case '↓':
 60          keystroke.key = 'down'
 61          break
 62        case '←':
 63          keystroke.key = 'left'
 64          break
 65        case '→':
 66          keystroke.key = 'right'
 67          break
 68        default:
 69          keystroke.key = lower
 70          break
 71      }
 72    }
 73  
 74    return keystroke
 75  }
 76  
 77  /**
 78   * Parse a chord string like "ctrl+k ctrl+s" into an array of ParsedKeystrokes.
 79   */
 80  export function parseChord(input: string): Chord {
 81    // A lone space character IS the space key binding, not a separator
 82    if (input === ' ') return [parseKeystroke('space')]
 83    return input.trim().split(/\s+/).map(parseKeystroke)
 84  }
 85  
 86  /**
 87   * Convert a ParsedKeystroke to its canonical string representation for display.
 88   */
 89  export function keystrokeToString(ks: ParsedKeystroke): string {
 90    const parts: string[] = []
 91    if (ks.ctrl) parts.push('ctrl')
 92    if (ks.alt) parts.push('alt')
 93    if (ks.shift) parts.push('shift')
 94    if (ks.meta) parts.push('meta')
 95    if (ks.super) parts.push('cmd')
 96    // Use readable names for display
 97    const displayKey = keyToDisplayName(ks.key)
 98    parts.push(displayKey)
 99    return parts.join('+')
100  }
101  
102  /**
103   * Map internal key names to human-readable display names.
104   */
105  function keyToDisplayName(key: string): string {
106    switch (key) {
107      case 'escape':
108        return 'Esc'
109      case ' ':
110        return 'Space'
111      case 'tab':
112        return 'tab'
113      case 'enter':
114        return 'Enter'
115      case 'backspace':
116        return 'Backspace'
117      case 'delete':
118        return 'Delete'
119      case 'up':
120        return '↑'
121      case 'down':
122        return '↓'
123      case 'left':
124        return '←'
125      case 'right':
126        return '→'
127      case 'pageup':
128        return 'PageUp'
129      case 'pagedown':
130        return 'PageDown'
131      case 'home':
132        return 'Home'
133      case 'end':
134        return 'End'
135      default:
136        return key
137    }
138  }
139  
140  /**
141   * Convert a Chord to its canonical string representation for display.
142   */
143  export function chordToString(chord: Chord): string {
144    return chord.map(keystrokeToString).join(' ')
145  }
146  
147  /**
148   * Display platform type - a subset of Platform that we care about for display.
149   * WSL and unknown are treated as linux for display purposes.
150   */
151  type DisplayPlatform = 'macos' | 'windows' | 'linux' | 'wsl' | 'unknown'
152  
153  /**
154   * Convert a ParsedKeystroke to a platform-appropriate display string.
155   * Uses "opt" for alt on macOS, "alt" elsewhere.
156   */
157  export function keystrokeToDisplayString(
158    ks: ParsedKeystroke,
159    platform: DisplayPlatform = 'linux',
160  ): string {
161    const parts: string[] = []
162    if (ks.ctrl) parts.push('ctrl')
163    // Alt/meta are equivalent in terminals, show platform-appropriate name
164    if (ks.alt || ks.meta) {
165      // Only macOS uses "opt", all other platforms use "alt"
166      parts.push(platform === 'macos' ? 'opt' : 'alt')
167    }
168    if (ks.shift) parts.push('shift')
169    if (ks.super) {
170      parts.push(platform === 'macos' ? 'cmd' : 'super')
171    }
172    // Use readable names for display
173    const displayKey = keyToDisplayName(ks.key)
174    parts.push(displayKey)
175    return parts.join('+')
176  }
177  
178  /**
179   * Convert a Chord to a platform-appropriate display string.
180   */
181  export function chordToDisplayString(
182    chord: Chord,
183    platform: DisplayPlatform = 'linux',
184  ): string {
185    return chord.map(ks => keystrokeToDisplayString(ks, platform)).join(' ')
186  }
187  
188  /**
189   * Parse keybinding blocks (from JSON config) into a flat list of ParsedBindings.
190   */
191  export function parseBindings(blocks: KeybindingBlock[]): ParsedBinding[] {
192    const bindings: ParsedBinding[] = []
193    for (const block of blocks) {
194      for (const [key, action] of Object.entries(block.bindings)) {
195        bindings.push({
196          chord: parseChord(key),
197          action,
198          context: block.context,
199        })
200      }
201    }
202    return bindings
203  }