# Keybindings

> The desktop app's keyboard actions, their default bindings and the user's overrides, a global dispatcher, and keycaps that draw a binding.

`zidane-ui/keybindings` holds the desktop app's keyboard actions: the catalog (every action with its default binding, label and description), a dispatcher that fires your handlers on the document, and `KeyHint` / `FooterHint` to draw a binding as keycaps. A `KeybindingsHost` keeps the user's overrides. The previews below run on a fixture host that rebinds two actions.

## Mount it

```tsx
import { createBrowserKeybindingsHost } from 'zidane-ui/adapters'
import { KeybindingsProvider, useKeybindings } from 'zidane-ui/keybindings'

const keybindings = createBrowserKeybindingsHost()

function Shortcuts() {
  useKeybindings({ toggleSidebar: () => toggle(), openPalette: () => openPalette() })
  return null
}

export function Root() {
  return (
    <KeybindingsProvider host={keybindings}>
      <Shortcuts />
      <App />
    </KeybindingsProvider>
  )
}
```

Without a provider the defaults apply. `AppHostProvider` (`zidane-ui/app`) puts `host.keybindings` in a `KeybindingsProvider` for you.

## Register commands

An app's shortcuts come from many parts: the session on show, the side panel, the zoom. Each part registers its handlers with the key command registry, and one dispatcher fires them. `AppHostProvider` mounts a `KeyCommandsProvider`; mount one yourself without it. The desktop app and the web app register the same commands this way, so their shortcuts match.

```tsx
import { useMemo } from 'react'
import { useAppHost, useConfirm } from 'zidane-ui/app'
import type { ChatSession } from 'zidane-ui/chat/headless'
import { useKeyCommands } from 'zidane-ui/keybindings'
import { usePaletteActions } from 'zidane-ui/palette'
import { createSessionActions, sessionKeyCommands } from 'zidane-ui/session/headless'
import { useWorkspaceKeyCommands } from 'zidane-ui/workspace'

function SessionKeys({ session }: { session: ChatSession | null }) {
  const host = useAppHost()
  const confirm = useConfirm()
  const palette = usePaletteActions()
  const actions = useMemo(() => createSessionActions({ session: host.session, sessions: host.chat?.sessions, confirm }), [host, confirm])
  const panel = useWorkspaceKeyCommands({ scope: !!session, session: !!session })
  useKeyCommands({ ...sessionKeyCommands(actions, session), ...panel, openPalette: palette.toggle })
  return null
}
```

- Registering re-renders nothing. Handlers are read at press time; the dispatcher re-attaches only when the set of bound actions or the user's overrides change.
- Parts merge in the order they mounted. A later part's handler for an action wins.
- `useKeyCommandActions()` re-renders only when the set changes. `useActionsPaletteSource()` (`zidane-ui/palette`) lists the registered actions in the palette with their chords.
- `createKeyCommandRegistry()` is the React-free registry, for code outside React.
- `useHelpKey(open, blocked)` opens your shortcut reference on a bare `?`, never while the user types or `blocked()` says a modal owns input.

| Commands | From |
| - | - |
| `sessionKeyCommands(actions, session)` | `zidane-ui/session/headless`: generate a title, compact, export, delete (asked first), favorite, copy the id |
| `useWorkspaceKeyCommands(options)` | `zidane-ui/workspace`: the panel and Todos toggles, tab cycling, ⌥⌘←/→ where the focus is, close the tab in reach |
| `zoomKeyCommands(prefs)` | `zidane-ui/appearance/headless`: zoom in, out, reset |
| `useSelectTurnKeys(options)` | `zidane-ui/chat/transcript`: `enterSelectTurnMode`, then the mode's keys and pointer |
| `useTranscriptFindKey(options)` | `zidane-ui/chat/transcript`: ⌘F to Files or Diffs in reach, else the conversation |

## How the dispatcher matches

- A binding matches exactly: `'o'` does not fire `ctrl+o`, and `ctrl+o` does not fire on `ctrl+shift+o`.
- A spec may list alternatives: `ctrl+w | cmd+w`. The grammar is zidane's, so a `keybindings.json` reads the same in the terminal.
- Bare keys are ignored while the user types in a field or the code editor. `keyScopeOf(event)` says who owns a key press: `editor`, `input`, `menu` or `app`. An editor host opts in with `data-zg-keyscope="editor"`.
- In the editor, `ctrl` plus a letter belongs to the editor (`bindingAllowedIn`), except closing the tab.
- The tab keys (`tabNext`, `tabPrev`) match before the focused widget sees them.
- Handlers are read at press time, so a fresh object each render costs nothing.

`matchesBinding(event, spec)` is the matcher, for a listener of your own. Punctuation matches by its physical key where a layout types a stand-in.

## Pick a host

| Host | From | Keeps |
| - | - | - |
| `createBrowserKeybindingsHost()` | `zidane-ui/adapters` | `localStorage` (`zg:keybindings:v1`). zidane-server keeps no keybindings. |
| `createFixtureKeybindingsHost(options)` | `zidane-ui/keybindings/testing` | Memory, for demos and tests |
| Your own | `zidane-ui/keybindings/headless` | Anything. `createKeybindingOverrides(read)` keeps the known actions of what `read` answers. |

| Member | Gives |
| - | - |
| `overrides`, `refresh()` | The user's specs per action. The first `useKeybindings` reads them once; `refresh` re-reads them and every dispatcher rebinds. |
| `save(map)` | Replace the overrides |
| `openInEditor()`, `location()` | Open the file they live in, and say where it is (the desktop app's `~/.zidane/keybindings.json`) |

`getMergedKeybindings(host)` and `useMergedKeybindings()` give the bindings in effect.

## Parts

| Part | Does |
| - | - |
| `KeyHint` | A binding as keycaps (`size="sm"` in dense rows). Alternatives are separated by the `keyHintOr` label. |
| `FooterHint` | A legend entry: the keys, then what they do |
| `KEYBINDING_DEFS`, `DEFAULT_KEYBINDINGS` | The catalog and the default map |

## Test your host

```ts
import { describeKeybindingsHostContract } from 'zidane-ui/keybindings/testing/contract'

describeKeybindingsHostContract('my host', backend => makeHarness(backend), ['save'])
```

| Entry point | Holds |
| - | - |
| `zidane-ui/keybindings` | `KeybindingsProvider`, `useKeybindings`, `useMergedKeybindings`, `KeyCommandsProvider`, `useKeyCommands`, `useKeyCommandRegistry`, `useKeyCommandActions`, `useHelpKey`, `bindKeyListeners`, `matchesBinding`, `KeyHint`, `FooterHint` |
| `zidane-ui/keybindings/headless` | The catalog, `KeybindingsHost`, the overrides model, `createKeyCommandRegistry`, `keyScopeOf`, the labels |
| `zidane-ui/keybindings/testing` | `createFixtureKeybindingsHost` |
| `zidane-ui/keybindings/testing/contract` | `describeKeybindingsHostContract` |
