Repository navigation
Development
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.
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.
pkg/ holds the Arch PKGBUILD plus the patches as git am-able files
(authorship preserved), numbered in application order:
feat(input): add touchscreen 3-finger swipe gesturesfeat(config): add touchscreen-swipe gesture configurationdocs: document touchscreen swipe gesturesfeat(input): touchscreen tap and bottom-edge swipe gesturesfeat(input): discrete 4-finger vertical swipe gesturesfix(input): count late-joining fingers and fix deadlock in discrete swipestest(config): refresh the parse insta snapshot for the touchscreen gesture fieldsfeat(ui): show-touch-points: compositor-rendered touch point visualizationfeat(input): hold-swipe touchscreen gestures: move windows with a held swipefix(input): own multi-finger touch sequences: gestures stop leaking to appsfix(input): latch gesture ownership through smithay's implicit touch grabfix(input): clear touch gesture state when a touch device is removedfeat(input): gesture debug logging behind gestures debug-logfeat(input): single-finger edge and corner swipe gesturesdocs: document touchscreen edge and corner swipe gesturesfix(test): edge-swipe test helper rectangle constructionfeat(input): horizontal swipe column-resize modedocs: document the touchscreen-swipe action optionsfeat(gestures): derive the tap/flick tier from fingersfeat(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.
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)
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- regeneratepkg/*.patchfrom 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/*.patchSmall 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-Ctarget, not the caller's cwd; pass an absolute path. - Fresh git clones get fresh mtimes, so cargo rebuilds everything: the
scripts avoid
makepkg -fand share a cargo target dir (~/.cache/niri-tablet-target) for incremental rebuilds. -
pkg/niriis 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.
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.kdlTwo 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/eventXRun 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.
Learned the hard way while debugging gesture ownership and the phantom-touch bug; anyone touching the touch code needs these:
- smithay installs an implicit
TouchDownGrabon 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::motionignores its focus argument: delivery always goes to the surface recorded at the slot's down time. Forwarding grab events withfocus: Nonetherefore 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, andcancelskips 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.
-
Never run a niri action from inside a smithay touch-grab callback: the
seat touch mutex deadlocks. Actions are deferred via
Niri::pending_touch_actionand 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.
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.
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).