fix(hook,hid): revive gesture mode on Middle/Back/Forward - #918
fix(hook,hid): revive gesture mode on Middle/Back/Forward#918ngtanthanh-qc wants to merge 2 commits into
Conversation
Arming a capture session only ever writes the CIDs it wants diverted. A diversion left behind by a session that never tore down — the agent was killed, or another Logitech app set it — is therefore never cleared, and because diversion is volatile device state with no owner on the wire, nothing else clears it either. The control then emits no OS event (it is diverted) and no HID++ event (nothing armed it): dead until the device sleeps or reconnects. A Middle/Back/Forward button in gesture mode hits exactly this. Its swipes are detected by the OS hook, so plan_for_device deliberately keeps a gesture-mode button out of divert_buttons — diverting it would starve the hook of the very press it must see. Moving a button that had been diverted for a single action into gesture mode therefore has to clear that diversion, not merely stop renewing it. Until now it did not: the button stayed diverted, the hook never saw it, and gestures silently never fired. Reconcile at arm time instead. Once the spec's controls are diverted, read back every OpenLogi-managed control this device exposes as divertable that the session does not own, and undivert the ones still diverted. Only the managed tables are consulted, so another application's diversion is left exactly as found, and a control the firmware will not divert is skipped.
Greptile SummaryThe PR adds arm-time reconciliation for stale HID++ control diversions and allows unattributed auxiliary mouse buttons through the macOS remapping path.
Confidence Score: 3/5The PR is not yet safe to merge because reconciliation can disrupt another application's active diversion and can leave a stale diversion permanently unrecovered after a transient HID++ failure. The current reconciliation path treats local non-ownership as stale state even though the protocol state does not identify an owner, and it suppresses reconciliation failures while allowing the long-lived capture session to report successful startup. Files Needing Attention: crates/openlogi-device/src/session/gesture.rs
|
| Filename | Overview |
|---|---|
| crates/openlogi-device/src/session/gesture.rs | Adds stale-diversion reconciliation, but active foreign diversions remain indistinguishable from stale ones and reconciliation failures remain unretried. |
| crates/openlogi-device/src/session/gesture/tests.rs | Adds focused classification tests for managed, owned, foreign, and non-divertable controls. |
| crates/openlogi-agent-core/src/hook_runtime.rs | Permits unattributed auxiliary mouse buttons to use remapping while retaining identity checks for attributed events and conservative handling of primary buttons. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Start capture session] --> B[Enumerate divertable controls]
B --> C[Arm controls required by current plan]
C --> D[Build owned CID set]
D --> E[Inspect unowned managed CIDs]
E --> F{CID still diverted?}
F -->|No| G[Leave unchanged]
F -->|Yes| H[Write undiverted reporting state]
G --> I[Run capture session]
H --> I
Reviews (2): Last reviewed commit: "fix(hook): keep unattributed Middle/Back..." | Re-trigger Greptile
| Err(error) => { | ||
| debug!( | ||
| cid, | ||
| ?error, | ||
| "could not read reporting while releasing diversions" | ||
| ); | ||
| } |
There was a problem hiding this comment.
Failed reconciliation is never retried
When a transient HID++ error occurs while reading or clearing a stale diversion, the error is suppressed and the session reports healthy without retrying reconciliation, causing the affected control to remain dead until an unrelated re-arm, reconnect, or device sleep.
macOS usually attributes a CGEvent to an IOKit sender, and the hook leans on that to decide whether a button event may be remapped. When the attribution is missing it fails closed, on the grounds that an unattributed event is more likely a trackpad or a system source than a Logi mouse. That is right for the primary buttons and wrong for the auxiliary ones. Some mice deliver Middle/Back/Forward through a HID collection whose CGEvent carries no IOHIDEvent at all — CGEventCopyIOHIDEvent returns null, so there is no sender to resolve and `device` arrives as `None`. Observed on an MX Anywhere 3S over a Bolt receiver, where `LeftClick` resolves to `vendor_id: Some(0x046d)` while every `MiddleClick` on the same mouse arrives unattributed. Failing closed there loses every binding on those buttons, silently: the events still reach the tap, `handle_button` still runs, and it returns PassThrough before reading the binding. Gesture mode is the worst of it — the hook never claims the press, so hold-and-swipe never fires and the button looks dead even though the config, the plan and the hook maps are all correct. Nothing the closed rule protects can be the source: no trackpad emits button 2/3/4, and `handle_button` has already narrowed to the OS-hook buttons before this check runs. Give those three the benefit of the doubt when attribution is absent, exactly as Linux and Windows already do, and keep the primary buttons failing closed.
Summary
Middle-button gesture mode is dead on my MX Anywhere 3S: no direction fires, the plain click stops working too, and it survives restarts and reboots. Chasing it turned up two independent bugs, both on the path between the button and the hook. Either one alone is enough to kill gestures on Middle/Back/Forward; this branch fixes both, one commit each.
With both applied, hold-and-swipe works on this hardware — confirmed end to end, see Testing.
1. A stale
0x1b04diversion is never released (#917)arm_controls_intoonly writes the CIDs the currentCaptureSpecwants diverted. A diversion left behind by a session that never tore down (agent killed, crash, force quit) is therefore never cleared, and since diversion is volatile device state with no owner on the wire, nothing else clears it. The control then emits no OS event (it is diverted) and no HID++ event (nothing armed it).A gesture-mode Middle/Back/Forward button lands exactly there.
plan_for_devicedeliberately keeps it out ofdivert_buttons— "diverting it would starve the hook of events" — which correctly stops renewing the diversion but never clears the one already set. So "bind Middle Click to a single action, then later switch it to gesture mode" leaves the button diverted forever.The code already knows this state exists;
arm_reprog_controllogscontrol was already diverted before arming. It just only looks at CIDs it is arming, so a control the current config wants native is never read and never handed back.2. Unattributed auxiliary buttons fail closed (#920)
handle_buttongives up before it reads the binding whenbutton_source_may_remapsays no, and that fails closed when macOS attribution is missing. On this mouse the middle button's CGEvent carries no IOHIDEvent at all —CGEventCopyIOHIDEventreturns null — sodevicearrives asNone, every time.Nothing the closed rule protects can be the source there: no trackpad emits button 2/3/4, and
handle_buttonhas already narrowed to the OS-hook buttons before the check runs. So it buys nothing on those three while costing every binding on them, silently.Changes
crates/openlogi-device—session/gesture.rsrelease_unowned_diversionsreads back every OpenLogi-managed control the device exposes as divertable that this session does not own, and undiverts the ones still diverted. Ownership comes from what actually armed (gesture_cids+dpi_cids+button_cids), so the guard against undoing a diversion set moments earlier is exact.unowned_divertable_cids/managed_cidssplit the ownership rule out as pure functions, unit-testable without a device on the far end of a channel. OnlyGESTURE_SOURCE_BUTTONS,DPI_MODE_SHIFT_CIDSandDIVERTABLE_STANDARD_BUTTONSare consulted — another application's diversion is left exactly as found, and a control the firmware will not divert is skipped.undivert_change, so the semantics match the teardown path: cleardiverted/raw_xy, re-assertremap, leave every other bit untouched.getCidReportingper unowned managed control the device exposes — a handful, once per arm — and only writes when one is actually diverted.crates/openlogi-agent-core—hook_runtime.rsbutton_source_may_remaptakes theButtonIdand, when attribution is absent on macOS, keeps Middle/Back/Forward remappable while the primary buttons still fail closed. Linux/Windows are unchanged (they already treat unknown sources as remappable).Testing
Commands run, from the final tree:
Affected-package tier.
cargo tree --workspace --target all --invertgivesopenlogi-device → openlogi-hid → {openlogi-agent, openlogi-agent-core, openlogi-cli → openlogi}andopenlogi-agent-core → openlogi-agent. No GUI crate depends on either change.Not run: the full-workspace tier and
cargo xtask ci. This machine has Command Line Tools only, not full Xcode, so the GPUI crates do not build here. No cross-lint either —aarch64-apple-darwinis the only installed target, soclippy-windowsand the Linux jobs are not run, not green. The diff touches no wire type, no locale, and no#[cfg(target_os = …)]block; the one platform-conditional expression is acfg!()inbutton_source_may_remap, whose Linux/Windows arm is unchanged, and the one platform-gated test is#[cfg(target_os = "macos")]while the other two hold on every target.Hardware verification — MX Anywhere 3S over a Logi Bolt receiver, macOS 26.5 Apple Silicon:
Reproduced on 0.7.10 and
master@ 013ab99: middle button completely inert in gesture mode.Instrumented
mastershowed the config, plan and hook maps all correct (gkeys=[MiddleClick]) while the event never reached the binding.Bug 1 confirmed: a tail-append
CGEventTapof my own saw nothing from button 2 while seeing everyLeftClick. With the first commit the agent logs, on first arm:cid=82is0x0052, Middle Click — and button-2 events start reaching macOS again (470 of them in the next test).Bug 2 then surfaced underneath, with probes on
handle_button:110
MiddleClickevents, every one unattributed.With both commits: hold-and-swipe fires the bound direction, and a tap without a swipe still sends a plain middle click. Verified against this layout:
All five behave as configured, including the Actions Ring opening on a rightward swipe.
Worth a maintainer's pass on a device with a dedicated gesture button or an MX Master 4 haptic panel, to confirm nothing that should stay diverted gets released by the first commit.
Fixes #917
Fixes #920