Skip to content

Development

GGEZUS edited this page Sep 23, 2026 · 6 revisions

How the patchset is built and maintained. Start here if you want to hack on the gesture code, port it forward, or understand why it exists as patches at all.

Why a patchset

niri has no native touchscreen gestures (see the upstream discussion). This repo maintains a small series on top of a current niri release: animated multi-finger swipes reusing niri's touchpad gesture pipeline, taps and discrete vertical flicks at one finger more than the base count (3 or 4), hold-swipes, gesture ownership (multi-finger touches are cancelled client-side so apps don't react to them), single-finger edge and corner swipes (8 zones, any action), a horizontal-swipe mode that resizes the focused window's width live, touch point visualization, clean recovery when a touch device disappears mid-gesture, and opt-in gesture event logging.

The base swipe implementation comes from niri-touch-gestures by Mario St Jr (author of the Noctalia shell); this repo extends it with taps, discrete flicks, finger-count and deadlock fixes, and packaging.

Upstream is working on configurable gesture binds; when that lands, this patchset can be retired.

The patch series

pkg/ holds the Arch PKGBUILD plus the patches as git am-able files (authorship preserved), numbered in application order:

  1. feat(input): add touchscreen 3-finger swipe gestures
  2. feat(config): add touchscreen-swipe gesture configuration
  3. docs: document touchscreen swipe gestures
  4. feat(input): touchscreen tap and bottom-edge swipe gestures
  5. feat(input): discrete 4-finger vertical swipe gestures
  6. fix(input): count late-joining fingers and fix deadlock in discrete swipes
  7. test(config): refresh the parse insta snapshot for the touchscreen gesture fields
  8. feat(ui): show-touch-points: compositor-rendered touch point visualization
  9. feat(input): hold-swipe touchscreen gestures: move windows with a held swipe
  10. fix(input): own multi-finger touch sequences: gestures stop leaking to apps
  11. fix(input): latch gesture ownership through smithay's implicit touch grab
  12. fix(input): clear touch gesture state when a touch device is removed
  13. feat(input): gesture debug logging behind gestures debug-log
  14. feat(input): single-finger edge and corner swipe gestures
  15. docs: document touchscreen edge and corner swipe gestures
  16. fix(test): edge-swipe test helper rectangle construction
  17. feat(input): horizontal swipe column-resize mode
  18. docs: document the touchscreen-swipe action options
  19. feat(gestures): derive the tap/flick tier from fingers
  20. feat(gestures): swipe-more-left/right for extra-finger flicks

Patch 19 renames tap-4/swipe-4-up/swipe-4-down to tap-more/swipe-more-up/swipe-more-down, makes them apply at one finger more than fingers instead of a hardcoded four, and restricts fingers to 3 or 4 (hard config error outside that).

Patch 20 adds the horizontal counterparts swipe-more-left/ swipe-more-right; unbound (the shipped default), those flicks keep the horizontal drag's behavior.

Patches 3, 15, 18, 19 and 20 extend niri's own wiki docs inside the niri source tree; that documentation is mirrored (and expanded for this fork's specifics) on Gestures and Configuration.

Repo layout

pkg/       Arch PKGBUILD + the patch series against upstream niri
update-niri-tablet.sh   user-facing updater (install/update/pin)
scripts/   OSK toggle + auto-rotate helpers
config/    example niri config fragments
demo/      demo videos embedded in the README
extras/    optional extras (wvkbd build with mobile layouts)
niri-tablet-easysetup/   GUI gesture configurator (GTK4)
bugreports/ evidence captures for fixed bugs
test.kdl   minimal config for nested (in-window) testing
niri/      maintainer's dev clone for rebasing (not part of the repo)

Maintainer workflow (rebasing onto a new niri release)

Three scripts, all runnable from any cwd inside the clone:

  • update.sh - fetch upstream tags, rebase the patch branch onto the latest niri release, regenerate the patch series, and rebuild the package. After it finishes: sudo pacman -U pkg/niri-tablet-*.pkg.tar.zst, then log out and back in.
  • install.sh - rebuild the package from the current patchset state (regenerates the series from the dev clone first).
  • regen-patches.sh - regenerate pkg/*.patch from the dev clone's branch.

Each rebase gets a new tag v<niri-version>.<patch-count>, matching the installed pkgver, and the release notes append the changelog. The dev clone of niri lives in niri/ (not part of the repo); update.sh and install.sh need it.

update-niri-tablet.sh at the repo root is the user-facing updater (Updating); it never touches the dev clone or the patch series.

git am re-commits with new hashes on each machine, so exactly one machine should ever regen the series (here: the T480; the Surface only pulls and makepkgs). Recreating a dev clone elsewhere, if ever needed:

git clone https://github.com/niri-wm/niri
cd niri
git checkout -b gestures v26.04
git am ../pkg/*.patch

Small print that has bitten before:

  • Patches must sit flat in pkg/: makepkg looks up local sources by basename only, stripping any directory component.
  • git -C <dir> format-patch -o <dir> resolves the output path relative to the -C target, not the caller's cwd; pass an absolute path.
  • Fresh git clones get fresh mtimes, so cargo rebuilds everything: the scripts avoid makepkg -f and share a cargo target dir (~/.cache/niri-tablet-target) for incremental rebuilds.
  • pkg/niri is a bare clone of upstream niri doubling as makepkg's source cache (src/ clones borrow objects from it, and its origin is validated against the source URL). Don't repoint it at the dev clone, and re-point both if the project dir ever moves.

Hacking on the compositor

Hand-building outside makepkg, from the dev clone:

cargo build --release
cargo test touch_

One-time dependency: clang (bindgen/libclang). Do not export makepkg's CFLAGS/LDFLAGS into that shell: combined with rustc's default lld linker they fail the libspa static shim link (the PKGBUILD unsets them for cargo).

To test compositor changes safely, run the new binary nested, as a window inside the running session; the host forwards real touch events into it. Use the minimal config so the nested instance doesn't spawn the whole autostart stack:

niri/target/release/niri -c test.kdl

Two caveats: the nested window can stop repainting while the host holds a touch grab on it (a frame-callback stall; check NIRI_SOCKET=<nested socket> niri msg -j windows before blaming the gesture), and the nested output's edges are the window's edges, not the screen's.

For delivery bugs (touches leaking into apps, phantom fingers after gestures), the harness that cracked the phantom-touch bug:

WAYLAND_DEBUG=1 firefox | grep --line-buffered wl_touch
sudo libinput debug-events --device /dev/input/eventX

Run both side by side, timestamped, then do a per-id open/close pass over the wl_touch stream: unmatched downs and double-downs fall out immediately. gestures { debug-log } (Troubleshooting) covers the recognition side.

smithay touch delivery: facts the patches build on

Learned the hard way while debugging gesture ownership and the phantom-touch bug; anyone touching the touch code needs these:

  • smithay installs an implicit TouchDownGrab on every delivered touch-down. It routes the rest of the sequence to the first finger's surface and releases when all points lift. Consequence: is_grabbed() is true whenever any app-visible touch is down, so a !is_grabbed() gate on gesture logic never holds over app windows. The patches treat the implicit grab as "no grab" and drop it when taking ownership (patch 0011).
  • TouchInternal::motion ignores its focus argument: delivery always goes to the surface recorded at the slot's down time. Forwarding grab events with focus: None therefore swallows nothing; a real swallow means not calling the pass-through at all.
  • TouchHandle::cancel() is a silent no-op for frame-flushed slots at the smithay rev niri currently pins (every event gets frame-flushed, and cancel skips slots whose last event was flushed). Upstream's touch rework (smithay PR #2067) fixed it, but niri's pin predates the fix, so the patches end client-visible touches with synthetic up events plus a frame instead. Clients also read a clean lift better than a cancel.

Design notes for gesture-code changes

  • Never run a niri action from inside a smithay touch-grab callback: the seat touch mutex deadlocks. Actions are deferred via Niri::pending_touch_action and executed outside the grab.
  • Gesture ownership must latch through smithay's implicit touch grab (patch 0011): without the latch, a grab started before the gesture began keeps delivering touches to the app.
  • Late-joining fingers count toward the gesture (patch 0006): a 4-finger gesture where one finger lands a few ms late must still be recognized as 4.
  • Touch-device removal mid-gesture must clear all gesture state (patch 0012), or the session wedges waiting for fingers that will never lift.

Tests and CI

cargo test touch_ runs the patchset's unit tests. The full niri test suite passes with the patches applied (235 tests across the crates, including the config and wiki-doc suites) and runs in CI on every push: workflow.

test.kdl at the repo root is a minimal config for running the compositor nested (in a window) for quick manual testing.

Credits and license

License: GPL-3.0-only (the patches modify niri, also GPL-3.0-only). Swipe foundation by Mario St Jr; taps, flicks, fixes, scripts and packaging by GGEZUS; NixOS instructions by lamarios (PR #1).

Clone this wiki locally