/ keybindings / reservedShortcuts.ts
reservedShortcuts.ts
  1  import { getPlatform } from '../utils/platform.js'
  2  
  3  /**
  4   * Shortcuts that are typically intercepted by the OS, terminal, or shell
  5   * and will likely never reach the application.
  6   */
  7  export type ReservedShortcut = {
  8    key: string
  9    reason: string
 10    severity: 'error' | 'warning'
 11  }
 12  
 13  /**
 14   * Shortcuts that cannot be rebound - they are hardcoded in Claude Code.
 15   */
 16  export const NON_REBINDABLE: ReservedShortcut[] = [
 17    {
 18      key: 'ctrl+c',
 19      reason: 'Cannot be rebound - used for interrupt/exit (hardcoded)',
 20      severity: 'error',
 21    },
 22    {
 23      key: 'ctrl+d',
 24      reason: 'Cannot be rebound - used for exit (hardcoded)',
 25      severity: 'error',
 26    },
 27    {
 28      key: 'ctrl+m',
 29      reason:
 30        'Cannot be rebound - identical to Enter in terminals (both send CR)',
 31      severity: 'error',
 32    },
 33  ]
 34  
 35  /**
 36   * Terminal control shortcuts that are intercepted by the terminal/OS.
 37   * These will likely never reach the application.
 38   *
 39   * Note: ctrl+s (XOFF) and ctrl+q (XON) are NOT included here because:
 40   * - Most modern terminals disable flow control by default
 41   * - We use ctrl+s for the stash feature
 42   */
 43  export const TERMINAL_RESERVED: ReservedShortcut[] = [
 44    {
 45      key: 'ctrl+z',
 46      reason: 'Unix process suspend (SIGTSTP)',
 47      severity: 'warning',
 48    },
 49    {
 50      key: 'ctrl+\\',
 51      reason: 'Terminal quit signal (SIGQUIT)',
 52      severity: 'error',
 53    },
 54  ]
 55  
 56  /**
 57   * macOS-specific shortcuts that the OS intercepts.
 58   */
 59  export const MACOS_RESERVED: ReservedShortcut[] = [
 60    { key: 'cmd+c', reason: 'macOS system copy', severity: 'error' },
 61    { key: 'cmd+v', reason: 'macOS system paste', severity: 'error' },
 62    { key: 'cmd+x', reason: 'macOS system cut', severity: 'error' },
 63    { key: 'cmd+q', reason: 'macOS quit application', severity: 'error' },
 64    { key: 'cmd+w', reason: 'macOS close window/tab', severity: 'error' },
 65    { key: 'cmd+tab', reason: 'macOS app switcher', severity: 'error' },
 66    { key: 'cmd+space', reason: 'macOS Spotlight', severity: 'error' },
 67  ]
 68  
 69  /**
 70   * Get all reserved shortcuts for the current platform.
 71   * Includes non-rebindable shortcuts and terminal-reserved shortcuts.
 72   */
 73  export function getReservedShortcuts(): ReservedShortcut[] {
 74    const platform = getPlatform()
 75    // Non-rebindable shortcuts first (highest priority)
 76    const reserved = [...NON_REBINDABLE, ...TERMINAL_RESERVED]
 77  
 78    if (platform === 'macos') {
 79      reserved.push(...MACOS_RESERVED)
 80    }
 81  
 82    return reserved
 83  }
 84  
 85  /**
 86   * Normalize a key string for comparison (lowercase, sorted modifiers).
 87   * Chords (space-separated steps like "ctrl+x ctrl+b") are normalized
 88   * per-step — splitting on '+' first would mangle "x ctrl" into a mainKey
 89   * overwritten by the next step, collapsing the chord into its last key.
 90   */
 91  export function normalizeKeyForComparison(key: string): string {
 92    return key.trim().split(/\s+/).map(normalizeStep).join(' ')
 93  }
 94  
 95  function normalizeStep(step: string): string {
 96    const parts = step.split('+')
 97    const modifiers: string[] = []
 98    let mainKey = ''
 99  
100    for (const part of parts) {
101      const lower = part.trim().toLowerCase()
102      if (
103        [
104          'ctrl',
105          'control',
106          'alt',
107          'opt',
108          'option',
109          'meta',
110          'cmd',
111          'command',
112          'shift',
113        ].includes(lower)
114      ) {
115        // Normalize modifier names
116        if (lower === 'control') modifiers.push('ctrl')
117        else if (lower === 'option' || lower === 'opt') modifiers.push('alt')
118        else if (lower === 'command' || lower === 'cmd') modifiers.push('cmd')
119        else modifiers.push(lower)
120      } else {
121        mainKey = lower
122      }
123    }
124  
125    modifiers.sort()
126    return [...modifiers, mainKey].join('+')
127  }