Skip to content

feat: wheel tilt-left/right as bindable buttons (verified on MX Ergo)#357

Open
g30r93g wants to merge 6 commits into
AprilNEA:masterfrom
g30r93g:feat/mx-ergo-tilt-buttons
Open

feat: wheel tilt-left/right as bindable buttons (verified on MX Ergo)#357
g30r93g wants to merge 6 commits into
AprilNEA:masterfrom
g30r93g:feat/mx-ergo-tilt-buttons

Conversation

@g30r93g

@g30r93g g30r93g commented Jul 5, 2026

Copy link
Copy Markdown

What

Adds the scroll-wheel tilt-left / tilt-right controls as first-class bindable buttons. These horizontal-tilt controls (HID++ 0x1b04 reprogrammable controls, CIDs 0x5b/0x5d) weren't detected or configurable before; they now appear on the mouse diagram with an action picker and default to horizontal scroll.

Addresses #100 (wheel tilt not detected / not configurable). That issue is filed against the M500S; this implements the general mechanism and is verified end-to-end on an MX Ergo (same standard Logitech tilt CIDs). I don't have an M500S to confirm, but the capture path and slot-name mapping are device-agnostic.

How (mirrors existing patterns)

  • ButtonId::TiltLeft / TiltRight (appended, TOML-stable), default-bound to HorizontalScrollLeft / HorizontalScrollRight.
  • Captured over HID++ via the same diverted-button rising-edge path as the DPI button; diverted only when a tilt is rebound (mirrors thumbwheel_armed), so native horizontal scrolling is untouched otherwise.
  • GUI hotspots come from the existing asset markers by mapping SLOT_NAME_SCROLL_LEFT / SLOT_NAME_SCROLL_RIGHT — no asset-registry change.
  • Localized across all 20 locales.

Verification

  • cargo fmt --check, cargo clippy --workspace -D warnings, and cargo test --workspace (417 passed, 0 failed) all green.
  • New unit tests: the capture rising-edge for both tilt CIDs, tilt_armed gating, and the slot-name mapping.
  • On-device (MX Ergo, Bluetooth): diag controls reports 0x5b/0x5d as divertable, and a live capture session recorded 142 physical tilt events correctly mapped to TiltLeft / TiltRight (not swapped).

Not in scope

  • The MX Ergo's Virtual Gesture Button (0xd7).
  • Label-layout changes — the right gutter is intentionally reserved for the DPI/gesture column per existing code comments, so the tilt labels stay in the left column.

g30r93g added 6 commits July 5, 2026 14:13
Address code-review: the module docs mentioned wheel tilt but the
CapturedInput::ButtonPressed variant doc, the agent-core watcher module
doc, and the ButtonId doc still described the pre-tilt set. Also clarify
that ButtonId variants are appended (TOML-stable) while ButtonId::ALL
carries the physical layout order.
@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds wheel tilt-left and tilt-right as bindable mouse controls. The main changes are:

  • New TiltLeft and TiltRight button IDs with horizontal-scroll defaults.
  • Optional HID++ diversion and capture for tilt CIDs.
  • GUI slot mapping and locale labels for tilt controls.
  • Tests for defaults, arming, slot mapping, and rising-edge capture.

Confidence Score: 5/5

This looks safe to merge after small follow-ups to tilt capture edge cases.

  • The main binding, GUI, and default-scroll paths look consistent.
  • Tilt capture can mis-handle rare event timing or crowded diverted-button frames.
  • The reported issues are limited to edge behavior around rebinding and simultaneous diverted controls.

crates/openlogi-hid/src/gesture.rs and crates/openlogi-agent-core/src/watchers/gesture.rs

Important Files Changed

Filename Overview
crates/openlogi-agent-core/src/watchers/gesture.rs Adds tilt arming and capture-session propagation; the default gating is correct, with a short restart gap after binding changes.
crates/openlogi-core/src/binding.rs Adds tilt button IDs, labels, ordering, and default horizontal-scroll actions.
crates/openlogi-hid/src/gesture.rs Adds tilt CID diversion and rising-edge dispatch; normal press/release handling follows the existing button pattern.
crates/openlogi-hid/src/reprog_controls.rs Adds constants for standard Logitech tilt-left and tilt-right CIDs.
crates/openlogi-gui/src/mouse_model/geometry.rs Maps scroll-left and scroll-right asset slots to the new tilt buttons.
crates/openlogi-gui/locales/*.yml Adds translated labels for the new tilt controls.

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "docs(tilt): note wheel-tilt buttons in c..." | Re-trigger Greptile

Comment on lines +383 to +393
let tilt_left_down = cids.contains(&reprog_controls::TILT_LEFT_CID);
if tilt_left_down && !acc.tilt_left_down {
let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::TiltLeft));
}
acc.tilt_left_down = tilt_left_down;

let tilt_right_down = cids.contains(&reprog_controls::TILT_RIGHT_CID);
if tilt_right_down && !acc.tilt_right_down {
let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::TiltRight));
}
acc.tilt_right_down = tilt_right_down;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Dropped CID Replays Tilt

When more than four diverted controls are held, the HID++ DivertedButtons frame cannot include every pressed CID. This code treats an absent tilt CID as a release, so a held tilt can fire a second ButtonPressed when it reappears in a later frame after another control is released.

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fair observation, though it isn't introduced by the tilt change — the rising-edge detection here is a direct mirror of the existing DPI-button handling a few lines up in the same handle_reprog, so it shares the identical 4-slot DivertedButtons limitation. It also can't trigger on the MX Ergo, which only ever diverts ≤4 controls (DPI + the two tilts + the optional gesture button), so a held CID never gets crowded out of a frame. A robust fix would have to change the shared rising-edge logic for the DPI button too, so I've kept it consistent here — happy to do that hardening as a separate shared-layer follow-up.

Comment on lines 238 to 260
@@ -235,6 +260,7 @@ async fn manage(
if let Err(e) = run_capture_session(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Restart Gap Drops Tilt

When a tilt binding changes from default to a custom action, the manager stops the old capture session and waits for the next poll before starting the new one. During that gap the tilt CIDs are restored to native behavior, so a tilt press made immediately after rebinding scrolls horizontally instead of running the new binding.

Fix in Codex Fix in Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is the existing behavior of the shared manage() loop rather than something specific to tilt — thumbwheel_armed and gesture-owner changes have the same ~1s re-arm transient. During the gap the tilt just falls back to native horizontal scroll (no lost input) and self-heals on the next poll. Happy to harden the shared restart timing in a separate follow-up so DPI / thumbwheel / gesture all benefit.

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.

1 participant