AprilNEA / AprilNEA/OpenLogi

[Bug]: gesture mode kills Middle Click — a stale 0x1b04 diversion is never released

Open
#917 1 comment 0 reactions 0 assignees View on GitHub
area: hidpp platform: all type: bug
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.