Skip to content

Fix zoom shortcuts on macOS by routing through renderer keymap#75

Merged
fgilio merged 4 commits intomainfrom
claude/fix-zoom-out-shortcut-tV3Qu
Apr 29, 2026
Merged

Fix zoom shortcuts on macOS by routing through renderer keymap#75
fgilio merged 4 commits intomainfrom
claude/fix-zoom-out-shortcut-tV3Qu

Conversation

@fgilio
Copy link
Copy Markdown
Owner

@fgilio fgilio commented Apr 29, 2026

Summary

Replaces the broken Electron 38 role-based zoom accelerators with a custom action and renderer-side keymap bindings. On macOS, Electron 38's zoomIn/zoomOut/resetZoom roles register menu accelerators but fail to fire the webContentsMethod when the keystroke is pressed (only mouse clicks work). This change bypasses the role system entirely by routing zoom adjustments through a Livewire component that owns the persisted zoom state.

Key Changes

  • New ZoomWindowAction: Single source of truth for zoom adjustments with caching, clamping (0.5–3.0), and floating-point rounding to prevent drift
  • Renderer keymap bindings (zoom-shortcuts.js): Registers ⌘=, ⌘-, and ⌘0 to dispatch rfa-zoom events to the keepalive Livewire component
  • Menu item refactor: Replaced ZoomRoleMenuItem with plain labeled menu items (no role, no accelerator) so keystrokes fall through to the renderer keymap instead of being consumed by Electron's role system
  • Keepalive component: Added #[On('rfa-zoom')] listener to handle both keyboard and menu-click zoom requests
  • Window initialization: Restores cached zoom factor on app startup
  • Comprehensive test coverage: Unit tests for the action (clamping, caching, floating-point handling) and integration tests for the JavaScript keymap bindings

Implementation Details

  • Zoom factor is cached for 30 days and restored on app launch
  • Redundant Electron calls are skipped when already at MIN/MAX bounds
  • Floating-point arithmetic is rounded to 2 decimals to maintain a clean cache grid
  • The keymap is re-registered on every livewire:navigated event (cleared by keymap-store on navigation)
  • Both keyboard shortcuts and View menu clicks route through the same action for consistency

https://claude.ai/code/session_01CBuZ56PLvqwDjPDKK9CGYu

Summary by CodeRabbit

Release Notes

  • New Features

    • Added keyboard shortcuts for zoom in, zoom out, and reset on window
    • Window zoom level now automatically persists across sessions with 30-day retention
  • Improvements

    • Refined zoom menu controls with improved shortcut management
    • Enhanced zoom application to prevent unnecessary window updates

claude added 2 commits April 29, 2026 11:24
⌘- silently failed because Electron 38 registers the zoomOut role's
keyEquivalent on macOS but never fires the role's webContentsMethod from
the keystroke (electron/electron#19559, #15496). The previous fix tried
overriding the accelerator on a normal-typed item, but Electron still
treats the item as role-based once `role` is set, so the override doesn't
help — the keystroke is consumed and dropped.

Drop both `role` and `accelerator` from the View-menu zoom items so the
keystrokes flow through to the renderer, where a new public/js handler
dispatches `rfa-zoom` to a Livewire component (keepalive). The component
delegates to ZoomWindowAction, which clamps, persists the factor, and
calls Window::zoomFactor — restoring the cached factor on next launch.
Mouse clicks on the menu items reach the same handler via the existing
native MenuItemClicked bridge.
Spamming ⌘- at MIN (or ⌘+ at MAX, or ⌘0 at 1.0) wrote the same factor
to cache and posted to Electron's HTTP server every keystroke. Read the
current factor once, return early when the clamped target equals it.

Drop a few redundancies the cleanup turned up:
  - In-renderer keepalive doesn't need to re-validate the direction; the
    action already throws on unknown values.
  - createWindow now reads the cached factor through ZoomWindowAction
    instead of touching the cache key directly.
  - Pest test names dropped the handle("in") syntax in favour of
    behaviour-first phrasing.
  - tests/Js/zoom-shortcuts removed unused vitest imports.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The PR refactors zoom handling from role-based menu items to an event-driven architecture with cached zoom state. Global shortcuts are registered/unregistered based on window focus, triggering events that invoke a unified ZoomWindowAction for zoom management.

Changes

Cohort / File(s) Summary
Core Zoom Action
app/Actions/ZoomWindowAction.php, tests/Unit/Actions/ZoomWindowActionTest.php
Introduces cacheable zoom management with step-based adjustments (in/out/reset), MIN/MAX clamping, and direct window application via Window::get('main')->zoomFactor(). Tests validate clamping, rounding, invalid input rejection, and no-op behavior when already at target zoom.
Zoom Events & Handlers
app/Events/ZoomShortcutPressed.php, app/Listeners/HandleZoomShortcutPressed.php
New event class mapping keyboard shortcuts to zoom directions with direction() translation. Listener extracts direction and delegates to ZoomWindowAction::handle().
Global Shortcut Lifecycle
app/Listeners/RegisterZoomGlobalShortcuts.php, app/Listeners/UnregisterZoomGlobalShortcuts.php, tests/Unit/ZoomGlobalShortcutTest.php
Registers zoom shortcuts on main window focus and unregisters on blur. Test suite verifies focus-scoped registration, event-to-cache flow, and idle shortcut rejection.
Service Provider & Menu Integration
app/Providers/NativeAppServiceProvider.php, resources/views/livewire/keepalive.blade.php
Centralizes event wiring via registerNativeEventListeners() method; menu items changed from role-based to ID-based (zoom-in, zoom-out, reset-zoom); Livewire component maps menu IDs to directions and invokes ZoomWindowAction.
Removed
app/Support/ZoomRoleMenuItem.php, tests/Unit/Support/ZoomRoleMenuItemTest.php
Deletes custom zoom menu implementation that merged role metadata with accelerators; replaced by simpler ID-based menu entries delegated to action handlers.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant GlobalShortcut as GlobalShortcut Facade
    participant Event as ZoomShortcutPressed Event
    participant Listener as HandleZoomShortcutPressed
    participant Action as ZoomWindowAction
    participant Cache as Cache
    participant Window as Window (Electron)

    User->>GlobalShortcut: Press Cmd+Plus (zoom-in)
    GlobalShortcut->>Event: Emit ZoomShortcutPressed
    Event->>Listener: handle(event)
    Listener->>Listener: event.direction() → "in"
    Listener->>Action: handle("in")
    Action->>Cache: Get current zoom factor
    Action->>Action: Compute next = current + STEP<br/>Clamp to MIN/MAX
    alt Clamped ≠ Current
        Action->>Cache: Set new factor (30-day TTL)
        Action->>Window: zoomFactor(clamped)
    else Clamped = Current
        Action->>Action: No-op, return current
    end
    Action-->>Listener: Return updated zoom
    Listener-->>User: Zoom applied
Loading
sequenceDiagram
    participant OS as Operating System
    participant Window as Main Window
    participant EventBus as Event Bus
    participant Register as RegisterZoomGlobalShortcuts
    participant Unregister as UnregisterZoomGlobalShortcuts
    participant GlobalShortcut as GlobalShortcut Facade

    Window->>EventBus: WindowFocused('main')
    EventBus->>Register: handle(event)
    Register->>Register: Check window id == 'main'
    Register->>GlobalShortcut: Register all zoom keys<br/>(Cmd+Plus, Cmd+Minus, Cmd+0)
    GlobalShortcut->>OS: Listen for shortcuts

    OS->>GlobalShortcut: Shortcut pressed
    GlobalShortcut->>EventBus: Emit ZoomShortcutPressed

    Window->>EventBus: WindowBlurred('main')
    EventBus->>Unregister: handle(event)
    Unregister->>Unregister: Check window id == 'main'
    Unregister->>GlobalShortcut: Unregister all zoom keys
    GlobalShortcut->>OS: Stop listening for shortcuts
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Zoom shortcuts now dance with events so keen,
Focus and blur paint the scene,
Cache remembers each magnified view,
No role-based menu, just actions so true!
Action and listener, a duo pristine.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: routing zoom shortcuts through the renderer keymap instead of Electron roles to fix macOS compatibility.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-zoom-out-shortcut-tV3Qu

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

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

ℹ️ 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 thread public/js/zoom-shortcuts.js Outdated
Comment on lines +41 to +43
keymap.register('⌘=', dispatch(root, 'in'));
keymap.register('⌘-', dispatch(root, 'out'));
keymap.register('⌘0', dispatch(root, 'reset'));
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 Register zoom shortcuts in editable fields

These registrations omit allowInEditable, so keymap-store keeps its default (false) and skips the handlers when focus is inside <input>, <textarea>, or contenteditable elements. Since NativeAppServiceProvider::createMenu now removes role/accelerator from the zoom menu items, there is no menu-level keyboard fallback, so pressing zoom shortcuts while typing will not route through ZoomWindowAction (and the persisted zoom path is bypassed).

Useful? React with 👍 / 👎.

Comment thread public/js/zoom-shortcuts.js Outdated
const keymap = root.Alpine?.store?.('keymap');
if (!keymap) return false;

keymap.register('⌘=', dispatch(root, 'in'));
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 Bind zoom-in to '+' instead of only '='

The zoom-in shortcut is registered as ⌘=, but the standard zoom-in combo is ⌘+. With this keymap matcher, ⌘+ is a shifted keypress (shiftKey=true, key='+') and will not match the unshifted ⌘= binding, so users who press the expected ⌘+ combination will not trigger zoom-in.

Useful? React with 👍 / 👎.

claude and others added 2 commits April 29, 2026 12:07
Two gaps from the renderer-keymap zoom path:

  - Without `allowInEditable`, the keymap-store skips the handler when
    focus is in an <input>/<textarea>/contenteditable. There is no
    menu-level fallback now that the View-menu items don't carry an
    accelerator, so pressing ⌘- while typing in a comment was a dead
    keystroke.
  - The matcher checks shift state strictly. `⌘+` on US layouts is
    Cmd+Shift+= (`shiftKey=true, key='+'`), so `⌘=` alone never caught
    the canonical "Cmd plus" combo. Bind both `⌘=` and `⌘⇧+` to zoom-in,
    matching Chrome/Safari.
Co-authored-by: Codex CLI <fgilio+codex-cli@publica.la>
@fgilio fgilio merged commit 7ebc7db into main Apr 29, 2026
14 of 15 checks passed
@fgilio fgilio deleted the claude/fix-zoom-out-shortcut-tV3Qu branch April 29, 2026 13:45
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.

2 participants