Skip to content

Fix Dvorak Cmd+C colliding with notifications shortcut#762

Merged
lawrencecchen merged 20 commits intomainfrom
issue-759-dvorak-cmd-c
Mar 6, 2026
Merged

Fix Dvorak Cmd+C colliding with notifications shortcut#762
lawrencecchen merged 20 commits intomainfrom
issue-759-dvorak-cmd-c

Conversation

@lawrencecchen
Copy link
Copy Markdown
Contributor

@lawrencecchen lawrencecchen commented Mar 3, 2026

Summary

  • make app-level command shortcut matching prioritize layout-aware characters so Cmd+C under Dvorak no longer collides with the Cmd+I notifications shortcut
  • keep keycode fallback for control combos and command punctuation shortcuts so non-US layout shortcuts (for example Cmd+Shift+]) still work
  • add regression tests for Dvorak Cmd+C behavior, normal Cmd+I, shifted symbol shortcuts, and non-US bracket fallback behavior

Testing

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -only-testing:cmuxTests/AppDelegateShortcutRoutingTests test (pass)
  • codex --dangerously-bypass-approvals-and-sandbox --model gpt-5.3-codex -c model_reasoning_effort="medium" review --uncommitted (No findings)

Issues

Summary by CodeRabbit

  • Bug Fixes

    • Improved keyboard shortcut handling across non‑US layouts, including punctuation/bracket normalization and more reliable fullscreen (Cmd+Ctrl+F) recognition with ANSI fallback.
  • UX

    • Shortcut hints now respond to modifier keys (Command/Control) and window focus more accurately.
    • New Settings toggle to show/hide shortcut hints; preference persisted and resettable.
  • Refactor

    • Renamed command-key policy/monitors to modifier-based equivalents and updated public signatures.
  • Tests

    • Added extensive tests covering layouts, window contexts, and shortcut routing.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 6, 2026 2:25am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Makes main shortcut handling layout‑aware (new normalization and ANSI fallbacks), renames command‑focused shortcut hint monitors/policies to modifier‑focused variants across UI and titlebar, adds a persisted shortcut‑hint visibility setting, and expands tests for layout‑specific shortcut behavior (including Dvorak).

Changes

Cohort / File(s) Summary
AppDelegate — layout‑aware shortcut matching
Sources/AppDelegate.swift
Replaced direct char/keyCode checks with a layout‑aware matching pipeline: added layoutCharacterProvider parameter, normalization helpers (shouldRequireCharacterMatchForCommandShortcut, shortcutCharacterMatches, normalizedShortcutEventCharacter), bracket/punctuation handling, and ANSI keyCode fallbacks; public API signature updated.
Keyboard / shortcut tests — expanded coverage
cmuxTests/AppDelegateShortcutRoutingTests.swift, cmuxTests/CmuxWebViewKeyEquivalentTests.swift
Large additions: save/restore shortcut test state, temporary-shortcut helper, many new tests for Dvorak/non‑ANSI layouts, control sequences, event→window routing, and updated expectations for renamed policy/monitor APIs.
UI hint monitors / policies renamed
Sources/ContentView.swift, Sources/Update/UpdateTitlebarAccessory.swift
Renamed command‑centric types to modifier‑centric (SidebarCommandHintPolicyShortcutHintModifierPolicy, *CommandKeyMonitor*ShortcutHintModifierMonitor), replaced isCommandPressed with isModifierPressed, updated call sites, lifecycle calls, and timing references.
App settings / persistence
Sources/cmuxApp.swift
Added AppStorage-backed alwaysShowShortcutHints setting wired into SettingsView and reset/default flows via ShortcutHintDebugSettings.resetVisibilityDefaults().
Misc — public API / tests signature updates
Sources/..., cmuxTests/...
Updated public/test signatures for policy methods (e.g., isCurrentWindow and shouldShowHints) to include additional parameters (keyWindowNumber, window context) and renamed constants (intentionalHoldDelay → ShortcutHintModifierPolicy.intentionalHoldDelay).

Sequence Diagram(s)

sequenceDiagram
  participant User as User (Key Event)
  participant App as AppDelegate.matchShortcut
  participant Layout as KeyboardLayout
  participant Action as Feature Handler

  User->>App: key event + modifier flags
  App->>Layout: layoutCharacterProvider(keyCode)
  Layout-->>App: layout-aware character or nil
  App->>App: normalizedShortcutEventCharacter(chars, keyCode)
  alt character-based match required
    App->>Action: dispatch action (character match)
  else ANSI fallback allowed
    App->>Action: dispatch action (ANSI keyCode match)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through keys both near and far,
I taught Dvorak where each shortcut are.
Cmd and Control now learn the right way,
Brackets and slashes no longer stray.
A rabbit pressed shift — and saved the day! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive The PR includes broader refactoring beyond the stated Dvorak fix: renaming SidebarCommandHintPolicy to ShortcutHintModifierPolicy and related monitor/policy refactoring affects UI hint visibility logic that appears tangential to keyboard layout matching. Clarify whether the policy/monitor renaming and modifier-key hint visibility changes are necessary to fix issue #759 or represent separate improvements that should be in a separate PR.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix Dvorak Cmd+C colliding with notifications shortcut' accurately describes the primary change: addressing layout-aware keyboard shortcut matching to resolve the Dvorak Cmd+C collision issue.
Description check ✅ Passed The PR description follows the template structure with Summary, Testing, and Issues sections; it clearly explains what changed, why, and how it was tested, meeting all required criteria.
Linked Issues check ✅ Passed The code changes comprehensively address issue #759 by implementing layout-aware keyboard shortcut matching to fix Dvorak Cmd+C collision, maintaining keycode fallback for control combos/punctuation, and adding regression tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-759-dvorak-cmd-c

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
cmuxTests/AppDelegateShortcutRoutingTests.swift (1)

328-330: Consider extracting temporary-shortcut setup into a test helper.

The same set/restore KeyboardShortcutSettings pattern appears in multiple tests; a small helper (for example, withTemporaryShortcut) would reduce duplication and future maintenance risk.

Also applies to: 371-373, 412-417, 456-461

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/AppDelegateShortcutRoutingTests.swift` around lines 328 - 330,
Extract the repeated save/set/restore pattern for KeyboardShortcutSettings into
a test helper (e.g., withTemporaryShortcut) that takes the action
(KeyboardShortcutSettings.Action), a temporary shortcut (or uses
action.defaultShortcut), and a closure to run; inside the helper capture the
original via KeyboardShortcutSettings.shortcut(for:), set the temporary via
KeyboardShortcutSettings.setShortcut(..., for:), run the closure, and restore
the original in a defer block. Replace the inline patterns (calls to
KeyboardShortcutSettings.shortcut(for:),
KeyboardShortcutSettings.setShortcut(..., for:), and the defer restore) in the
tests that reference Action.showNotifications (and the other occurrences at the
noted ranges) with calls to withTemporaryShortcut(action: .showNotifications) {
... } so tests remain behaviorally identical but share the helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxTests/AppDelegateShortcutRoutingTests.swift`:
- Around line 328-330: Extract the repeated save/set/restore pattern for
KeyboardShortcutSettings into a test helper (e.g., withTemporaryShortcut) that
takes the action (KeyboardShortcutSettings.Action), a temporary shortcut (or
uses action.defaultShortcut), and a closure to run; inside the helper capture
the original via KeyboardShortcutSettings.shortcut(for:), set the temporary via
KeyboardShortcutSettings.setShortcut(..., for:), run the closure, and restore
the original in a defer block. Replace the inline patterns (calls to
KeyboardShortcutSettings.shortcut(for:),
KeyboardShortcutSettings.setShortcut(..., for:), and the defer restore) in the
tests that reference Action.showNotifications (and the other occurrences at the
noted ranges) with calls to withTemporaryShortcut(action: .showNotifications) {
... } so tests remain behaviorally identical but share the helper.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d9587c and 69e47a9.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift

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: 69e47a9a8d

ℹ️ 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".

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 3, 2026

Greptile Summary

Fixed Dvorak keyboard layout issue where Cmd+C (physical I key producing 'c') was incorrectly triggering the Cmd+I notifications shortcut. The solution refactors matchShortcut() to prioritize character-based matching for alphanumeric Command shortcuts, ensuring layout-aware behavior across QWERTY, Dvorak, and other layouts.

Key changes:

  • Command alphanumeric shortcuts (letters/digits) now require character match, preventing physical key collisions across layouts
  • Added normalizedShortcutEventCharacter() to handle shifted symbols (e.g., ? normalizes to / for Cmd+Shift+/ shortcuts)
  • Preserved ANSI keyCode fallback for Control-modified shortcuts and Command punctuation shortcuts to support non-US layouts
  • Added helper methods shouldRequireCharacterMatchForCommandShortcut() and shortcutCharacterMatches() for cleaner logic
  • Four new regression tests validate the fix and ensure existing shortcuts continue to work

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is well-designed with comprehensive test coverage that validates both the fix and existing behavior. The logic correctly prioritizes character matching for alphanumeric shortcuts while preserving keyCode fallback for edge cases, and the refactoring adds helpful abstractions without introducing complexity
  • No files require special attention

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Refactored shortcut matching to prioritize layout-aware character matching for alphanumeric Command shortcuts while preserving keyCode fallback for punctuation and Control shortcuts
cmuxTests/AppDelegateShortcutRoutingTests.swift Added comprehensive test coverage for Dvorak layout handling, shifted symbol normalization, and non-US layout keyCode fallback behavior

Last reviewed commit: 69e47a9

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
cmuxTests/CmuxWebViewKeyEquivalentTests.swift (1)

2577-2579: Tighten the hold-delay assertion to catch overly long regressions.

Line 2578 only enforces a minimum, so an accidentally large delay would still pass.

♻️ Proposed test hardening
 func testShortcutHintUsesIntentionalHoldDelay() {
     XCTAssertGreaterThanOrEqual(ShortcutHintModifierPolicy.intentionalHoldDelay, 0.25)
+    XCTAssertLessThanOrEqual(ShortcutHintModifierPolicy.intentionalHoldDelay, 1.0)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/CmuxWebViewKeyEquivalentTests.swift` around lines 2577 - 2579, The
test testShortcutHintUsesIntentionalHoldDelay currently only asserts a minimum;
tighten it to also assert an upper bound by checking
ShortcutHintModifierPolicy.intentionalHoldDelay equals the expected value (e.g.,
0.25) within a small tolerance (use XCTAssertEqual with an accuracy or assert
both >= 0.25 and <= 0.25 + ε) so overly large delays fail; update the assertion
in testShortcutHintUsesIntentionalHoldDelay accordingly referencing
ShortcutHintModifierPolicy.intentionalHoldDelay.
Sources/AppDelegate.swift (1)

6532-6539: Precompute layout character once per key event to avoid repeated translation calls.

matchShortcut(event:shortcut:) can be called many times per keyDown, and each miss can trigger KeyboardLayout.character(forKeyCode:). Consider passing a per-event precomputed layout character to reduce hot-path overhead.

♻️ Suggested optimization
-    private func matchShortcut(event: NSEvent, shortcut: StoredShortcut) -> Bool {
+    private func matchShortcut(
+        event: NSEvent,
+        shortcut: StoredShortcut,
+        precomputedLayoutCharacter: String? = nil
+    ) -> Bool {
@@
-        if shortcutCharacterMatches(
-            eventCharacter: KeyboardLayout.character(forKeyCode: event.keyCode),
+        if shortcutCharacterMatches(
+            eventCharacter: precomputedLayoutCharacter ?? KeyboardLayout.character(forKeyCode: event.keyCode),
             shortcutKey: shortcutKey,
             applyShiftSymbolNormalization: false
         ) {
             return true
         }
// In handleCustomShortcut(event:)
let eventLayoutCharacter = KeyboardLayout.character(forKeyCode: event.keyCode)
// then reuse via a local helper/wrapper for all shortcut probes
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 6532 - 6539, Precompute the layout
character once per key event and pass it into the shortcut-matching path instead
of calling KeyboardLayout.character(forKeyCode:) repeatedly: in
handleCustomShortcut(event:) call KeyboardLayout.character(forKeyCode:
event.keyCode) once (e.g. eventLayoutCharacter) and update the
matchShortcut(event:shortcut:) calls (or create a small wrapper that accepts
eventLayoutCharacter) so matchShortcut uses that precomputed character when
invoking shortcutCharacterMatches(... eventCharacter: ..., shortcutKey: ...,
applyShiftSymbolNormalization: false).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxTests/CmuxWebViewKeyEquivalentTests.swift`:
- Around line 2577-2579: The test testShortcutHintUsesIntentionalHoldDelay
currently only asserts a minimum; tighten it to also assert an upper bound by
checking ShortcutHintModifierPolicy.intentionalHoldDelay equals the expected
value (e.g., 0.25) within a small tolerance (use XCTAssertEqual with an accuracy
or assert both >= 0.25 and <= 0.25 + ε) so overly large delays fail; update the
assertion in testShortcutHintUsesIntentionalHoldDelay accordingly referencing
ShortcutHintModifierPolicy.intentionalHoldDelay.

In `@Sources/AppDelegate.swift`:
- Around line 6532-6539: Precompute the layout character once per key event and
pass it into the shortcut-matching path instead of calling
KeyboardLayout.character(forKeyCode:) repeatedly: in
handleCustomShortcut(event:) call KeyboardLayout.character(forKeyCode:
event.keyCode) once (e.g. eventLayoutCharacter) and update the
matchShortcut(event:shortcut:) calls (or create a small wrapper that accepts
eventLayoutCharacter) so matchShortcut uses that precomputed character when
invoking shortcutCharacterMatches(... eventCharacter: ..., shortcutKey: ...,
applyShiftSymbolNormalization: false).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69e47a9 and 9985d57.

📒 Files selected for processing (5)
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Update/UpdateTitlebarAccessory.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmuxTests/AppDelegateShortcutRoutingTests.swift

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: 9985d576a1

ℹ️ 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".

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxTests/AppDelegateShortcutRoutingTests.swift`:
- Around line 355-540: Wrap each affected test's setup+ invocation in a
deterministic shortcut context using withTemporaryShortcut(...) so the tests do
not depend on persisted UserDefaults remaps: for
testCmdPhysicalPWithDvorakCharactersDoesNotTriggerCommandPaletteSwitcher(),
testCmdShiftPhysicalPWithDvorakCharactersDoesNotTriggerCommandPalette(),
testCmdOptionPhysicalTWithDvorakCharactersDoesNotTriggerCloseOtherTabsShortcut(),
and testCmdPhysicalWWithDvorakCharactersDoesNotTriggerClosePanelShortcut() call
withTemporaryShortcut for the relevant actions (the command-palette-switcher
action, command-palette action, close-other-tabs action, and close-panel action
respectively) around the notification expectation/ASSERT and the call to
AppDelegate.debugHandleCustomShortcut(event:), so the inverted
NotificationCenter expectations and XCTAssertFalse/XCTAssertEqual checks run
with pinned shortcuts rather than whatever is in UserDefaults.

In `@Sources/AppDelegate.swift`:
- Around line 1017-1024: Store the result of
KeyboardLayout.character(forKeyCode: keyCode) in a local (e.g. let char =
KeyboardLayout.character(forKeyCode: keyCode)), then only run the ANSI fallback
(return keyCode == 3) when that char is empty / translation-unavailable;
otherwise preserve the existing special-case check for "f" and return false for
non-empty non-"f" characters. This ensures KeyboardLayout.character(forKeyCode:)
is consulted once and the ANSI fallback is gated to the “layout translation
unavailable” case.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9985d57 and a47996b.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift

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: 0bb7615c1f

ℹ️ 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".

@lawrencecchen
Copy link
Copy Markdown
Contributor Author

Addressed remaining review nits in 1582c64: tightened testShortcutHintUsesIntentionalHoldDelay to assert the exact configured delay (0.30) with tolerance, so oversized regressions are caught.

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: b58e463502

ℹ️ 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".

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 6 files

@lawrencecchen lawrencecchen deleted the issue-759-dvorak-cmd-c branch March 6, 2026 02:32
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
* Fix layout-safe command shortcut matching

* Fix unshifted symbol shortcut coercion

* Fix layout handling for hardcoded Cmd shortcuts

* Support Cmd/Ctrl modifier hold shortcut hints

* Address PR shortcut review feedback

* Handle Caps Lock in command shortcut matching

* Address remaining PR shortcut review comments

* Handle Cmd+Ctrl+F control-character fallback

* Tighten shortcut hint hold-delay test

* Expose shortcut hint visibility in settings

* Restore Cmd+digit fallback on symbol-first layouts

* Stabilize shortcut regression coverage

* Align shortcut hint toggle with Cmd/Ctrl behavior

* Restore Claude workflow file

* Match Claude workflow file to main

* Keep shortcut hint hold behavior command-only

* Restore command shortcut fallback when layout data is unavailable

* Preserve command-aware shortcut translation
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.

Cmd-C doesn't work in Dvorak

1 participant