Skip to content

Customizable keyboard shortcuts#37

Merged
vaayne merged 29 commits intomainfrom
feat-customizable-keyboards
Apr 2, 2026
Merged

Customizable keyboard shortcuts#37
vaayne merged 29 commits intomainfrom
feat-customizable-keyboards

Conversation

@vaayne
Copy link
Copy Markdown
Owner

@vaayne vaayne commented Apr 1, 2026

Summary

  • All ~39 Mori-owned keyboard shortcuts are now customizable via Settings > Keyboard, with 11 system shortcuts (Edit menu, Quit, Hide, etc.) displayed but locked
  • Shortcut recorder captures key combinations inline; conflict detection blocks locked collisions and warns on configurable ones with "Assign Anyway" option
  • Bindings persist as sparse JSON (~/Library/Application Support/Mori/keybindings.json) — only overrides are stored, defaults are never written
  • New MoriKeybindings package (macOS-only) houses KeyBindingStore (@mainactor @observable) and AppKit bridging (matchesEvent, menu helpers)
  • AppDelegate menu items and key monitor now driven by the store; changes take effect immediately without restart
  • 1,100 test assertions across 5 packages (678 core + 64 persistence + 48 keybindings + 249 tmux + 61 IPC)
  • 73 new localization strings (en + zh-Hans)

Fix: #32

Test plan

  • Launch app → all default shortcuts work unchanged
  • Settings > Keyboard → Mori shortcuts shown grouped by category with inline recorders
  • Record a new shortcut → menu item updates immediately, shortcut works
  • Assign a shortcut that conflicts with a configurable action → warning, "Assign Anyway" displaces the old binding (shows "—")
  • Assign a shortcut that conflicts with a locked action (e.g., ⌘C) → error, recording rejected
  • Clear a shortcut via × button → menu shows no key equivalent, action still available via menu click
  • Reset single binding → reverts to default
  • Reset All → reverts all to defaults
  • Quit and relaunch → overrides persisted
  • Locked shortcuts (⌘C, ⌘Q, etc.) → recorder disabled, not editable

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fdae7eb50c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +112 to +115
private func startRecording() {
isRecording = true
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
handleKeyEvent(event)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent duplicate recorder monitors from being installed

startRecording() always adds a new local keyDown monitor and overwrites eventMonitor without removing any existing monitor first. If the user clicks the shortcut field twice before pressing a key, the first monitor is leaked; stopRecording() removes only the most recent one, leaving a stale monitor that keeps consuming key events and can trigger unexpected shortcut recordings/actions afterward.

Useful? React with 👍 / 👎.

Comment on lines +26 to +27
self.overrides = storage.loadOverrides()
self.bindings = Self.merge(defaults: defaults, overrides: storage.loadOverrides())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate loaded overrides against locked shortcuts

The initializer merges persisted overrides directly into active bindings without any conflict/lock validation. If keybindings.json contains a stale or hand-edited override that reuses a locked shortcut (for example ⌘C or ⌘Q), that invalid binding becomes active at startup and the app-level key monitor can intercept the shortcut before responder-chain/system behavior, effectively breaking reserved commands until the file is cleaned up.

Useful? React with 👍 / 👎.

vaayne added 25 commits April 2, 2026 16:59
…onflictResult types and KeyBindingStorageProtocol
Implements KeyBindingStorageProtocol with thread-safe NSLock-based
JSON file storage. Only user-overridden bindings are persisted.
Gracefully handles missing and corrupt files.
22 new assertions covering round-trip, sparse storage, missing file,
corrupt file, and overwrite scenarios.
- Fix window.toggleSidebar default from ⌘0 to ⌘B to match AppDelegate and docs
- Add testKeyBindingDefaultsNoShortcutConflicts to catch duplicate shortcuts
…ridging

Create KeyBindingStore (@mainactor @observable) that merges defaults with
user overrides, supports conflict validation, displacement on update, and
single/bulk reset. Add KeyBinding+AppKit extension for NSEvent matching
and NSMenuItem key equivalents.
Test KeyBindingStore lifecycle (init, update, validate, displacement, reset),
conflict detection (locked vs configurable), sparse persistence, and
KeyModifiers<->NSEvent.ModifierFlags round-trips.
Add MoriKeybindings as dependency of Mori executable target and add
test:keybindings task to mise test suite.
…hortcuts

Replace hardcoded keyboard shortcuts in setupMainMenu() and setupCommandPalette()
with KeyBindingStore-driven lookups. Menu items and the key monitor now read
shortcuts from the store, enabling runtime customization. Locked system shortcuts
(Edit menu, Quit, Hide, Minimize, Fullscreen) remain hardcoded. The
rebuildMenuKeyBindings() callback updates menu items when bindings change.
SwiftUI view that displays current shortcut (formatted with modifier
symbols) and captures new key combinations via NSEvent local monitor.
Supports locked state, clear/unassign, and Escape to cancel.
Pure data + callbacks view that groups bindings by category, shows
per-row reset buttons for overridden bindings, handles locked conflicts
(error, rejected) and configurable conflicts (warning + Assign Anyway).
Replace static moriKeybinds list with editable KeyBindingsSettingsView.
Thread key binding callbacks through GhosttySettingsView and
SettingsWindowContent to KeyBindingStore in AppDelegate.
Add en + zh-Hans translations for: shortcut recorder UI strings,
category names, conflict messages, and all 50 keybinding display names.
…UI in sync

The @State keyBindings in SettingsWindowContent was a snapshot that never
refreshed after store mutations, causing the UI to show stale data and making
customized shortcuts appear to reset immediately.
Use opacity(0) instead of conditional placeholder so the Reset button
always occupies the same space, preventing layout shift when a shortcut
is customized.
…load

- Guard against double-click in ShortcutRecorderView creating leaked monitors
- Validate persisted overrides at startup: strip entries that conflict with
  locked shortcuts or override locked bindings, preventing hand-edited JSON
  from breaking system shortcuts like ⌘C or ⌘Q
- Cache defaultsById in KeyBindingStore to avoid rebuilding on every persist()
- Convert KeyBindingDefaults.byId from computed var to stored let
- Extract managerAction helper to deduplicate 16 repetitive closures in keyMonitorActionMap
@vaayne vaayne force-pushed the feat-customizable-keyboards branch from 892e1a3 to 01aee45 Compare April 2, 2026 09:14
vaayne added 3 commits April 2, 2026 17:16
The AppDelegate key monitor was intercepting events (e.g. CMD+T) before
the ShortcutRecorderView could capture them for rebinding. Added a
@mainactor global flag isRecordingShortcut that the AppDelegate checks
to pass events through during shortcut recording.
Add other.projectSwitcher binding (CMD+P) to default keybindings,
action map, and localization strings (en + zh-Hans).
… category

Group CMD+Shift+P (command palette) and CMD+P (project switcher) together
under the Other category. Remove now-empty commandPalette category from
settings view ordering.
@vaayne vaayne merged commit 5964c73 into main Apr 2, 2026
5 checks passed
@vaayne vaayne deleted the feat-customizable-keyboards branch April 2, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support Customizable Keyboard Shortcuts

1 participant