Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-dev.txt
- name: Unit and property tests
env:
HYPOTHESIS_PROFILE: ci
run: python -m pytest -q

fuzz:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-dev.txt
- name: Fuzz engine and config (short run)
run: bash fuzz/run_fuzz.sh 90
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-artifacts
path: fuzz/artifacts/

package:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Build .deb
run: bash scripts/build-deb.sh
- name: Lint package
run: |
sudo apt-get update && sudo apt-get install -y lintian
lintian --no-tag-display-limit quickdrag_*_all.deb || true
- uses: actions/upload-artifact@v4
with:
name: deb
path: quickdrag_*_all.deb
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,12 @@ Thumbs.db

# Config (user-specific)
# Note: Don't ignore config.json as it's in ~/.config/quickdrag/

# Tests and fuzzing
.pytest_cache/
.hypothesis/
fuzz/corpus/
fuzz/artifacts/
crash-*
leak-*
timeout-*
100 changes: 100 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,106 @@ All notable changes to Linux Quick Drag will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2026-09-23

Parity release with the PowerToys Mouse Button Lock module. The per-button
state machine (`src/engine.py`) is now a port of the PowerToys engine and
carries a port of its unit tests plus property-based tests and fuzzing.

### Added
- Lock the left, right or middle button independently (each has its own enable
setting). Left and right are on by default; middle is off.
- Press any button to release a lock. The locked button's own press is a
tap-to-release; a different button releases the held button and then clicks
normally.
- Drag threshold (`move_cancel_pixels`, default 5 px): moving the pointer
beyond it during the hold marks the gesture as a deliberate drag and the
lock does not engage.
- Context-menu dismissal: when a locked right button is released on its
own (a tap on it, turning its setting off, or stopping the service) Quick
Drag sends Escape so the menu opened by the release closes, as PowerToys
does. A release caused by pressing another button never sends it. Best
effort on X11, so it can be turned off: `dismiss_context_menu` config key
(default on), a GUI checkbox, and
`--dismiss-context-menu`/`--no-dismiss-context-menu`.
- Single instance: the GUI is a `Gtk.Application`
(`io.github.owenpkent.quickdrag`). Launching Quick Drag while the
autostarted copy is running shows and raises its window instead of
starting a second service; a second launch with `--minimized` does
nothing. Without a D-Bus session bus it runs as an independent instance.
- Live settings: changes in the GUI apply on the next input event without
restarting the service. Disabling a locked button's setting releases it.
A failed save is logged and never blocks the live change.
- Validated config loading shared by the GUI and CLI: wrong types fall back
to defaults, out-of-range numbers are clamped to 200-60000 ms and
0-10000 px, NaN and Infinity are rejected, and a `config.json` that is not
valid JSON never crashes the app. A UTF-8 byte order mark is accepted.
The `QUICKDRAG_CONFIG` environment variable and `--config PATH` select an
alternative file (the GUI accepts `--config PATH` too).
- CLI flags `--hold-duration` (`-t`), `--move-cancel`, `--lmb/--no-lmb`,
`--rmb/--no-rmb`, `--mmb/--no-mmb`,
`--dismiss-context-menu/--no-dismiss-context-menu`, `--config`. The CLI
now reads the config file; flags override it for that run.
- Test suite (313 tests): a one-to-one port of the 17 PowerToys engine unit tests,
Hypothesis stateful tests with engine invariants, config validation tests,
X11 adapter tests against fake RECORD replies, and Atheris libFuzzer
harnesses for the engine and the config parser (`fuzz/`).
- GitHub Actions CI running tests, a short fuzz pass and the .deb build.

### Changed
- Default hold duration is now 1200 ms (was 500 ms), matching Windows
ClickLock. The GUI slider covers 200-2200 ms in 100 ms steps; hand-edited
files may go up to 60000 ms.
- Config key `threshold_ms` is migrated to `hold_duration_ms` on load, and
`--threshold` remains as a hidden alias for `--hold-duration`. A 1.0
config (legacy key, no per-button keys) keeps left-button-only locking
until other buttons are enabled.
- Debug logging is off by default (the GUI previously defaulted it on). The
GUI's debug checkbox takes effect on the next Start.
- The GUI's worker thread is no longer a daemon: Stop, closing the window,
Ctrl+C and SIGTERM stop the service and join the worker. Events from a
stopped instance are ignored, and an error or an unexpected stop resets
the window to Stopped.
- The command-line mode exits with status 1 when it cannot start or input
monitoring stops unexpectedly (it always exited 0 before).
- Packaging: both .deb build routes (`scripts/build-deb.sh` and
`debian/rules`) install the shared launcher `scripts/quickdrag-launcher`
plus `engine.py` and `config.py`; `debian/rules` now also installs the
autostart entry; `debian/compat` is gone (`debhelper-compat (= 13)` in
`Build-Depends`); the checked-in package is `quickdrag_1.1.0_all.deb`.
The launcher written by `install.sh` exits with the GUI's exit code.

### Fixed
- Stopping the service from the GUI could leave the RECORD thread blocked
until the next mouse event. The disable request now goes over a separate
connection with a round trip (not just a flush), so Stop takes effect
immediately; an event that still arrives after the stop request ends the
loop too.
- A `config.json` with wrong types or out-of-range values no longer crashes
the GUI at startup.
- Saving the config is atomic (unique temporary file, fsync, rename), so an
interrupted save cannot leave a truncated file; a symlinked config is
updated at its target instead of being replaced by a regular file.
- Closing the window, Ctrl+C or SIGTERM while a button was locked could
leave it held; the worker is now joined so the release is injected
before exit.
- If input monitoring died, the GUI kept showing the service as running.
The adapter now reports it as an error and the window resets to Stopped.
- The `.deb` built with `dpkg-buildpackage` had a launcher that could not
find the modules. Both build routes now install the same launcher.

### Known limitations (X11)
- RECORD can observe but not suppress input, so the initial click of a
locked right or middle button still reaches applications: a context menu
or middle-click paste may appear when the lock engages. Windows suppresses
this; a future evdev/uinput backend could too.
- Because the hold is re-established by injecting a second press, a hold
duration shorter than the desktop's double-click time (usually 400 ms) can
make the lock register as a double click in some applications. The default
1200 ms is well clear of it.
- Our own XTEST input is told apart from real hardware by device, so the
service needs the XInput extension (present on every Xorg and XWayland).

## [1.0.0] - 2026-01-24

### Added
Expand Down
39 changes: 36 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ cd linux-quickdrag
# Install dependencies
sudo apt install python3-xlib python3-gi gir1.2-gtk-3.0

# Run in debug mode
# Run the command-line mode with debug logging
python3 src/quickdrag.py --debug

# Or the GUI (single instance: a second launch raises the running window)
python3 run.py
```

## Code Style
Expand All @@ -53,9 +56,39 @@ python3 src/quickdrag.py --debug

## Testing

Set up the dev environment once (GTK/`gi` comes from system packages, so the
venv needs `--system-site-packages`):

```bash
python3 -m venv --system-site-packages .venv
.venv/bin/pip install -r requirements-dev.txt
```

Run the automated suite before opening a PR:

```bash
.venv/bin/pytest # 313 unit and property tests (default profile)
HYPOTHESIS_PROFILE=ci .venv/bin/pytest # the profile CI runs: more examples
bash fuzz/run_fuzz.sh 60 # Atheris fuzzing, 60 s per target
```

CI (`.github/workflows/ci.yml`) runs the same `pytest` suite with
`HYPOTHESIS_PROFILE=ci` on every push to `main` and every pull request,
on Python 3.10 and 3.12, plus a 90 s fuzz pass and the `.deb` build. See
[docs/TESTING.md](docs/TESTING.md) for what each test file covers, how to
read a fuzzing failure, and how to reproduce or minimize a crash.

Automated coverage does not replace hands-on accessibility testing:

- Test with actual drag operations (file manager, drawing apps, etc.)
- Test start/stop functionality
- Test configuration persistence
with each button (left, right, middle) enabled
- Test release by tapping the locked button and by pressing another button,
and right-button releases with "Close the context menu" on and off
- Test start/stop functionality, and that closing the window or Ctrl+C
while a button is locked releases it
- Test that settings changes apply live and persist across restarts
- Test that a second launch raises the running window (and that
`--minimized` starts the service with the window hidden)
- If possible, get feedback from users with motor disabilities

## Areas Where Help is Needed
Expand Down
Loading
Loading