[Bug]: gesture mode kills Middle Click — a stale 0x1b04 diversion is never released
- Dominant language
- Rust
- Stars
- 21k
- Forks
- 675
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 172
Description
### Pre-flight
- Searched existing issues. Closest are #560 and #465, but both describe a button that is still *alive* (a tap still sends a plain middle click). Here the button goes completely dead — no OS event and no HID++ event — which is a different failure.
- Reproduced on 0.7.10 (latest release) and on `master` @ 013ab99.
- Logi Options+ is not installed on this machine.
### Which part of OpenLogi?
Both — the fault is in the agent's capture session, so the GUI shows the binding as configured.
### OpenLogi version
0.7.10, and `master` @ 013ab99.
### Operating system
macOS
### OS version & architecture
macOS 26.5 (build 25F71), Apple Silicon
### Device model
MX Anywhere 3S
### How is the device connected?
Logi Bolt receiver (`btle = true`), config key `receiver::slot:1`.
### Affected area(s)
`area: hidpp` (0x1b04 diversion lifecycle), `area: hook` (the OS-hook gesture path is the victim).
### What happened?
Putting **Middle Click into gesture mode kills the button**: no direction ever fires, and the plain click stops working too. The button stays dead across agent restarts, app restarts and reboots — it only comes back if something re-diverts it and then restores it cleanly.
Root cause: a leftover `0x1b04` diversion is never released.
`arm_controls_into` only ever writes the CIDs the current `CaptureSpec` wants diverted. A diversion left behind by a session that never tore down — the agent was killed, a crash, a force quit — is therefore never cleared, and since 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).
A gesture-mode Middle/Back/Forward button lands in exactly that hole. `plan_for_device` deliberately keeps it out of `divert_buttons`:
```rust
// A gesture-mode OS-hook button must stay native: the hook needs to see
// its press to run hold+swipe detection, and diverting it would starve the
// hook of events.
```
That correctly stops *renewing* the diversion, but nothing *clears* the one already set. So the sequence "bind Middle Click to a single action → later switch it to gesture mode" leaves the button diverted forever.
The code already knows this state exists — `arm_reprog_control` logs it — but only for CIDs it is arming:
```rust
if original.diverted {
// Left over from a session that never tore down (agent killed, or
// another Logitech app). Worth a line: it is the state that used to be
// replayed on restore, leaving the button dead.
debug!(cid, "control was already diverted before arming");
}
```
A CID outside the spec is never read, so it is never noticed and never handed back.
### Steps to reproduce
1. Bind Middle Click to any single action that is not its default — e.g. `MiddleClick = "ShowActionsRing"`. Confirm it works (it does; the button is diverted and the agent dispatches it).
2. Kill the agent without a clean teardown (`pkill openlogi-agent`, a crash, or force quit).
3. Switch Middle Click to **gesture mode** and bind the four directions.
4. Restart the agent, hold the middle button and swipe.
### Expected
The bound direction fires; a tap without a swipe sends a plain middle click.
### Actual
Nothing fires, in any direction, and the plain click is gone too. The button is inert. Restarting the agent, the app, or the machine does not recover it.
### Diagnostics
Instrumented `master` build. With the gesture binding in place the hook maps are correct:
```
hook_maps_for key=Some("receiver:…:slot:1") current=0
devices=[("…:slot:1", true), ("…:slot:2", false)]
gesture_keys=[MiddleClick]
```
…but `handle_button` is never reached for `MiddleClick`, while `LeftClick` on the same mouse arrives normally:
```
handle_button entry id=LeftClick pressed=true
device=Some(EventDevice { vendor_id: Some(1133), product_id: Some(50504),
product_name: Some("USB Receiver") })
oshook=false may_remap=true
```
So the event never reaches macOS at all — the button is still diverted at HID++. A tail-append `CGEventTap` of my own confirms it: it sees every `LeftClick`, and nothing whatsoever from button 2.
The gesture logic itself is fine. Feeding synthetic `otherMouseDown` + drag + `otherMouseUp` through the hook (with the macOS sender check relaxed in a debug build) runs the whole path correctly and fires the bound `VolumeUp`.
Session arming reports what you would expect — nothing armed, because gesture mode wants the button native:
```
control capture active index=1 gesture_sources=0 dpi_buttons=1 buttons=0 thumbwheel=false
```
### Fix
PR follows: reconcile at arm time. After diverting what the spec wants, read back every OpenLogi-managed control the device exposes as divertable that this session does not own, and undivert the ones still diverted. On this machine it clears the leak on the first run:
```
INFO releasing a diversion this session does not own cid=82
```
`cid=82` is `0x0052`, Middle Click.
Only the managed tables (`GESTURE_SOURCE_BUTTONS`, `DPI_MODE_SHIFT_CIDS`, `DIVERTABLE_STANDARD_BUTTONS`) are consulted, so another application's diversion is left exactly as found.
This also makes any ungraceful agent exit self-healing on the next arm, rather than leaving a button dead until the device sleeps or reconnects.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start at arm_controls_into and plan_for_device, then inspect arm_reprog_control and the managed diversion tables: GESTURE_SOURCE_BUTTONS, DPI_MODE_SHIFT_CIDS, and DIVERTABLE_STANDARD_BUTTONS. Reproduce the stale 0x1b04 state from the issue and verify that arm-time reconciliation releases unmanaged OpenLogi diversions, preserves other applications' state, and restores native gesture-button clicks.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100