Skip to content

fix: divert thumbwheel for rotation rebinds even without single-tap capability - #415

Open
tagawa0525 wants to merge 2 commits into
AprilNEA:masterfrom
tagawa0525:fix-thumbwheel-divert-without-single-tap
Open

fix: divert thumbwheel for rotation rebinds even without single-tap capability#415
tagawa0525 wants to merge 2 commits into
AprilNEA:masterfrom
tagawa0525:fix-thumbwheel-divert-without-single-tap

Conversation

@tagawa0525

Copy link
Copy Markdown

Problem

On an MX Master 4, rebinding the thumbwheel rotation (ThumbwheelScrollUp / ThumbwheelScrollDown) or changing the thumbwheel sensitivity has no effect — silently. The GUI accepts and persists the config, reload_config goes through, the capture session restarts, but the wheel keeps scrolling natively in both directions.

Root cause

arm_controls in openlogi-hid/src/gesture.rs only diverts the thumb wheel over 0x2150 when getThumbwheelInfo reports the single-tap capability:

if supports_single_tap {
    tw.set_reporting(true, false).await?;
    thumb = Some((tw, info.index));
} else {
    debug!("thumb wheel reports no single tap — click not capturable");
}

The MX Master 4 does not set that capability bit (verified on hardware — Bolt receiver, feature 0x2150 v0 present at index 19; getInfo byte 5 has 0x08 clear). Its thumb area was reworked into a haptic pad, which already behaves differently over HID++ elsewhere (see #313), so the missing bit is plausibly genuine rather than a firmware reporting bug.

But single-tap support only matters for capturing the wheel's click. Rotation rebinds and the sensitivity multiplier need the diverted event stream too — thumbwheel_armed() in openlogi-agent-core correctly requests capture for them — and the arming side then silently refuses. The user gets no error anywhere: just a debug-level line, and settings that appear to do nothing. This affects every platform equally; it just hasn't been visible because MX Master 3/3S report the bit and arm fine.

Fix

Divert whenever capture was requested, regardless of the single-tap capability. A missing single tap now only means a bound click can never fire (the single_tap event bit simply never arrives), which is the accurate scope of the limitation. Doc comments updated to match.

Behavior is unchanged for wheels that do report single tap.

If the original gate was intentional (avoid suppressing native scroll on wheels where re-synthesis was considered untested), I'd argue the inconsistency should then be fixed on the thumbwheel_armed() side instead — requesting capture and silently not arming is the worst of both. Happy to rework in that direction if preferred.

Verification

On real hardware (MX Master 4, slot 2 on a Bolt receiver, Linux/Wayland):

Before — with ThumbwheelScrollUp = "HorizontalScrollLeft" saved and reloaded:

DEBUG openlogi_hid::gesture: thumb wheel reports no single tap — click not capturable
INFO  openlogi_hid::gesture: control capture active index=2 gesture=false dpi_buttons=1 thumbwheel=false

After:

INFO  openlogi_hid::gesture: control capture active index=2 gesture=false dpi_buttons=1 thumbwheel=true

and the wheel behaves as configured (both rotation rebinds and the sensitivity multiplier now apply).

cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01N76prZoUHMrHZajXq4jET1

Copilot AI review requested due to automatic review settings July 18, 2026 11:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes silent thumbwheel rebind failures on devices (e.g. MX Master 4) that don't set the single-tap capability bit in getThumbwheelInfo, by decoupling the divert decision from that capability. Previously, arm_controls only called set_reporting(true) when supports_single_tap was true, so rotation rebinds and sensitivity multiplier changes were silently ignored; now the wheel is diverted whenever capture_thumbwheel is set, regardless of single-tap support.

  • gesture.rs: Removes the if supports_single_tap guard and calls set_reporting(true, false) unconditionally within the capture_thumbwheel block; the absence of single-tap now only means a bound click can never fire, not that the wheel won't divert.
  • thumbwheel.rs + gesture.rs doc comments: Updated throughout to describe the new "divert on any non-default thumbwheel config" semantics.

Confidence Score: 5/5

Safe to merge — the change is a minimal, surgical removal of a single conditional gate with no impact on devices that already reported single-tap support.

The fix correctly narrows the responsibility of the single-tap capability check to click binding only, leaving divert logic driven purely by whether capture was requested. Devices that already set the capability bit are completely unaffected. The error path (getInfo failure → treat as no single tap) still proceeds to divert, which is reasonable since the feature index was already confirmed present. Doc comments are consistent with the new behaviour.

No files require special attention.

Important Files Changed

Filename Overview
crates/openlogi-hid/src/gesture.rs Core fix: removes the single-tap gate so set_reporting(true) is called unconditionally when capture_thumbwheel is true; doc comments updated throughout to match.
crates/openlogi-hid/src/thumbwheel.rs Doc-only update: module-level comment now correctly describes divert-on-any-config-change behaviour instead of divert-on-click-bound-only.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant AC as arm_controls
    participant TW as Thumbwheel (0x2150)

    note over AC: capture_thumbwheel = true

    AC->>TW: get_feature(0x2150)
    TW-->>AC: feature info (index)

    AC->>TW: getThumbwheelInfo
    alt getInfo OK
        TW-->>AC: "ThumbwheelInfo { supports_single_tap }"
        alt !supports_single_tap (e.g. MX Master 4)
            AC->>AC: debug "no single tap — click not capturable"
        end
    else getInfo error
        TW-->>AC: Err(e)
        AC->>AC: warn "getInfo failed"
    end

    note over AC,TW: Before fix: only divert if supports_single_tap
    note over AC,TW: After fix: always divert when capture was requested

    AC->>TW: "setThumbwheelReporting(diverted=true)"
    TW-->>AC: OK
    AC->>AC: "thumb = Some((tw, index))"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant AC as arm_controls
    participant TW as Thumbwheel (0x2150)

    note over AC: capture_thumbwheel = true

    AC->>TW: get_feature(0x2150)
    TW-->>AC: feature info (index)

    AC->>TW: getThumbwheelInfo
    alt getInfo OK
        TW-->>AC: "ThumbwheelInfo { supports_single_tap }"
        alt !supports_single_tap (e.g. MX Master 4)
            AC->>AC: debug "no single tap — click not capturable"
        end
    else getInfo error
        TW-->>AC: Err(e)
        AC->>AC: warn "getInfo failed"
    end

    note over AC,TW: Before fix: only divert if supports_single_tap
    note over AC,TW: After fix: always divert when capture was requested

    AC->>TW: "setThumbwheelReporting(diverted=true)"
    TW-->>AC: OK
    AC->>AC: "thumb = Some((tw, index))"
Loading

Reviews (3): Last reviewed commit: "docs: update remaining doc comments to t..." | Re-trigger Greptile

@tagawa0525

Copy link
Copy Markdown
Author

Both stale doc comments flagged by the review are updated in 6304f4b — the module-level doc and the CapturedInput::Scroll variant doc now describe the broadened divert trigger (click bound, rotation rebound, or sensitivity changed).

tagawa0525 and others added 2 commits July 18, 2026 23:33
…apability

arm_controls refused to divert the thumb wheel unless getThumbwheelInfo
reported the single-tap capability. Rotation rebinds and the sensitivity
multiplier need the diverted event stream too, and devices like the
MX Master 4 report no single-tap bit — leaving ThumbwheelScrollUp/Down
bindings silently inert. Divert whenever capture is requested; a missing
single tap now only means a bound click can never fire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N76prZoUHMrHZajXq4jET1
Review follow-up: the module-level doc and the CapturedInput::Scroll doc
still described diversion as click-bound-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N76prZoUHMrHZajXq4jET1
@tagawa0525
tagawa0525 force-pushed the fix-thumbwheel-divert-without-single-tap branch from 6304f4b to 45a22e5 Compare July 18, 2026 14:33
tagawa0525 added a commit to tagawa0525/OpenLogi that referenced this pull request Jul 19, 2026
…ilNEA#415)

## Why
On wheels that report no single-tap capability (e.g. MX Master 4), the
thumbwheel was never diverted, so rotation rebinds and the sensitivity
multiplier silently did nothing.

## What
Divert the thumb wheel whenever capture is requested, not only when the
wheel advertises single-tap; lacking the tap only means a bound click
can never fire.

## Impact
openlogi-hid capture path; MX Master 4 thumbwheel rotation rebinds and
sensitivity start working. Upstream PR AprilNEA#415.
tagawa0525 added a commit to tagawa0525/OpenLogi that referenced this pull request Jul 19, 2026
## Why
Capture previously followed only the GUI's selected device: bindings on
any other online device did nothing, and a selection switch tore down
and re-armed the single session.

## What
Every online device gets its own capture session, driven by per-device
capture plans (config + inventory) that carry the device's own binding
maps; inputs dispatch against the plan of the session they arrived on.
Includes the per-device manage toggle, per-device thumbwheel
sensitivity / DPI cycle state, the plain divert of a single-bound
gesture button, session-teardown draining, plan republish on app
switch, the capture-channel slot ownership fix, and the Linux
evdev/devenv fixes made along the way. The AprilNEA#415 thumbwheel divert fix
is stacked underneath this branch and arrives with it.

## Impact
openlogi-agent-core watchers, openlogi-hid capture, GUI gallery ring.
Upstream PRs AprilNEA#415 + AprilNEA#419 (in review; merged at the current tip).
tagawa0525 added a commit to tagawa0525/OpenLogi that referenced this pull request Jul 25, 2026
## Why
Capture previously followed only the GUI's selected device: bindings on
any other online device did nothing, and a selection switch tore down
and re-armed the single session.

## What
Every online device gets its own capture session, driven by per-device
capture plans (config + inventory) that carry the device's own binding
maps; inputs dispatch against the plan of the session they arrived on.
Includes the per-device manage toggle, per-device thumbwheel
sensitivity / DPI cycle state, the plain divert of a single-bound
gesture button, session-teardown draining, plan republish on app
switch, the capture-channel slot ownership fix, and the Linux evdev
fix made along the way. The AprilNEA#415 thumbwheel divert fix is stacked
underneath this branch and arrives with it; the devenv fixes ride in
their own branch merged just before this one.

## Impact
openlogi-agent-core watchers, openlogi-hid capture, GUI gallery ring.
Upstream PRs AprilNEA#415 + AprilNEA#419 are still open; this merges a copy of the
branch rebased onto the current origin/master — the PR branches
themselves are left untouched (and therefore unmerged here).
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.

2 participants