Skip to content

Customizable keyboard shortcuts #121

Description

@samkeen

#118 built the detection half of this and deliberately stopped there. conflicts(), shadows() (ui/src/bindings.ts) and editorOverlaps() (ui/src/editorkeys.ts) are pure functions over the registry, shaped to serve a rebinding UI — today they only run as a CI gate. Nothing lets a user rebind anything.

Scope

  • A chord recorder in Settings → Keyboard, beside the table it would edit.
  • Persistence in localStorage, the theme (main.ts) / pane-sizes (panes.ts) idiom: a UI preference, never vault state, so it doesn't touch the host or the index.
  • Live conflict feedback from what already exists. conflicts() is the refuse tier — two commands answering to one keystroke in one scope, where which runs depends on branch order in the handler. shadows() and editorOverlaps() are advisory: legal, and worth saying out loud (e.g. "CodeMirror binds this while editing").
  • Reset to default, per chord and wholesale.
  • The empirical probe. If the recorder receives no keydown for a chord the user pressed, something upstream took it — macOS dispatches menu key equivalents and system hotkeys before the key window's responder chain, so absence of the event is a real observation, not a lookup. It needs no FFI and stays correct when the user remaps Spotlight or installs Raycast tomorrow. Limits worth stating in the UI copy: it only evaluates the chord actually pressed (no pre-validating a list), and it's sufficient-but-not-necessary — some system hotkeys do pass the keydown through, so it under-reports rather than false-alarms, which is the right failure direction for an advisory.
  • Bring the arrow-navigation families into the registry. treenav.ts (arrowMove), sidenav.ts (sideArrowMove) and settingstabs.ts (tabMove) own their own key → move mapping today, deliberately: one owner per mapping, which is the rule bindings.ts states and the reason those keys are literal rows in the sheet. That's right for a fixed keyboard and wrong the moment a user can rebind — otherwise they're the one part of the keyboard nobody can change and the checker can't see. Moving them in means the registry, not each module, decides which key means "next row".

Design notes carried over

  • Scope is what keeps the checker from crying wolf: ↑/↓ means three different things in B2 and none of them conflict. The lattice (global, editor, overlay:*, textentry:*, …) already models this; new bindings need an honest scope more than they need a new mechanism.
  • Resist growing a when-clause expression language. The table holds {id, keys, scope} and the conditions stay ordinary code — see the Binding doc comment for why that trade holds at this size. User-defined chords change which keystroke, never when a command applies.

Sequencing. #119 (declare the macOS menu) is worth landing first, so the chords the OS menu reserves are enumerable and the recorder can refuse them outright instead of letting a user pick ⌘W and wonder why it closes the window.

Source: docs/design/invariants.md K1; ui/src/bindings.ts and ui/src/editorkeys.ts module headers.

The original motivation for the keyboard-registry work in #118 — the detection landed there, the customization did not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions