Repository navigation
Conversation
added 9 commits
September 23, 2026 23:10
Port the PowerToys Mouse Button Lock state machine to a pure Python module with no X11 or GTK dependency: per-button hold/lock state for left, right and middle, a move-cancel dead zone, any-button-releases semantics with tap-to-release and chorded cross-button release, and the context-menu dismissal flag. Add a shared config loader that validates types, clamps ranges to the PowerToys limits, rejects non-finite numbers, migrates the 1.0 threshold_ms key while keeping left-button-only behaviour, and saves atomically.
Rewrite quickdrag.py as an adapter: XInput slave device events are taken from RECORD with the XTEST and master devices filtered out so our own injections never echo back as taps, core MotionNotify feeds the dead zone, lock engage re-injects an XTEST press, and releases inject XTEST ups with the right-button Escape dismissal deferred until the tap's release. Settings apply live on the next input event. request_stop uses a separate connection with a round trip so Stop no longer waits for the next mouse event. The CLI gains per-button flags, --move-cancel, --config and clamped overrides.
Replace the single threshold spin button with per-button checkboxes, a 200-2200 ms hold slider, a drag threshold spin button and the Escape dismissal option, all saved immediately and applied to the running service. Run as a single Gtk.Application instance so launching the app while the autostarted copy runs presents its window. Join the worker on close and on SIGINT/SIGTERM so a locked button is released before exit, and reset the UI when the service stops or errors.
Port the 17 PowerToys engine tests one to one, add Python-specific engine tests, a Hypothesis stateful machine with engine invariants, config validation tests including property tests, and X11 adapter tests driven by hand-built RECORD replies and a recording injector. A live smoke test runs only when an X display is available and injects nothing.
Mirror the PowerToys libFuzzer harness: byte-driven engine operations with settings mutation, arbitrary ticks and coordinates, plus in-harness invariants. A second target feeds arbitrary text and JSON-like structures to the config parser. run_fuzz.sh runs both for a time budget.
Bump the version, update both .deb build routes to ship engine.py and config.py through a shared launcher, drop debian/compat, install the autostart entry from debian/rules, and replace the checked-in package.
Bring every document, docstring and help text in line with the shipped code: the dismiss_context_menu setting, single-instance GUI, worker join on exit, the sync-based stop, packaging routes and the real test count. Along the way: run.py now checks the XInput extension the service needs, the config fuzzer covers the new key, the CLI gains --no-debug, the stop fallback path uses a round trip too, and the banner reports the dismissal setting.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
The current atheris wheel fails at import on 3.10 (it needs dis.Positions from 3.11), which took the canned-input replay tests down with it. Treat any import failure as "atheris unavailable" so the pure-Python fallback is used, and only require atheris on 3.11 or newer.
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
Brings Linux Quick Drag to parity with the PowerToys Mouse Button Lock module (microsoft/PowerToys#49279). The per-button state machine is a line-by-line port of the PowerToys engine and carries the same unit tests and fuzzing, so the two ports behave the same wherever X11 allows it.
Version 1.1.0.
Behaviour
move_cancel_pixels, default 5 px): moving beyond it during the hold marks the gesture as a deliberate drag and the lock does not engage.dismiss_context_menu).Architecture
src/engine.py: pure state machine, no X11 or GTK imports. MirrorsMouseButtonLockCore.h.src/config.py: one validated loader shared by GUI and CLI. Wrong types fall back to defaults, numbers are clamped to the PowerToys limits, NaN and Infinity are rejected, invalid JSON never crashes the app, saves are atomic. The 1.0threshold_mskey is migrated and a 1.0 file keeps left-button-only locking.src/quickdrag.py: X11 adapter. Button events come from XInput slave devices with the XTEST and master devices filtered out, so our own injections never echo back as release taps (they do through core events). Lock engage re-injects an XTEST press because RECORD cannot suppress the physical release.src/gui.py: single-instanceGtk.Application, per-button checkboxes, hold slider, drag threshold, live apply, and the worker is joined on close and on SIGINT/SIGTERM so a locked button is released before exit.Fixes found along the way
config.jsonwith wrong types or out-of-range values no longer crashes the GUI at startup.dpkg-buildpackageroute shipped a launcher that could not find the modules. Both routes now install the same launcher.Tests and fuzzing
pytest.debbuild.Known limitations (X11)
Manual testing still needed
The lock, tap-to-release and cross-button release paths were verified against fake wire data and in Xephyr, not with real hardware on a live desktop. Please test a real drag before merging. See
docs/TESTING.md.