Skip to content

feat(desktop): add support for mouse side buttons on macOS w/Logi Options+#130

Open
michael131468 wants to merge 1 commit intosipeed:mainfrom
michael131468:macos-mouse-tap
Open

feat(desktop): add support for mouse side buttons on macOS w/Logi Options+#130
michael131468 wants to merge 1 commit intosipeed:mainfrom
michael131468:macos-mouse-tap

Conversation

@michael131468
Copy link
Copy Markdown

The use case for this commit is to enable the MacOS version of the app to collect mouse side button events and forward them on through the usb-kvm.

By default, it's configured to collect mouse side button events that are configured as forward/backward navigation events (configured by Logi Options+). But it can be reconfigured to forward select mouse events by modifying a json configuration file.

This change is quite complex because it was made to handle a case where Logi Options+ intercepts the raw HID side-button presses and re-emits them as macOS NSEventTypeGesture events (CGEventType 29), which Chromium does not surface to the renderer as mouse events. This requires significant workarounds.

It is architected into two parts:

(1) The Hook (desktop/native/):

Electron doesn't seem to expose macOS NSEventTypeOtherMouseDown (the AppKit event type that carries button 3/4 back/forward) to the main process JavaScript layer. And if configured via Logi Options+ to be backward/forward events, Chromium consumes those events and doesn't forward them onto the app. Therefore we need a side process to tap into the events and forward them on to the app.

That is what this hook is.

  • A C++ N-API addon opens a CGEventTap on kCGSessionEventTap from a dedicated thread running a CFRunLoop.
  • The hook itself is generic: it has no hardcoded knowledge of CGEventTypes. startHook(rules, callback) accepts a rules array supplied by the Electron app; each rule specifies a CGEventType, a list of field matchers (field id + any-of values), and which button ('side' or 'extra') to emit on match. The tap callback iterates the rules and, on the first match, emits {kind: 'button', button: 'side' | 'extra'} back to Node via a ThreadSafeFunction.
  • The default rule set (loaded from the JSON config in the Electron app) matches CGEventType 29 with phase=ended (field 132) and reads the direction (field 115) to distinguish side vs. extra, which is what Logi Options+ emits for back/forward. The rule config can be edited without rebuilding the native addon.

Note that the build instructions for MacOS get more complicated as we need to build this hook application too as a dependency for the main app. See desktop/README.md.

(2) The Forwarder (desktop/src/main/):

This modifies the electron app to load the hook as an addon and receive events to forward on as HID side-button press+release events.

  • index.ts loads the addon (different path in dev vs packaged) and translates each side-button event into an HID press+release.
  • The forwarder owns the rule configuration: on startup it reads side-button-rules.json from the user data directory if present, otherwise falls back to the bundled side-button-rules.default.json, and passes the parsed rules array into startHook(). This is what lets users reconfigure which CGEvents map to side/extra without rebuilding the native addon.
  • Side buttons are only declared on the device's absolute HID interface (report prefix 0x02); the relative interface (0x01) declares only 3 buttons, so bits 0x08/0x10 sent there are silently dropped by the Linux host. Button presses are always routed through the absolute interface, regardless of which mode the user is in.
  • Device.sendMouseData now sniffs outgoing absolute reports and remembers the last x/y, exposed via getLastAbsPosition(), so the injected press doesn't teleport the cursor.

…ions+

The use case for this commit is to enable the MacOS version of the app to
collect mouse side button events and forward them on through the usb-kvm.

By default, it's configured to collect mouse side button events that are
configured as forward/backward navigation events (configured by Logi Options+).
But it can be reconfigured to forward select mouse events by modifying a json
configuration file.

This change is quite complex because it was made to handle a case where Logi
Options+ intercepts the raw HID side-button presses and re-emits them as macOS
NSEventTypeGesture events (CGEventType 29), which Chromium does not surface to
the renderer as mouse events. This requires significant workarounds.

It is architected into two parts:

(1) The Hook (desktop/native/):

Electron doesn't seem to expose macOS NSEventTypeOtherMouseDown (the AppKit
event type that carries button 3/4 back/forward) to the main process JavaScript
layer. And if configured via Logi Options+ to be backward/forward events,
Chromium consumes those events and doesn't forward them onto the app. Therefore
we need a side process to tap into the events and forward them on to the app.

That is what this hook is.

- A C++ N-API addon opens a CGEventTap on kCGSessionEventTap from a
  dedicated thread running a CFRunLoop.
- The hook itself is generic: it has no hardcoded knowledge of CGEventTypes.
  startHook(rules, callback) accepts a rules array supplied by the Electron
  app; each rule specifies a CGEventType, a list of field matchers (field id +
  any-of values), and which button ('side' or 'extra') to emit on match. The tap
  callback iterates the rules and, on the first match, emits {kind: 'button',
  button: 'side' | 'extra'} back to Node via a ThreadSafeFunction.
- The default rule set (loaded from the JSON config in the Electron
  app) matches CGEventType 29 with phase=ended (field 132) and reads
  the direction (field 115) to distinguish side vs. extra, which is
  what Logi Options+ emits for back/forward. The rule config can be
  edited without rebuilding the native addon.

Note that the build instructions for MacOS get more complicated as we need
to build this hook application too as a dependency for the main app. See
desktop/README.md.

(2) The Forwarder (desktop/src/main/):

This modifies the electron app to load the hook as an addon and receive events
to forward on as HID side-button press+release events.

- index.ts loads the addon (different path in dev vs packaged) and
  translates each side-button event into an HID press+release.
- The forwarder owns the rule configuration: on startup it reads
  side-button-rules.json from the user data directory if present,
  otherwise falls back to the bundled side-button-rules.default.json,
  and passes the parsed rules array into startHook(). This is what
  lets users reconfigure which CGEvents map to side/extra without
  rebuilding the native addon.
- Side buttons are only declared on the device's absolute HID
  interface (report prefix 0x02); the relative interface (0x01)
  declares only 3 buttons, so bits 0x08/0x10 sent there are silently
  dropped by the Linux host. Button presses are always routed through
  the absolute interface, regardless of which mode the user is in.
- Device.sendMouseData now sniffs outgoing absolute reports and
  remembers the last x/y, exposed via getLastAbsPosition(), so the
  injected press doesn't teleport the cursor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@michael131468
Copy link
Copy Markdown
Author

Hi, I'm not really expecting this to get merged as it's quite the added complexity for a very small use case. But I wanted to share it in a pull request to increase visibility in case there are others out there who want such a thing and need such a workaround.

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