Skip to content

Fix terminal zoom on non-US ISO keyboard layouts - #4302

Open
OneMuppet wants to merge 1 commit into
manaflow-ai:mainfrom
OneMuppet:fix-zoom-non-us-keyboards
Open

OneMuppet wants to merge 1 commit into
manaflow-ai:mainfrom
OneMuppet:fix-zoom-non-us-keyboards

Conversation

@OneMuppet

@OneMuppet OneMuppet commented May 18, 2026 •

Copy link
Copy Markdown

Summary

On Swedish (and other non-US ISO) layouts, pressing ⌘+ in a focused terminal pane fires zoom-out instead of zoom-in. The + key on Swedish sits at the physical position of US - (keyCode 27), so US-keycode-based matchers route the keystroke to the wrong action.

This PR fixes it at three layers:

  1. AppDelegate.swift — main fix. When a zoom shortcut is detected and the terminal owns first responder, cmux was forwarding the raw NSEvent to Ghostty. Ghostty's internal keybind matcher then resolved the event by US-layout keycode, mis-firing decrease_font_size on Swedish ⌘+. Instead, dispatch the action by name via ghostty_surface_binding_action using the result of the already-layout-aware browserZoomShortcutAction helper.

  2. KeyboardShortcutSettings.swift — matches. The ANSI-keycode fallback fired unconditionally for symbol command shortcuts, masking the typed ASCII character even when it clearly didn't match the bound shortcut key. Letter shortcuts already required a character match in that case; symbol shortcuts now do too. Non-Latin layouts still fall back to US keycodes (hasEventChars && !eventCharsAreASCII) so ⌘W remains reachable on Cyrillic/etc.

  3. KeyboardShortcutSettings.swift — normalizedShortcutEventCharacter. "+" → "=" and "_" → "-" were gated on applyShiftSymbolNormalization. On Swedish (and similar ISO layouts) + is typed without shift, so the gate left the typed "+" un-mapped and Cmd++ could not match the zoom-in binding stored as "=". These two mappings are now unconditional; nothing binds to literal +/_ so there is no conflict.

Test plan

  • New KeyboardShortcutLayoutTests (9 tests, all passing):
    • Swedish ⌘+ matches .browserZoomIn and does not match .browserZoomOut
    • Swedish ⌘_ maps to .browserZoomOut
    • US ⌘=, ⌘⇧= (typed as +), ⌘− baselines preserved
    • Russian ⌘W still resolves via US-keycode fallback (non-ASCII typed char)
    • Anti-regression: an unrelated ASCII char at the same physical position does not match through the keycode fallback
  • Manual test on a Swedish ISO keyboard:
    • ⌘+ in a terminal pane zooms in
    • ⌘− zooms out
    • ⌘0 resets

Fixes #3362
Refs #1456

🤖 Generated with Claude Code


Summary by cubic

Fixes terminal zoom shortcuts on non‑US ISO layouts (e.g., Swedish). Cmd + now zooms in (not out) when a terminal pane is focused.

  • Bug Fixes
    • Route zoom shortcuts to Ghostty by action name using the layout‑aware browserZoomShortcutAction, instead of forwarding raw NSEvent.
    • Require a character match for Command symbol shortcuts before falling back to US ANSI keycodes; non‑Latin layouts still use the fallback.
    • Normalize "+"→"=" and "_"→"-" unconditionally so stored zoom bindings match on layouts where "+" is unshifted.
    • Added KeyboardShortcutLayoutTests covering Swedish ⌘+, US baselines, and Russian ⌘W.

Written for commit 0804909. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved keyboard shortcut detection across international keyboard layouts (Swedish, Russian, etc.)
    • Fixed font zoom keyboard shortcuts in the terminal to function correctly across different layouts
    • Enhanced keyboard event matching to ensure shortcuts work reliably regardless of physical keyboard configuration

Review Change Stack

On Swedish (and other non-US ISO) layouts the "+" key sits at the US "-"
physical position (keyCode 27). Pressing Cmd++ in a focused terminal pane
fired the zoom-out action because:

1. Ghostty's internal keybind matcher uses US-layout keycodes, so when
   cmux forwarded the raw NSEvent it resolved Cmd++ to Cmd+- (decrease
   font size). Fixed by dispatching the action via
   `ghostty_surface_binding_action` based on the result of the existing
   layout-aware `browserZoomShortcutAction` helper rather than handing
   the raw event to Ghostty.
2. The Swift-side `KeyboardShortcut.matches` ANSI-keycode fallback fired
   unconditionally for symbol command shortcuts, masking a typed ASCII
   character that simply did not match. Now the same character-match
   gate already used for letter shortcuts also applies to symbols.
3. `normalizedShortcutEventCharacter` only mapped "+" to "=" (and "_" to
   "-") when shift was held. On layouts that produce "+" without shift
   the existing zoom-in binding ("=") never matched. Those two
   mappings are now unconditional; no shortcut binds to literal "+" or
   "_" so there is no conflict.

Adds KeyboardShortcutLayoutTests covering Swedish ⌘+, Russian ⌘W
(non-Latin layout keycode fallback), and US baselines.

Fixes manaflow-ai#3362
Refs manaflow-ai#1456
@vercel

vercel Bot commented May 18, 2026

Copy link
Copy Markdown

@OneMuppet is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 18, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 27ede51c-53cf-4cd4-bfa5-568ec67ba838

📥 Commits

Reviewing files that changed from the base of the PR and between 11218fe and 0804909.

📒 Files selected for processing (4)
  • Sources/AppDelegate.swift
  • Sources/KeyboardShortcutSettings.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/KeyboardShortcutLayoutTests.swift

📝 Walkthrough

Walkthrough

This PR fixes keyboard shortcut matching for alternative keyboard layouts (addressing issue #3362) by refining character normalization and fallback logic, updating Ghostty font-zoom dispatch to use binding actions, and adding comprehensive layout-specific tests covering Swedish, Russian, and US layouts.

Changes

Keyboard Layout Awareness for Shortcuts

Layer / File(s) Summary
Keyboard shortcut matching improvements across layouts
Sources/KeyboardShortcutSettings.swift
ShortcutStroke.matches(...) tightens ANSI keycode fallback conditions based on layout safety (non-ASCII characters or empty layout mapping) and removes prior shouldRequireCharacterMatchForCommandShortcut check. normalizedShortcutEventCharacter(...) unconditionally maps +→= and _→- before shift-symbol normalization to ensure consistent cross-layout matching.
Ghostty font-zoom binding action dispatch
Sources/AppDelegate.swift
NSWindow.cmux_performKeyEquivalent replaces raw key forwarding with explicit zoom-action computation and binding action dispatch (increase_font_size:1, decrease_font_size:1, reset_font_size).
Layout-specific test suite and project registration
cmuxTests/KeyboardShortcutLayoutTests.swift, cmux.xcodeproj/project.pbxproj
New test suite with Swedish ISO and Russian PC layout simulators validates Swedish ⌘"+" zoom-in regression (issue #3362), US-layout command-symbol baselines (⌘"=" and ⌘"-"), non-Latin keycode fallback for Cmd+W and zoom-out, and anti-regression for unrelated ASCII characters; test file registered in Xcode build system.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#4186: Modifies NSWindow.cmux_performKeyEquivalent keyboard routing logic; previous work on browser editing shortcut suppression.

Poem

🐰 Keys dance across layouts near and far,
Swedish plus signs find their keyboard star,
From Cyrillic depths to ANSI's home,
Each layout's path now safely known—
Shortcuts bloom where layouts roam! 🌍

🚥 Pre-merge checks | ✅ 15 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: resolving terminal zoom shortcuts on non-US ISO keyboard layouts.
Description check ✅ Passed The PR description includes all required template sections: Summary (what/why), Testing (verification approach), manual testing confirmation, checklist items marked, and references linked issues.
Linked Issues check ✅ Passed Changes fully address issue #3362: AppDelegate and KeyboardShortcutSettings fixes enable layout-aware hotkey matching, preventing incorrect bindings on non-US layouts.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the keyboard layout hotkey issue: three targeted fixes plus new layout-aware tests, with no extraneous modifications.
Cmux Swift Actor Isolation ✅ Passed No actor isolation violations detected. AppDelegate is properly @MainActor. KeyboardShortcutSettings uses pure value types. Helper functions are pure. No shared mutable state or background access.
Cmux Swift Blocking Runtime ✅ Passed No blocking synchronization in production code. Zoom fix uses layout-aware matching without semaphores, waits, or locks. Test asyncAfter calls properly gated by #if DEBUG or CMUX_UI_TEST_MODE.
Cmux No Hacky Sleeps ✅ Passed Check scope excludes Swift. All PR changes are in Swift files or Xcode project config, not in TypeScript, JavaScript, shell, or non-Swift runtime scripts covered by this rule.
Cmux Swift Concurrency ✅ Passed All changes use only synchronous code. Keyboard zoom fix uses synchronous function calls. Tests are XCTest-based (allowed). No async, Task, DispatchQueue, or Combine patterns introduced.
Cmux Swift @Concurrent ✅ Passed Swift changes are all synchronous pure functions with no async/await, @concurrent annotations, or actor isolation violations. Keyboard event processors called from synchronous handlers.
Cmux Swift File And Package Boundaries ✅ Passed AppDelegate reduced 135 lines. KeyboardShortcutSettings +10 lines for focused layout-aware keyboard matching fix. Changes are appropriate AppKit glue and small bug fixes within allowed cases.
Cmux Swift Logging ✅ Passed No logging violations. Only cmuxDebugLog() added, guarded by #if DEBUG in AppDelegate. Complies with rules.
Cmux User-Facing Error Privacy ✅ Passed PR contains no user-facing error messages, alerts, or recovery copy modifications. Changes are internal logic (keyboard shortcut matching), internal binding action dispatching, and test code only.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state patterns introduced. Changes are AppKit keyboard handling and shortcut matching logic only, with unit tests. Check not applicable.
Cmux Architecture Rethink ✅ Passed Small correctness fixes with clear owners and invariants. No timing repairs, locks, observers, singleton state, or split lifecycle. Comprehensive tests added. No architectural rule violations.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR introduces no new NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup. Changes are keyboard logic and test-only fixtures, which are allowed cases per the rules.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented May 18, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes terminal zoom misfiring on non-US ISO keyboard layouts (e.g. Swedish, where + sits at the physical US - position, keyCode 27) by coordinating three changes: dispatching zoom actions by name via performBindingAction instead of forwarding the raw NSEvent to Ghostty, making the "+" → "=" and "_" → "-" character normalizations unconditional in normalizedShortcutEventCharacter, and tightening the ANSI-keycode fallback so it no longer fires for Command-symbol shortcuts when the typed character is ASCII.

  • AppDelegate.swift: Replaces ghosttyView.keyDown(with: event) with ghosttyView.performBindingAction(bindingActionName), resolving the action via the already layout-aware browserZoomShortcutAction helper rather than letting Ghostty's US-layout keybind matcher re-resolve the raw keycode.
  • KeyboardShortcutSettings.swift: Moves "+" → "=" and "_" → "-" out of the shift-gated normalization block, and removes the unconditional ANSI-keycode fallback for Command-symbol shortcuts that had allowed Swedish + (keyCode 27) to falsely match the - binding.
  • cmuxTests/KeyboardShortcutLayoutTests.swift: Adds nine regression tests covering Swedish zoom in/out, US baselines, non-Latin (Russian) keycode fallback, and an anti-regression case.

Confidence Score: 4/5

Safe to merge for the documented Swedish/ISO layout fix; two minor edge cases around numpad+ shortcut storage and residual US-keycode OR-branches in browserZoomShortcutAction are worth addressing but do not affect the corrected zoom paths.

The three-layer fix correctly handles Swedish cmd+/cmd-, preserves non-Latin (Russian) cmd+W fallback, and keeps US baselines intact. The one concrete gap is that numpad+ (keyCode 69) is absent from the storedKey switch, so a user who records a binding from numpad+ stores literal '+' as the key, and the new unconditional '+' to '=' normalization causes that binding to silently miss.

Sources/KeyboardShortcutSettings.swift — storedKey switch and normalizedShortcutEventCharacter; Sources/AppDelegate.swift — browserZoomShortcutAction keyCode OR-branch documentation.

Important Files Changed

Filename Overview
Sources/KeyboardShortcutSettings.swift Two-part fix: unconditional "+" → "=" / "_" → "-" normalization and ANSI-keycode fallback restriction for ASCII command-symbol shortcuts; correct for documented layouts, minor concern around numpad+ stored bindings.
Sources/AppDelegate.swift Routes terminal zoom via performBindingAction instead of raw keyDown forwarding; clear improvement for layout correctness, consistent with existing performBindingAction usage.
cmux.xcodeproj/project.pbxproj Routine test file registration; no logic changes.
cmuxTests/KeyboardShortcutLayoutTests.swift Nine regression tests covering the Swedish fix, US baselines, non-Latin keycode fallback, and anti-regression case.

Comments Outside Diff (1)

  1. Sources/KeyboardShortcutSettings.swift, line 1597-1598 (link)

    P2 The claim "nothing binds to literal +/_" relies on storedKey normalising every source of those characters — but numpad+ (keyCode 69, kVK_ANSI_KeypadPlus) is absent from the storedKey hardcoded switch, so pressing numpad+ during shortcut recording falls through to charactersIgnoringModifiers, which produces "+", and the binding is stored with key = "+". After this change, normalizedShortcutEventCharacter("+") unconditionally returns "=", so "=" != "+" and the match silently fails. The browserZoomShortcutAction path handles keyCode 69 explicitly so the AppDelegate terminal-zoom dispatch is unaffected, but any other ShortcutStroke.matches call for a numpad+-recorded binding would stop firing. Adding keyCode 69 to storedKey (returning "=") closes the gap.

Reviews (1): Last reviewed commit: "Fix terminal zoom on non-US ISO keyboard..." | Re-trigger Greptile

Comment thread Sources/AppDelegate.swift
Comment on lines +14970 to 14987
if let zoomAction = browserZoomShortcutAction(
flags: event.modifierFlags,
chars: event.charactersIgnoringModifiers ?? "",
keyCode: event.keyCode,
literalChars: event.characters
) {
ghosttyView.keyDown(with: event)
let bindingActionName: String
switch zoomAction {
case .zoomIn: bindingActionName = "increase_font_size:1"
case .zoomOut: bindingActionName = "decrease_font_size:1"
case .reset: bindingActionName = "reset_font_size"
}
_ = ghosttyView.performBindingAction(bindingActionName)
#if DEBUG
cmuxDebugLog("zoom.shortcut stage=window.ghosttyKeyDownDirect event=\(Self.keyDescription(event)) handled=1")
cmuxDebugLog("zoom.shortcut stage=window.ghosttyBindingActionDirect action=\(bindingActionName) event=\(Self.keyDescription(event)) handled=1")
#endif
return true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The AppDelegate comment calls browserZoomShortcutAction "layout-aware," but the function still has || keyCode == 24 and || keyCode == 27 as unconditional OR conditions alongside character-based detection (in ShortcutRoutingSupport.swift). For a layout whose key at keyCode 27 produces an ASCII character that is neither "-" nor "_", keyCode == 27 still returns .zoomOut and performBindingAction("decrease_font_size:1") is called. The effective behavior matches the old keyDown forwarding path (Ghostty also used US keycodes), so this is not a regression — but documenting the known limitation directly on browserZoomShortcutAction would clarify the invariant for future maintainers.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Thank you for this, and for the thorough layout tests!

Part of it is already on main: + and _ now normalize to = and - regardless of Shift (normalizedShortcutEventCharacter in KeyboardShortcutSettings.swift), which covers the shortcut-matching half for Swedish and other layouts where + is an unshifted key.

The rest no longer applies cleanly (the AppDelegate routing has moved), and one piece worries me: dropping the keycode fallback for all non-letter Command shortcuts. That fallback is what keeps shortcuts like Cmd+[ reachable on layouts where that physical key types a different ASCII character (French AZERTY, for example), so removing it everywhere could break those users.

If Cmd+"+" still zooms out in the terminal on a Swedish layout with a current nightly, a rebased PR with just the terminal part (dispatching increase_font_size / decrease_font_size by name once browserZoomShortcutAction has matched) would be much easier to land. Thanks again :)

@teamleaderleo teamleaderleo added S3: minor Wrong behavior with a workaround area: input Keyboard, shortcuts, IME, mouse, clipboard and paste labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: input Keyboard, shortcuts, IME, mouse, clipboard and paste S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Alternative keyboard layouts not considered for hotkeys

2 participants