---
url: /guide/keybindings.md
---
# Keybindings

`@ufo-soft/caelus/keybindings` dispatches commands from key chords. The app supplies a keymap of
defaults, the user can supply a keymap in TOML, and one `keydown` listener invokes the command the
pressed chord maps to.

```ts
import { OK, Registry, ok } from '@ufo-soft/caelus'
import { keybindings } from '@ufo-soft/caelus/keybindings'

const registry = new Registry()
registry.register([
  { name: 'newTodo', command: { description: 'Open the new todo dialog', parameters: [] }, handler: () => ok() },
  { name: 'search', command: { description: 'Focus the search field', parameters: [] }, handler: () => ok() },
])

const defaults = {
  'cmd+n': 'newTodo',
  'cmd+k': 'search',
}

const kb = keybindings(registry, defaults, () => fetch('/keymap.toml').then((response) => response.text()))
const detach = kb.attach()
```

`keybindings(registry, defaults, source)` returns a handle. `attach()` adds the listener to `window`
and returns a function that removes it. To listen on another element, pass it to `attach`.

When the pressed chord is in the table, the listener calls `preventDefault()` on the event and
invokes the command with `{}` as args. A command bound to a chord gets no args.

## The user keymap

The source is any function that returns the user's keymap text, or a promise of it: a file read, a
fetch, a value from storage.

```toml
[bindings]
"ctrl+alt+n" = "newTodo"
"cmd+k" = "unbind"
"cmd+/" = "search"

```

The `[bindings]` table maps chords to command names. It is a patch over the defaults: a chord in
both takes the user's command, and a chord only in the defaults stays. A chord mapped to `unbind`
is removed. Values that are not strings are ignored, and a document with no `[bindings]` table is
an empty patch.

## Reloading

```ts
const outcome = await kb.reload()
if (outcome.kind === OK) console.log([...outcome.data])
else console.error(`keymap not applied: ${outcome.reason}`)
```

With the keymap above, the resolved table is:

```txt
[ [ 'cmd+n', 'newTodo' ], [ 'ctrl+alt+n', 'newTodo' ], [ 'cmd+/', 'search' ] ]
```

The handle starts on the defaults alone. `reload()` reads the source, parses it, and swaps in the
new table. A source that throws, or text that does not parse, is a failed outcome: the previous
table stays, and the reason says what went wrong. When reloads overlap, the latest one wins.

The table is resolved against the registry when it is built: at creation, and on each reload. A
chord whose command is not registered at that moment is dropped. Register commands before calling
`keybindings`, and reload after registering more.

`kb.table` is the current table, a `Map` of chord to command name. A reload replaces it and never
changes it in place.

## Chords

A chord is zero or more modifiers and one key, joined with `+`. Case, spaces, and modifier order do
not matter: every chord is converted to one canonical form before it is compared.

```ts
import { canonical } from '@ufo-soft/caelus/keybindings'

console.log(canonical('Shift+Cmd+K'))
console.log(canonical('control+option+esc'))
console.log(canonical('meta + up'))
console.log(JSON.stringify(canonical('shift')))

```

```txt
cmd+shift+k
ctrl+alt+escape
cmd+arrowup
""
```

The modifiers are `cmd`, `ctrl`, `alt`, and `shift`, always written in that order in canonical
form. `cmd` is the Meta key: Command on macOS, the Windows key on Windows.

| Alias | Canonical |
|---|---|
| `command`, `meta`, `super` | `cmd` |
| `control` | `ctrl` |
| `option`, `opt` | `alt` |
| `esc` | `escape` |
| `return` | `enter` |
| `del` | `delete` |
| `up`, `down`, `left`, `right` | `arrowup`, `arrowdown`, `arrowleft`, `arrowright` |

The space bar is `space`. Other keys use the name the browser gives them, in lowercase: `f1`, `tab`,
`pageup`.

A chord that does not parse is dropped from the table. That covers an unknown modifier, and a
modifier with no key.

With `alt` held, or `shift` held on a symbol, a keymap names the unshifted key. Write `shift+/`, not
`?`, and `alt+e`, not the accent Option+E types on macOS.

## Not supported

* A platform modifier (`mod` for `cmd` on macOS and `ctrl` elsewhere). Bind both chords.
* Chord sequences, such as `cmd+k cmd+s`.
* Focus context. The listener fires wherever focus is, text fields included.
* Electron menu accelerators.
* Layout-aware `alt` chords. With `alt` held, the key is read by its physical position on a US
  layout, so on other layouts an `alt` chord can name a different character than the key shows.

## Lower-level functions

The handle is built from exported functions an app can use directly: `parseKeymap` reads the TOML,
`canonical` converts a chord string, `chordOf` converts a `KeyboardEvent`, `resolve` merges the two
keymaps into a table, and `attach` adds the listener. See the
[API reference](/api/@ufo-soft/caelus/keybindings/) for their signatures.
