feat: translate Shift+wheel into horizontal scroll on macOS/Windows/Linux#191
Open
FunJim wants to merge 3 commits into
Open
feat: translate Shift+wheel into horizontal scroll on macOS/Windows/Linux#191FunJim wants to merge 3 commits into
FunJim wants to merge 3 commits into
Conversation
Hold Shift while turning the wheel to scroll horizontally in every app. The CGEventTap rewrites a vertical scroll into a horizontal one with the Shift modifier stripped so apps that already do their own translation don't double it. Honors the existing `invert_hscroll` setting to flip the direction.
Use GetAsyncKeyState to detect Shift on every WM_MOUSEWHEEL event, queue a MOUSEEVENTF_HWHEEL injection through the existing raw-input wndproc, and block the original vertical wheel event. The translated direction honors `invert_hscroll`, matching the macOS path.
Open evdev keyboards read-only at first use to query active_keys(), and when Shift is held during REL_WHEEL forward a REL_HWHEEL on the virtual mouse and suppress the vertical scroll (REL_WHEEL_HI_RES is also swallowed so the original vertical scroll doesn't leak through). The direction follows `invert_hscroll` to match the macOS path. REL_HWHEEL is added to the uinput device's capabilities so the translation works even on basic mice without a tilt wheel.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hold Shift while turning the mouse wheel to scroll horizontally in every app — on macOS, Windows, and Linux. Each platform implements the translation in its own mouse hook so the behavior is consistent across the trio.
Why
The OS doesn't translate Shift+wheel to horizontal scroll system-wide — only some apps (mostly browsers) do it themselves. With Mouser active, the user-expected "Shift+wheel = horizontal scroll" behavior is now consistent across every app.
Behavior
HSCROLL_LEFT/RIGHTdispatch path untouched.invert_hscroll), so no new UI is required.Per-platform implementation
macOS (
core/mouse_hook_macos.py)_SHIFT_WHEEL_HSCROLL_MARKERso the synthetic horizontal scroll we post passes through our own CGEventTap on re-entry without being reprocessed._post_shift_hscroll_eventhelper copies axis-1 (vertical) deltas onto axis-2 (horizontal), zeros axis-1, copies scroll/momentum phases, and strips the Shift modifier from the new event's flags so apps that already do their own Shift+scroll translation (browsers, NSScrollView) don't double-translate.Windows (
core/mouse_hook_windows.py)WM_APP_INJECT_SHIFT_HSCROLLdeferred-injection message, mirroring the existingWM_APP_INJECT_HSCROLLinvert path.GetAsyncKeyState(VK_SHIFT)is queried in the LL hook for everyWM_MOUSEWHEEL; if Shift is held the original is blocked and aMOUSEEVENTF_HWHEELis queued for the raw-input window thread to inject viaSendInput.Linux (
core/mouse_hook_linux.py)active_keys()forKEY_LEFTSHIFT/KEY_RIGHTSHIFT._handle_rel, when Shift is held duringREL_WHEEL, writes a translatedREL_HWHEELon the virtual mouse and swallows the original vertical scroll; matchingREL_WHEEL_HI_RESevents are also suppressed so vertical scroll doesn't double-fire.REL_HWHEELso the translation works even on basic mice without a tilt wheel.Test plan
python -m py_compile main_qml.py core/*.py ui/*.py— passespython -m unittest discover -s tests -p "test_*.py"— 510 passed, 1 skipped (existing skip)MacOSShiftWheelHScrollTests(7 cases) — translation, Shift stripping, marker passthrough, no-Shift / tilt-wheel no-ops,invert_hscrollboth directions.LinuxShiftWheelHScrollTests(7 cases) — keyboard discovery,_shift_heldstate, REL_WHEEL→REL_HWHEEL translation, passthrough without Shift,invert_hscrolldirection, hi-res suppression, virtual-uinput REL_HWHEEL injection.Notes