Remap wheel tilt and middle click, with dedicated GUI slots - #23
distractedhero wants to merge 4 commits into
Conversation
The event tap returned early for anything that was not a keyboard event, so bindings could never replace middle click or horizontal pan. Learned wheel controls were recorded but never acted on. Match device-bound HID observations to auxiliary mouse and horizontal scroll events by hardware timestamp, deferring 25ms so main-loop HID callbacks can run, and replay anything unmatched. Suppress matched events and dispatch the configured action instead. Direction comes from the signed HID pan value, so it is independent of Natural Scrolling. One physical tilt emits a burst of pan reports, so debounce per direction: reports arriving within 150ms extend the gesture rather than firing again. Verified on a Naga V2 HS where 136 HID reports collapsed to 14 actions across 14 physical tilts. Expose the capability in the mapping window as slots 15, 16 and 17: Wheel Tilt Left, Wheel Tilt Right and Wheel Click. They ship unbound because tilt and click report different HID usages per device, so each user pairs them with Learn Hardware Trigger. Without these cards the feature is reachable only by hand-editing profiles.json. Also releases shortcut modifiers explicitly, accepts "opt" as an Option alias, and ignores continuous trackpad scrolling and primary/secondary buttons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017mrRRGrRu2XN8q5ruiZKkT
A normal launch logged every HID report, every matched device and every mapping lookup. Seven wheel tilts produced 136 lines. Route that tracing through Log.debug, which stays silent unless NAGA_DEBUG is set in the environment. Permission state, event tap state, profile load counts and errors still log unconditionally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017mrRRGrRu2XN8q5ruiZKkT
… document NAGA_DEBUG - Restore AppDelegate's status item title from main: showing the active profile there landed in DParent10#12 and this branch reverted it unintentionally. - deferMappedPointer runs for every tapped event including keystrokes; check the event type before building the bindings dictionary. - README troubleshooting now explains NAGA_DEBUG and lists the log lines the app actually prints today, including the pointer router's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
This is excellent work, @distractedhero — thank you. Matching HID to CGEvent by hardware timestamp and deferring instead of blocking is exactly the right shape, taking direction from the signed HID pan so Natural Scrolling can't flip it is a detail I'd have missed, and the router tests are a real gift. I also want to call out a bonus you may not have noticed: because the router works for any slot, a learned mouse-button trigger now fires on buttons 1–12 too, which closes a limitation I'd documented in the user guide last week. I pushed one small commit on top of yours (maintainer edit) rather than round-tripping:
One thing I'd like you to add to the PR description rather than change: the ButtonMapper changes — private event source for synthesized keys, key-up carrying the physical flags, and I'll do a hardware pass on my V2 HS today (tilt, click, Cmd+Tab, and a plain Cmd+C hold to check the modifier change) and merge once that's clean. Thanks again — this one's been on the wish list since #4. |
With only one tilt direction learned, tilting the other way fell through to the key-press path, whose single-candidate fallback ignores the pan sign and fired the learned slot twice per burst while the native horizontal scroll leaked through (seen on a Naga V2 HS). Pointer inputs — auxiliary mouse buttons and horizontal pan — now stop at the router whether or not they matched a binding. Also drop the stale AC Pan → DPI Up/Down fallback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Problem
Pointer input can't be remapped. The event tap subscribes to mouse and scroll
events, but
eventCallbackreturns early for anything that isn'tkeyDown,keyUporflagsChanged, so a learned wheel binding is recorded and thennever acted on. Binding wheel tilt or middle click has no effect today.
Approach
HID identifies which physical device produced the input; CGEvent is where the
native action can be suppressed. The two are matched by hardware timestamp
within a 50ms tolerance. A matched pointer event is deferred 25ms rather than
blocked, since HID and CGEvent callbacks share the main run loop, and anything
that fails to match is replayed so unrelated input is never swallowed.
Direction comes from the signed HID pan value rather than the CGEvent delta, so
Natural Scrolling doesn't invert it.
A single physical tilt emits a burst of pan reports, so dispatch is debounced
per direction with a 150ms gesture gap.
Surfacing it in the UI
Wheel controls appear in the mapping window as slots 15, 16 and 17, named Wheel
Tilt Left, Wheel Tilt Right and Wheel Click, in their own card row alongside the
existing DPI slots. They ship unbound, since tilt and click report different HID
usages per device, so each user pairs them through the existing Learn Hardware
Trigger flow. Without these cards the capability would only be reachable by
hand-editing profiles.json.
They are independent slots, so binding the wheel no longer consumes a thumb
button mapping.
Verification
Tested on a Naga V2 HS over Bluetooth. Wheel tilt left and right and wheel click
all perform their mapped actions, native behaviour suppressed, no leakage. 136
HID pan reports collapsed to 14 dispatches across 14 physical tilts. Existing
tests pass, plus new coverage for the router, modifier release, and profile
decoding.
Logging
Per-event tracing is gated behind a
NAGA_DEBUGenvironment variable. A normallaunch previously logged every HID report and mapping lookup; that now stays
silent unless the variable is set. Permission state, event tap state, profile
load counts and errors still log unconditionally.
🤖 Generated with Claude Code
https://claude.ai/code/session_017mrRRGrRu2XN8q5ruiZKkT