Skip to content

feat(hook): reverse the wheel in software for devices without 0x2121 - #847

Merged
davidbudnick merged 5 commits into
AprilNEA:masterfrom
4ni1ak:feat/software-scroll-inversion
Oct 10, 2026
Merged

davidbudnick merged 5 commits into
AprilNEA:masterfrom
4ni1ak:feat/software-scroll-inversion

Conversation

@4ni1ak

@4ni1ak 4ni1ak commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Scroll inversion was offered only where the firmware exposes 0x2121 HiResWheel with its invert bit. A large part of the current Logitech line
does not — the Signature M650 / M750 family exposes 0x2130 RatchetWheel and
no 0x2121 at all, and the M575S is in the same position — so the GUI answered
"This device does not report native HID++ scroll inversion support" for a mouse
Logi Options+ happily reverses.

This reverses the wheel in the capture layer for exactly those devices. One
implementation covers both reports, so it closes both.

Fixes #694
Fixes #776

Where the inversion happens, and why there

Each backend transforms the event in place, where it still carries its native
units
: the fields the reader used are the fields the writer negates, so
nothing is converted or rounded.

Re-injecting from the normalised delta_y — which #694 offers as the primary
option — was the first design and it is wrong on macOS. usable_scroll_delta
reads whichever of the point / fixed / line fields is non-zero, so a hi-res
wheel arrives as pixels and a detented one as lines; re-synthesising from the
single normalised value would have to pick a unit and would be wrong for one of
them. Negating the same fields sidesteps the choice.

  • Linux — REL_WHEEL / REL_WHEEL_HI_RES negated at the point the grabbed
    event is re-emitted through the pass-through uinput device.
  • macOS — the vertical scroll fields are negated on the tapped event before
    it is passed through, scoped to non-trackpad sources the hook would remap
    buttons for, so a second vendor's mouse and the trackpad are untouched.
  • Windows — not supported, and not because WH_MOUSE_LL hands back a
    read-only event: suppressing and re-injecting would be exact there. The
    blocker is attribution. The Windows hook reports no device for a wheel event
    (MouseEvent::Scroll always carries device: None), so the inversion could
    not be confined to the configured mouse, and applying it to every wheel event
    would reverse a precision touchpad too — the one thing this setting must not
    do. SOFTWARE_SCROLL_INVERSION is false there and the GUI keeps reporting
    the device as unsupported.

Changes

openlogi-hook

  • set_scroll_inversion / scroll_inversion, and the
    SOFTWARE_SCROLL_INVERSION capability const.
  • The Linux and macOS transforms above.
  • examples/invert_scroll.rs: a manual smoke test that grabs, runs one window
    with inversion off and one with it on, then releases. Two phases on purpose —
    a single window cannot tell an applied inversion from a desktop that was
    already scrolling that way.

openlogi-agent-core

  • The orchestrator publishes the flag for the selected device only, and only
    when its firmware has no invert bit: a device that has one still gets the
    HID++ write from configured_wheel_mode, and doing both would cancel out.
    Disabling the device, deselecting it, or turning the setting off clears it.

openlogi-desktop

  • current_scroll_inversion_available / _is_software; the toggle no longer
    gates on native support.
  • The description now distinguishes the two, because "OpenLogi reverses this as
    it captures it" is a different guarantee from the device doing it. New string
    added to every catalog at the same position.

Horizontal scrolling is untouched throughout: the setting reverses the wheel,
not the thumb wheel.

Known scope

The flag is process-global, so it applies to every pointer the hook captures.
The hook attributes events to a device, but nothing maps an OS event source
back to the config key the setting is stored under, so a second Logitech mouse
would follow the first. Documented on the flag rather than left to be
discovered; per-device scoping needs that mapping to exist first.

Testing

Linux, x86_64, Rust 1.98.0:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings      # RUSTFLAGS=-D warnings
cargo test --workspace
cargo clippy --target x86_64-pc-windows-gnu -p openlogi-hook -p openlogi-agent-core \
  --all-targets -- -D warnings
cargo test -p openlogi-ui locale

All green. Four unit tests on the Linux transform: both vertical axes negated,
off is a byte-for-byte pass-through, horizontal and pointer axes untouched, and
i32::MIN saturates instead of wrapping back to the same sign.

Verified on hardware — Linux, Signature M650 over Bluetooth. That device is
the class in question: openlogi diag features lists 0x2130 and no 0x2121,
and it advertises both REL_WHEEL and REL_WHEEL_HI_RES, so both axes the
transform handles are live on it. Running the two-phase example, the hook
grabbed the mouse, OpenLogi virtual mouse appeared, 620 wheel events were
captured across the two windows, and the scroll direction was normal in the
pass-through phase, reversed in the inverted phase, and normal again once the
grab was released. Horizontal events: zero, as expected — the M650 has no thumb
wheel.

Not verified: macOS. There is no macOS SDK on this host, so that backend is
neither run nor compiled here. The core-graphics setters it uses
(set_double_value_field, set_integer_value_field) were checked against the
vendored 0.25 source, and it negates exactly the fields usable_scroll_delta
reads, but it needs a real Mac and a 0x2130 mouse — the configuration #694 was
reported on. Also not run here: tests (macos), cargo-deny, macOS clippy.

To check on macOS: connect an M650/M750, enable the toggle (it should now be
offered, labelled as the software fallback), and confirm the wheel reverses
while the trackpad keeps the system direction.

@4ni1ak
4ni1ak requested a review from AprilNEA as a code owner August 23, 2026 08:50
@greptile-apps

greptile-apps Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with no outstanding actionable findings.

Summary

Adds software wheel inversion on Linux and macOS for devices without native support. Windows remains unsupported.

  • OpenLogi reverses vertical wheel scroll for devices without native inversion.
  • The scroll setting is available when a mouse needs software inversion.

Reviews (13) · Last reviewed commit: "fix(hook): read scroll inversion once pe..." · Reviewed by Greptile

@4ni1ak
4ni1ak force-pushed the feat/software-scroll-inversion branch from fa99cfd to 9e1bccc Compare August 24, 2026 19:23
@davidbudnick davidbudnick added type: feature New feature request platform: all Cross-platform issue labels Aug 25, 2026
@4ni1ak
4ni1ak force-pushed the feat/software-scroll-inversion branch 3 times, most recently from 316eb23 to 334a2f1 Compare August 26, 2026 00:39
@hamidrezahanafi

Copy link
Copy Markdown

Thanks for the PR, I built openlogi with your change and it's working perfectly with my MX Vertical!

@tata-meow

Copy link
Copy Markdown

I went through the code and the approach is solid — negating at the native event level avoids the unit-conversion pitfalls of suppress+reinject, and scoping the macOS path to attributed non-trackpad sources is the right guard.

I have a Signature M650 on macOS + Bolt, which is exactly the configuration #694 was reported on and the macOS path you haven't been able to verify. Happy to build and test if you rebase past the merge conflicts — the locale files migrated from .yml to .toml with semantic keys in #1169, so that'll be the bulk of the conflict resolution.

One note on the process-global flag: you documented the limitation honestly, but it's worth noting that scroll_source_may_intercept already attributes scroll events to a device on macOS, so the per-device scoping might be closer than it looks — the event-to-config-key mapping is the missing piece rather than event attribution itself.

@mingtsay

mingtsay commented Sep 6, 2026

Copy link
Copy Markdown

Rebased this onto current master (d5e2b880) and verified the macOS path on hardware. Branch: https://github.com/mingtsay/OpenLogi/tree/feat/software-scroll-inversion

Conflicts. Three, all mechanical except the last:

One thing that wasn't wired up. scrolling_card() still read current_scroll_inversion_supported() into inversion_supported, so on a 0x2130 device the toggle stayed disabled and the description stayed "does not report native HID++ scroll inversion support" — current_scroll_inversion_available() was never called from anywhere. Switched it to available(), which is what makes the three-way inversion_description() reachable. Also moved two doc comments that had ended up on the wrong functions (scrolling_card's onto inversion_description, run_tap_callback's onto wheel_scroll_to_invert).

macOS, verified on hardware. Signature M650 over a Logi Bolt receiver, macOS 26.6.2, Apple Silicon — the configuration #694 was reported on. openlogi diag wheel gives device does not expose HID++ feature 0x2121, and the device's persisted capabilities carry scroll_inversion = false, so this is the class in question. With the toggle on, the agent logs accessibility granted — installing OS mouse hook / OS input hook installed, the wheel reverses, and a paired Magic Trackpad on the same machine keeps the system scroll direction. Your core-graphics field negation is correct against a real hi-res wheel.

Worth noting for anyone testing: the macOS transform runs on the CGEventTap, so the agent needs Accessibility, not just Input Monitoring. On a config with no button remappings that is easy to miss — nothing else would have started the hook.

Gates. macOS (Apple Silicon): cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace --all-targets — all green. Linux (aarch64, 1.98.0, in Docker): the same plus cargo test --workspace --exclude openlogi-desktop and cargo clippy --target x86_64-pc-windows-gnu -p openlogi-hook -p openlogi-agent-core --all-targets. Not run: cargo-deny.

@4ni1ak
4ni1ak force-pushed the feat/software-scroll-inversion branch from 0c5a9e2 to 46a265a Compare September 7, 2026 15:47
Scroll inversion was offered only where the firmware exposes `0x2121
HiResWheel` with its invert bit. A large part of the current Logitech line
does not: the Signature M650 / M750 family exposes `0x2130 RatchetWheel`
and no `0x2121` at all, and the M575S is in the same position, so the GUI
answered "This device does not report native HID++ scroll inversion
support" for a mouse Logi Options+ happily reverses.

Reverse it in the capture layer instead, for devices that cannot do it
themselves. `openlogi_hook::set_scroll_inversion` turns it on; each backend
transforms the event in place, where it still carries its native units —
the fields the reader used are the fields the writer negates, so nothing is
converted or rounded. Re-injecting from the normalised `delta_y` would have
had to pick a unit, and a macOS hi-res wheel reports pixels in one field
while a detented one reports lines in another.

The orchestrator publishes the flag for the selected device only, and only
when the firmware has no invert bit of its own — a device that has one
still gets the HID++ write, and doing both would cancel out. The GUI stops
gating the toggle on native support and says which of the two is in play,
because "OpenLogi reverses this as it captures it" is a different guarantee
from the device doing it.

`SOFTWARE_SCROLL_INVERSION` is false on Windows, and not because
`WH_MOUSE_LL` cannot rewrite an event — suppressing and re-injecting would
be exact there. The blocker is attribution: the Windows hook reports no
device for a wheel event, so the inversion could not be confined to the
configured mouse, and applying it to every wheel event would reverse a
precision touchpad too. That is the one thing this setting must not do.

Horizontal scrolling is untouched throughout: the setting reverses the
wheel, not the thumb wheel.

`scrolling_card()` was reading `current_scroll_inversion_supported()`
instead of `current_scroll_inversion_available()`, so on a `0x2130` device
the toggle stayed disabled and the software-inversion path — the entire
point of this change — was never reachable from the GUI. Fixed, and rebased
onto current master (locale keys carried through AprilNEA#1169's `.yml` → `.toml`
migration, `orchestrator.rs`'s `publish_hook_maps()` extraction).

Fixes AprilNEA#694
Fixes AprilNEA#776

Co-authored-by: mingtsay <mt@mingtsay.tw>
@4ni1ak
4ni1ak force-pushed the feat/software-scroll-inversion branch from 46a265a to 165350a Compare September 16, 2026 08:55
@4ni1ak

4ni1ak commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Adopted @mingtsay's rebase and fix — credited via a co-author trailer on the commit.

  • Rebased onto current master (was 115 commits behind, includes the gpui-kit migration and feat(i18n): migrate catalogs to semantic TOML keys #1169's .yml → .toml locale port).
  • Fixed the real bug found: `scrolling_card()` was reading `current_scroll_inversion_supported()` instead of `current_scroll_inversion_available()`, so on a 0x2130 device the software-inversion toggle stayed disabled — the entire point of this PR was unreachable from the GUI.
  • All translations carried through, including the new be.toml feat(i18n): migrate catalogs to semantic TOML keys #1169 added.

Full local gate (fmt/clippy/test --workspace) green on the rebased tree. Verified on macOS hardware per mingtsay's report (Signature M650 over a Bolt receiver, the #694 configuration) — thanks for that, and for catching that Accessibility (not just Input Monitoring) is what the macOS transform needs since it runs on the CGEventTap.

@yadav-prakhar

Copy link
Copy Markdown

@AprilNEA please have a look. Many devices will be unblocked by this.

@rvanhorn

Copy link
Copy Markdown

@4ni1ak looks like there are conflicts

@AprilNEA any chance once resolved we can get this merged in?

# Conflicts:
#	crates/openlogi-agent-core/src/orchestrator.rs
#	crates/openlogi-desktop/src/state/scroll.rs
@davidbudnick
davidbudnick self-requested a review as a code owner October 10, 2026 21:05
Comment thread crates/openlogi-hook/src/macos.rs
Comment thread crates/openlogi-hook/src/linux.rs Outdated
Comment thread crates/openlogi-ui/locales/de.toml Outdated
Comment thread crates/openlogi-hook/src/linux.rs Outdated
Co-authored-by: David Budnick <david@budnick.ca>

@davidbudnick davidbudnick left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for everyone's work on this change!

@davidbudnick
davidbudnick merged commit c58808f into AprilNEA:master Oct 10, 2026
23 checks passed
@aprilnea aprilnea Bot mentioned this pull request Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform: all Cross-platform issue type: feature New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Support Inverse scroll direction without native HID++ [Feature]: Software scroll inversion for RatchetWheel (0x2130) devices

8 participants