Skip to content

Back and Forward mouse buttons now work in Safari and Chrome#363

Open
b0x42 wants to merge 1 commit into
AprilNEA:masterfrom
b0x42:fix/mx-vertical-back-forward-buttons
Open

Back and Forward mouse buttons now work in Safari and Chrome#363
b0x42 wants to merge 1 commit into
AprilNEA:masterfrom
b0x42:fix/mx-vertical-back-forward-buttons

Conversation

@b0x42

@b0x42 b0x42 commented Jul 7, 2026

Copy link
Copy Markdown

What this fixes

If you have a Logitech MX Vertical (or any Logitech mouse with dedicated Back/Forward buttons), those buttons previously did nothing in Safari and other browsers on macOS — even though Chrome eventually worked. This PR fixes that: pressing the Back button navigates back in both Safari and Chrome, and Forward navigates forward.


Why it didn't work

Logitech mice don't send Back/Forward as ordinary mouse button clicks. They use a proprietary HID++ firmware protocol to report those buttons, and macOS never translates them into the standard mouse events that apps like Safari listen for. OpenLogi's existing input hook only captured standard mouse events, so it never saw Back/Forward presses at all.

On top of that, Safari has a special restriction: even when you synthesize the correct mouse button events or keyboard shortcuts in software, Safari's web engine (WKWebView) runs in a sandboxed subprocess and ignores them entirely. Chrome doesn't have this restriction, which is why Chrome worked once the buttons were being captured — but Safari didn't.

There was also a subtle timing problem: the MX Vertical's buttons are visible at two levels simultaneously — as standard OS mouse button events (intercepted by OpenLogi's CGEventTap hook) and as HID++ firmware events. The hook path was firing first and sending Cmd+[ to Safari (which Safari ignores), while the correct AX path was running too late.


How it's fixed

Capturing the buttons (works for all apps)

The fix adds the Back and Forward button IDs to OpenLogi's HID++ capture session — the same mechanism already used for the DPI button and gesture controls. When you press Back or Forward, the firmware event is now intercepted, identified, and forwarded into OpenLogi's remapping pipeline.

A 350ms debounce prevents the MX Vertical's firmware from registering a single physical click as multiple rapid presses.

Making Safari work

Since Safari ignores synthetic input events, the fix uses macOS's Accessibility API instead. Rather than injecting a fake button press, OpenLogi finds Safari's actual Back/Forward toolbar button and programmatically clicks it — the same way a user would with a mouse cursor. Safari responds to this correctly.

For Chrome and other browsers, the fix continues to use keyboard shortcuts (Cmd+[ / Cmd+]), which those apps handle natively.

Fixing the timing race

The frontmost app's process ID is captured at the exact moment you press the physical button — on the CGEventTap callback thread, where macOS guarantees the app context is current. This ensures OpenLogi always acts on the right browser window, whether the event comes through the hook path or the HID++ path.


Files changed

Crate What changed
openlogi-hid Added Back/Forward CID constants; capture and debounce button presses; embed frontmost app PID in button events
openlogi-inject New ax_navigate_browser() — finds and presses Safari's toolbar button via Accessibility API; falls back to keyboard shortcut for other browsers
openlogi-agent-core Route Back/Forward through AX path first (in both the CGEventTap hook and the HID++ gesture watcher), keyboard shortcut second

Technical details

HID++ CIDs for Back/Forward

The MX Vertical uses CIDs 0x0053 (Back) and 0x0056 (Forward) — below the documented 0xB8 range in Logitech's control-ID list. This PR also adds the higher-range MultiPlatform CIDs (0x00BD, 0x00CE, 0x00DB for Back; 0x00CF for Forward) used on other Logitech models.

Why the Accessibility approach for Safari

Safari's WKWebView runs in a separate sandboxed process. CGEvents posted at the HID or Session tap level don't reach it — this is by design. Tools like Karabiner-Elements and Mac Mouse Fix that support Safari use a DriverKit virtual HID device (kernel-level injection), which requires a special Apple entitlement. The AX approach achieves the same result without requiring a kernel extension.

The AX tree search skips AXSplitGroup, AXTabGroup, AXOpaqueProviderGroup, and AXRadioButton subtrees to avoid exhausting search depth on Safari's tab bar.

The dual-path timing issue

The MX Vertical's buttons are visible at two layers simultaneously: as OtherMouseDown OS events (CGEventTap hook) and as HID++ DivertedButtons CID events (gesture capture session). The hook path was firing first and sending Cmd+[ to Safari (ignored). Now both paths capture NSWorkspace.frontmostApplication.processIdentifier on the tap callback thread and call AXPress on the app's toolbar button directly.

Notes for reviewers
  • CapturedInput::ButtonPressed now carries #[serde(skip)] Option<i32> (frontmost PID). Skipped in serialization — dispatch hint only, not part of the wire format.
  • The 350ms debounce is tuned for the MX Vertical. Other devices may tolerate a tighter window.
  • Long-term, a DriverKit virtual HID device would be more robust. The AX path is pragmatic and avoids requiring Apple entitlements.

Testing

Tested on MX Vertical (046d:b020) over Bluetooth on macOS:

  • Safari ✅ — Back/Forward navigate browser history
  • Chrome ✅ — Back/Forward navigate browser history

@b0x42 b0x42 force-pushed the fix/mx-vertical-back-forward-buttons branch from 463796e to 356ab84 Compare July 7, 2026 12:04
@b0x42 b0x42 changed the title fix(hid): capture and dispatch Back/Forward buttons on MX Vertical Back and Forward mouse buttons now work in Safari and Chrome Jul 7, 2026
@b0x42 b0x42 force-pushed the fix/mx-vertical-back-forward-buttons branch 2 times, most recently from fcdcdf4 to 4c8beed Compare July 7, 2026 12:29
@b0x42 b0x42 marked this pull request as ready for review July 7, 2026 12:34
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds browser Back and Forward support for Logitech mouse buttons. The main changes are:

  • HID++ capture for Back and Forward control IDs.
  • Frontmost app PID capture when button events arrive.
  • AX-based Safari navigation with keyboard fallback for other browsers.
  • Rollback and debounce handling around diverted controls.

Confidence Score: 4/5

This is close, but one cleanup path should be fixed before merging.

  • Most of the AX navigation and localization fixes now follow the intended fallback flow.
  • The reprogrammable-control rollback covers several new failure paths.
  • A later thumbwheel feature-discovery error can still leave already-diverted buttons captured without an active session.

crates/openlogi-hid/src/gesture.rs

Important Files Changed

Filename Overview
crates/openlogi-hid/src/gesture.rs Adds Back and Forward diversion, debounce, PID capture, and rollback state; one later arming error can still skip cleanup.
crates/openlogi-inject/src/inject.rs Adds AX navigation for Safari with identifier, subrole, and positional lookup fallbacks.
crates/openlogi-agent-core/src/watchers/gesture.rs Routes HID++ Back and Forward button presses through AX navigation before falling back to existing dispatch.
crates/openlogi-agent-core/src/hook_runtime.rs Keeps hook-side browser navigation on keyboard shortcut injection.

Comments Outside Diff (1)

  1. crates/openlogi-hid/src/gesture.rs, line 371-375 (link)

    P1 Thumbwheel Failure Leaks Diversions

    This branch can still return after gesture, DPI, Back, and Forward CIDs have already been diverted. If thumbwheel feature discovery fails here, arm_controls exits before it creates ArmedControls, so run_capture_session has nothing to disarm. The already-diverted buttons can stay captured in firmware with no active session left to handle or restore them. This path needs the same accumulated rollback as the earlier reprogrammable-control failure paths, or it needs to skip thumbwheel capture without returning.

    Fix in Codex Fix in Claude Code

Fix All in Codex Fix All in Claude Code

Reviews (5): Last reviewed commit: "fix(hid): capture and dispatch Back/Forw..." | Re-trigger Greptile

Comment thread crates/openlogi-inject/src/inject.rs
Comment thread crates/openlogi-hid/src/gesture.rs
Comment thread crates/openlogi-hid/src/gesture.rs
Comment thread crates/openlogi-hid/src/gesture.rs Outdated
Comment thread crates/openlogi-inject/src/inject.rs Outdated
@b0x42 b0x42 force-pushed the fix/mx-vertical-back-forward-buttons branch from 4c8beed to dc770e7 Compare July 7, 2026 12:54
@b0x42

b0x42 commented Jul 7, 2026

Copy link
Copy Markdown
Author

Thanks for the review! All 5 issues addressed in the latest commit:

  1. Localized AX descriptions — now checks AXIdentifier first (BackForwardToolbarButton_Back/BackForwardToolbarButton_Forward, locale-independent), falls back to English description for older Safari versions.

  2. Partial arm leaves diversion enabled — Back/Forward CID diversion loops now roll back any already-diverted CIDs before propagating an error.

  3. Dual input paths can repeat — documented as a known non-issue: Safari only responds to AXPress (no double-navigation), Chrome only responds to keyboard shortcuts (idempotent). Added a TODO for proper cross-path suppression.

  4. Debounce swallows real clicks — reduced from 350ms to 150ms. This covers the MX Vertical's intra-press bounce window (~50-100ms) while allowing intentional rapid double-clicks (typically ≥200ms apart).

  5. CFArray leakfind_button now CFRetains the found button before returning, then immediately CFReleases each children_val after the loop. The caller receives a +1 retained pointer and releases it after AXPress.

Comment thread crates/openlogi-hid/src/gesture.rs
Comment thread crates/openlogi-agent-core/src/hook_runtime.rs Outdated
Comment thread crates/openlogi-inject/src/inject.rs
@b0x42 b0x42 force-pushed the fix/mx-vertical-back-forward-buttons branch from dc770e7 to fc200f0 Compare July 7, 2026 13:22
@b0x42

b0x42 commented Jul 7, 2026

Copy link
Copy Markdown
Author

Addressed the 3 remaining issues:

  1. Rollback misses DPI — the error path now rolls back gesture, DPI, Back, and Forward CIDs (all previously-diverted controls) before propagating the error.

  2. Duplicate AX navigation — removed the AX call from the hook path entirely. The hook now uses only Cmd+[/Cmd+] (keyboard shortcut for Chrome). Safari is handled exclusively by the HID++ gesture watcher, which calls ax_navigate_browser with the PID captured at press time. The two paths no longer both call AXPress.

  3. Localized fallback still fails — the button search now tries three locale-independent signals in order before falling back to the English description:

    • AXIdentifier (BackForwardToolbarButton_Back/_Forward) — primary, locale-independent
    • AXSubrole (AXBackForwardButtonBack/AXBackForwardButtonForward) — secondary, locale-independent
    • AXDescription English text — last resort for older Safari/macOS versions

Comment thread crates/openlogi-hid/src/gesture.rs
@b0x42 b0x42 force-pushed the fix/mx-vertical-back-forward-buttons branch from fc200f0 to 375c52e Compare July 7, 2026 13:37
@b0x42

b0x42 commented Jul 7, 2026

Copy link
Copy Markdown
Author

Fixed: Later Arm Failure Leaks Diversions

Changed tw.set_reporting(true, false).await? to a match that logs a warning and continues (skipping thumbwheel click capture) rather than propagating the error. This means a thumbwheel setup failure no longer aborts the session, so already-diverted Back/Forward and DPI controls are always returned inside ArmedControls where disarm() can restore them on teardown.

Comment thread crates/openlogi-inject/src/inject.rs Outdated
Comment thread crates/openlogi-hid/src/gesture.rs
Comment thread crates/openlogi-inject/src/inject.rs
The MX Vertical (and similar Logitech mice) report Back and Forward
buttons as HID++ 0x1b04 ReprogControls CIDs rather than standard OS
mouse button 4/5 events. macOS never translates these into
OtherMouseDown CGEvents, so OpenLogi's CGEventTap hook never saw them.

Changes:
- reprog_controls.rs: add BACK_CIDS (0x0053, 0xBD, 0xCE, 0xDB) and
  FORWARD_CIDS (0x0056, 0xCF) covering the MX Vertical's classic CIDs
  and the MultiPlatform Back/Forward CIDs used by other models
- gesture.rs: divert Back/Forward CIDs in arm_controls; emit
  ButtonPressed(Back/Forward) on rising edge with 350ms debounce (the
  MX Vertical sends multiple DivertedButtons frames per physical click);
  capture frontmost app PID on the listener thread at press time so AX
  dispatch uses the correct app regardless of async dispatch delay;
  CapturedInput::ButtonPressed now carries Option<i32> frontmost_pid
- inject.rs: add ax_browser_navigate() — uses AXPress on the toolbar's
  'Go back'/'Go forward' AXButton for Safari (WKWebView ignores
  synthetic CGEvents); skips AXSplitGroup/AXTabGroup/AXRadioButton to
  avoid exhausting search depth on Safari's tab bar; falls back to
  Cmd+[/] for Chrome and other browsers
- watchers/gesture.rs: use captured PID for AX navigation; fall back to
  dispatch_action (Cmd+[/]) when AX finds no matching button

Config: add Back='BrowserBack' and Forward='BrowserForward' under the
correct per-physical-device key in config.toml.

Tested on MX Vertical (046d:b020) over Bluetooth on macOS:
- Chrome: works via Cmd+[ / Cmd+]
- Safari: works via AXPress on toolbar navigation button

Closes: back/forward buttons not working on macOS with MX Vertical
@b0x42 b0x42 force-pushed the fix/mx-vertical-back-forward-buttons branch from 375c52e to ec20ae7 Compare July 7, 2026 14:17
@b0x42

b0x42 commented Jul 7, 2026

Copy link
Copy Markdown
Author

All 3 issues fixed:

1. Recursive Retain Leak — moved CFRetain to the leaf (inside the three button-match checks), removed the map(|el| CFRetain(el)) in each parent frame's propagation path. The button is now retained exactly once regardless of tree depth.

2. Earlier Diversion Leaks — the gesture and DPI CID diversion loops now use explicit error handling with rollback (matching the Back/Forward pattern already in place). A DPI failure rolls back gesture; a gesture failure has nothing prior to roll back.

3. Localized Fallback Fails — added find_nav_button_by_position() as a structural fallback when identifier/subrole/description all fail. It walks AXWindow → AXToolbar → AXGroup (nav group) → AXGroup → AXButton[0 or 1] by role and position, which is locale-independent and stable across Safari versions.

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.

1 participant