# Keyboard shortcuts

Local and global accelerators, command routing, customizable bindings, and keyboard events.

Source: https://legend.so/spark/keyboard-shortcuts/

Choose a shortcut surface according to its owner:

| Import | Purpose |
| --- | --- |
| `@legendapp/spark/shortcuts` | Shortcuts in the focused application, optionally scoped to a window. |
| `@legendapp/spark/global-shortcuts` | System-wide shortcuts, including when the app is unfocused. |
| `@legendapp/spark/shortcuts/commands` | Named commands, routing, capture controls, and persisted bindings. |
| `@legendapp/spark/shortcuts/keyboard` | Low-level observation of keydown and keyup. |

Local and global shortcuts have macOS and Windows backends. Command routing and low-level keyboard events require macOS. Test key layouts and focus changes on your target host.

## [Accelerator strings](https://legend.so/spark/keyboard-shortcuts/#accelerator-strings)

Menus and registered shortcuts share named accelerators such as `CmdOrCtrl+S`, `Alt+Left`, `Shift+Up`, and `F5`. `CmdOrCtrl` means Command on macOS and Control on Windows. Character shortcuts require a modifier on these surfaces. Use `Plus` for the plus key, for example `CmdOrCtrl+Plus`.

Commands additionally allow unmodified character keys, `Home`, `End`, `PageUp`, `PageDown`, `MediaPlayPause`, `MediaNext`, and `MediaPrevious`, with explicit `Fn` and `CapsLock` modifiers. Other shortcut surfaces reject those extensions. Bindings use named strings; physical codes belong to low-level events.

## [Local and global registrations](https://legend.so/spark/keyboard-shortcuts/#local-and-global-registrations)

```ts
import { registerShortcut } from '@legendapp/spark/shortcuts';
import { registerGlobalShortcut } from '@legendapp/spark/global-shortcuts';

const local = await registerShortcut('CmdOrCtrl+S', () => {
  console.log('Save requested');
}, { windowId: 'main' });

const global = await registerGlobalShortcut('CmdOrCtrl+Shift+K', () => {
  console.log('Global shortcut requested');
});

// When the owner ends:
await local.remove();
await global.remove();
```

Local shortcuts are consumed before insertion into text fields. Options are `windowId` and `repeat`; repeat defaults to `false`. Global options contain only `repeat`. Windows supports global repeat; requesting `repeat: true` on macOS rejects `E_UNSUPPORTED_OPTION`. Conflicting or reserved global shortcuts reject with `E_BUSY`.

`getShortcutAvailability()` and `getGlobalShortcutAvailability()` report host/module availability from their respective imports. Registration failures reject. Await removal and retain a failed handle for cleanup retry; removal stops JavaScript callbacks immediately.

To display a shortcut in an application menu, use the menu item's `shortcut` field; see [menus](https://legend.so/spark/menus/).

## [Named commands and routing](https://legend.so/spark/keyboard-shortcuts/#named-commands-and-routing)

```ts
import { createHotkeyRouter } from '@legendapp/spark/shortcuts/commands';

const definitions = [
  { id: 'save', title: 'Save', defaultBindings: ['CmdOrCtrl+S'] },
] as const;
const router = createHotkeyRouter();
const registration = await router.register({
  definitions,
  scope: { kind: 'window', windowId: 'editor' },
  handlers: { save: () => { console.log('Save requested'); } },
});

await registration.setEnabled(false);
await registration.setEnabled(true);
const suspension = await router.suspend({ kind: 'window', windowId: 'editor' });
await suspension.remove();
await registration.remove();
```

`defaultBindings` and per-command `bindings` overrides are arrays. An empty override disables a command; an omitted override uses its defaults. Registrations snapshot their configuration. Higher priority runs first: defaults are 100 for window scope and 0 for application scope. Equal priorities favor the newest registration.

A synchronous handler returning `false` lets dispatch continue to the next Spark handler. Other synchronous results stop routing. Native consumption is decided before JavaScript runs, so returning `false` cannot send the key back to AppKit. Launch asynchronous work explicitly and handle its errors. Only definitions with `repeat: true` run on repeated keydowns; `allowExtraModifiers` opts into subset modifier matching.

The event's window ID determines ownership. Window changes reset pressed keys; `setActiveWindowId` can reset them on focus changes too. Await `setEnabled(boolean)` before relying on the new state.

`router.suspend(scope?)` returns an async registration; await its `remove()` to resume. Window suspension also blocks application handlers in that window. The final command registration releases shared native observers and consumption rules. Setup, updates, and cleanup reject on failure, and cleanup can be retried on the same handle.

### [React ownership and capture](https://legend.so/spark/keyboard-shortcuts/#react-ownership-and-capture)

`useRoutedHotkeys({ router, definitions, bindings, handlers, ... })` manages registration for a component. It returns loading/ready/error state and keeps handlers current. Use `onError` for setup/update failures and `onCleanupError(error, registration)` to retry failed cleanup.

`useHotkeySuspension({ active, router, scope?, onError?, onCleanupError? })` owns suspension for an effect lifetime. Both hooks share the commands import.

`HotkeyCapture` takes `value`, `onChange`, and optional capture/error callbacks. It owns native capture only while recording, commits a named accelerator on key release, and cancels on Escape. Capture temporarily consumes monitored keys. `HotkeyBindingsSettingsContent` and `HotkeyBindingsSettingsPage` edit arrays of bindings through `definitions`, `values`, and `onChange(id, values)`; `maxBindingsPerCommand` can limit the number per command.

## [Persisting bindings](https://legend.so/spark/keyboard-shortcuts/#persisting-bindings)

`createHotkeyStore({ definitions, path, maxBindingsPerCommand?, storage?, debounceMs? })` comes from the commands import. Await the factory before using its Legend State `value$`, `error$`, `flush()`, and `close()`. The parent directory must exist; follow [observable persistence](https://legend.so/spark/settings/#observable-persistence).

The file format is:

```json
{ "version": 1, "bindings": { "save": ["CmdOrCtrl+S"] } }
```

Serialization preserves portable `CmdOrCtrl`. Missing commands acquire defaults, removed IDs are discarded, and empty arrays stay empty. Invalid versions, scalar bindings, or malformed accelerators reject and preserve existing file bytes. A nonnegative integer `maxBindingsPerCommand` keeps the first N bindings. Conflicts remain visible through `getHotkeyBindingConflicts` and the settings UI rather than silently replacing another command.

Await `flush()` to persist the current snapshot and `close()` when its owner ends. The store persists bindings; supply them to a router registration or `useRoutedHotkeys` to apply them.

## [Low-level keyboard events](https://legend.so/spark/keyboard-shortcuts/#low-level-keyboard-events)

```ts
import { addKeyboardListener } from '@legendapp/spark/shortcuts/keyboard';

const listener = await addKeyboardListener('down', event => {
  console.log(event.key, event.keyCode, event.consumed);
}, { windowIds: ['editor'] });
await listener.remove();
```

Use `'down'` or `'up'`. Events include `windowId` (or `null`), a layout-derived unmodified `key`, physical `keyCode`, a numeric `modifiers` mask, and `repeated`, `consumed`, and `captured` booleans. Window filtering applies to both event kinds. `KeyCodes`, `KeyText`, `hasModifier`, and `createModifierMask` are exported from the same path.

Callbacks observe the native decision; returning a value cannot consume a key. Check `getKeyboardAvailability()` before registering. Await removal when the owner ends; retry the same handle if cleanup fails.

See the [commands source guide](https://github.com/LegendApp/legend-spark/blob/main/docs/commands.md) for routing and persistence details.

[View Markdown](https://legend.so/spark/keyboard-shortcuts.md)[Report a documentation correction](https://github.com/LegendApp/legend-docs/issues/new?title=Docs%3A+Keyboard+shortcuts&body=Page%3A+https%3A%2F%2Flegend.so%2Fspark%2Fkeyboard-shortcuts%0A%0AWhat+is+incorrect+or+missing%3F%0A%0AExpected+behavior+or+supporting+source%3A%0A)
