Skip to content

feat(gui): Configurable and recordable keyboard shortcuts #630

@amiable-dev

Description

@amiable-dev

Summary

All keyboard shortcuts used throughout the GUI should be user-configurable via the Settings panel, with a "record" interaction — the user clicks a shortcut field then presses their desired key combination to assign it, similar to how IDEs and DAWs handle keybinding configuration.

This builds on #550 (which defines the default shortcuts) by adding a configuration and recording layer.

Proposed Design

Shortcut Settings Section

A new "Keyboard Shortcuts" section in the Settings workspace view, displaying a table of all registered actions and their current bindings:

Keyboard Shortcuts
──────────────────────────────────────────────
Action                  Shortcut        [Reset]
──────────────────────────────────────────────
Mappings view           ⌘1              [⊗]
Devices view            ⌘2              [⊗]
Profiles view           ⌘3              [⊗]
Settings view           ⌘4              [⊗]
Plugins view            ⌘5              [⊗]
Toggle Event Stream     ⌘E              [⊗]
Toggle Chat Panel       ⌘J              [⊗]
Developer Mode          ⌘⇧D             [⊗]
Mappings sub-view       ⌘⇧M             [⊗]
Config History          ⌘⇧H             [⊗]
Raw Config              ⌘⇧R             [⊗]
──────────────────────────────────────────────
                            [Reset All to Defaults]

Recording Interaction

  1. User clicks on a shortcut cell (or a "Record" button next to it)
  2. The cell enters recording mode — shows pulsing "Press keys..." placeholder
  3. User presses their desired key combination (e.g., Ctrl+Alt+M)
  4. The combo is captured, validated, and displayed
  5. If the combo conflicts with another binding, show an inline warning: "Already assigned to: Toggle Chat Panel — Reassign?" with confirm/cancel
  6. Press Escape to cancel recording without changing the binding
  7. The [⊗] button clears the shortcut (disables that action's hotkey)

Recording Implementation

// keyboard-shortcuts.ts

interface ShortcutBinding {
  action: string;          // e.g., 'workspace.mappings'
  label: string;           // e.g., 'Mappings view'
  default: string;         // e.g., 'CmdOrCtrl+1'
  current: string | null;  // user override, null = use default
}

/**
 * Capture a key combo from a keydown event.
 * Returns a normalised string like 'CmdOrCtrl+Shift+D'.
 */
function captureCombo(e: KeyboardEvent): string | null {
  // Ignore bare modifier presses (Shift, Ctrl, Alt, Meta alone)
  if (['Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) return null;

  const parts: string[] = [];
  if (e.metaKey || e.ctrlKey) parts.push('CmdOrCtrl');
  if (e.altKey) parts.push('Alt');
  if (e.shiftKey) parts.push('Shift');
  parts.push(normalizeKey(e.key));  // e.g., 'D', '1', 'F5'
  return parts.join('+');
}

Persistence

Store in localStorage under key conductor:shortcuts as a JSON map of action → combo string. Consistent with other GUI preferences (#559). The shortcut store merges user overrides with defaults at startup.

// shortcut-store.js
import { writable } from 'svelte/store';

const DEFAULTS = {
  'workspace.mappings':    'CmdOrCtrl+1',
  'workspace.devices':     'CmdOrCtrl+2',
  'workspace.profiles':    'CmdOrCtrl+3',
  'workspace.settings':    'CmdOrCtrl+4',
  'workspace.plugins':     'CmdOrCtrl+5',
  'panel.events':          'CmdOrCtrl+E',
  'panel.chat':            'CmdOrCtrl+J',
  'developer.toggle':      'CmdOrCtrl+Shift+D',
  'mappings.current':      'CmdOrCtrl+Shift+M',
  'mappings.history':      'CmdOrCtrl+Shift+H',
  'mappings.rawConfig':    'CmdOrCtrl+Shift+R',
};

// Load user overrides from localStorage, merge with defaults
function createShortcutStore() { ... }

Conflict Detection

  • Internal conflicts: Warn if two actions share the same combo. Allow reassignment with confirmation.
  • Reserved combos: Block assignment of OS-level shortcuts (Cmd+Q, Cmd+W, Cmd+C/V/X/Z/A) and Tauri defaults. Show "Reserved by system" message.
  • Context-aware: Shortcuts are suppressed when focus is in an input/textarea (typing takes priority, per GUI: Add keyboard shortcuts for workspace pane and view navigation #550).

Architecture

┌─────────────────────────────────────────┐
│  shortcut-store.js                      │
│  - defaults map                         │
│  - user overrides (localStorage)        │
│  - merged active bindings               │
│  - conflict detection                   │
└─────────┬───────────────────────────────┘
          │
    ┌─────┴──────┐          ┌──────────────────┐
    │ App.svelte │          │ SettingsPanel     │
    │ keydown    │◄─reads───│ ShortcutEditor    │
    │ handler    │          │ (record + table)  │
    └────────────┘          └──────────────────┘

The global keydown handler in App.svelte (from #550) reads from the shortcut store rather than hardcoding combos. The Settings panel writes to the store. Single source of truth.

Related Issues

Priority

P4 — Enhancement on top of #550. Implement #550 first with hardcoded defaults, then layer configurability on top.

Acceptance Criteria

  • Settings panel has a "Keyboard Shortcuts" section showing all registered actions and their bindings
  • Clicking a shortcut cell enters recording mode — captures the next key combo pressed
  • Escape cancels recording without changing the binding
  • Conflict detection warns when a combo is already assigned to another action
  • Reserved system shortcuts cannot be assigned (Cmd+Q, Cmd+C/V/X/Z, etc.)
  • [⊗] button clears individual shortcuts; "Reset All to Defaults" restores all
  • Custom bindings persist across app restarts (localStorage)
  • Global keydown handler reads from the shortcut store (not hardcoded)
  • Shortcuts suppressed when focus is in input/textarea elements

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions