4-Finger Swipe to Change Workspace in labwc on Wayland
If you use labwc on a laptop, you probably miss the 4-finger swipe to switch workspaces from GNOME or macOS. labwc 0.20 has no gesture support in rc.xml, and the usual answer for Linux gestures, touchegg, does not run on a pure Wayland session. Here is a setup that works: a small daemon that reads your touchpad through libinput and switches workspaces with wtype.
I run this on a Xiaomi Book Pro 14 (CachyOS, labwc 0.20.2, touchpad BLTP7853:00 347D:7853). The same steps apply to any Arch-based distro and most labwc setups.
Why touchegg does not work here
touchegg 2.0.18 is the first thing most guides suggest, and I tried it. It fails twice on this setup:
- The touchegg client needs an X11 display. On a pure Wayland labwc session (my
rc.xmlhas<xwayland>no</xwayland>) it aborts immediately:what(): Error opening your X11 display. Make sure your DISPLAY environment variable is set - Its
CHANGE_DESKTOPaction targets GNOME and KDE window managers. labwc ignores it.
touchegg detects gestures through libinput, so the hardware side is fine. The problem is only how actions get delivered. The fix is to skip touchegg and go straight from libinput to a Wayland-native key sender.
How it works
Three pieces:
libinput debug-eventsreports swipe gestures from the touchpad, including finger count and movement deltas.- A Python script watches that output, accumulates movement for 4-finger swipes, and decides the direction once the gesture ends.
wtypepressesCtrl+Super+LeftorCtrl+Super+Right, which labwc already binds toGoToDesktopinrc.xml.
No AUR packages, no X11, no compositor patches.
Step 1: Check prerequisites
You need libinput, wtype, and Python 3. Your user must be in the input group so the daemon can read the touchpad device:
sudo pacman -S libinput wtype python
sudo usermod -aG input $USER
Log out and back in after adding the group, then verify:
groups | grep -o input
libinput list-devices | grep -A2 -i touchpad | head -n 5
Look for Capabilities: ... gesture on your touchpad. If gesture is missing, the hardware does not report multi-finger swipes and this method cannot work.
Step 2: Confirm the labwc keybinds exist
Open ~/.config/labwc/rc.xml and check the <keyboard> section for workspace switching binds:
<keybind key="C-W-Left"><action name="GoToDesktop" to="left" /></keybind>
<keybind key="C-W-Right"><action name="GoToDesktop" to="right" /></keybind>
In labwc syntax C-W-Left means Ctrl + Super + Left. If these lines are missing, add them inside <keyboard> and reload with labwc --reconfigure. The gesture daemon presses exactly this combination, so an existing shortcut you already use keeps working alongside swipes.
Step 3: Install the gesture daemon
Save this as ~/.local/bin/labwc-4finger-gestures:
#!/usr/bin/env python3
"""4-finger touchpad swipe -> labwc workspace switch (Wayland-native).
Reads `libinput debug-events`, accumulates SWIPE dx/dy for 4-finger
gestures, then emulates Ctrl+Super+Left/Right via `wtype`
(which matches the GoToDesktop keybinds in ~/.config/labwc/rc.xml).
Direction (GNOME-style, natural):
swipe LEFT (fingers move left) -> next workspace (Right)
swipe RIGHT (fingers move right) -> previous workspace (Left)
swipe UP -> next workspace (Right)
swipe DOWN -> previous workspace (Left)
Needs: user in `input` group, `wtype`, `libinput`.
"""
import os
import re
import subprocess
import sys
import time
THRESHOLD = float(os.environ.get("LABWC_GESTURE_THRESHOLD", "70.0"))
COOLDOWN = float(os.environ.get("LABWC_GESTURE_COOLDOWN", "0.6"))
WTYPE = "/usr/bin/wtype"
RE_BEGIN = re.compile(r"GESTURE_SWIPE_BEGIN\s+\+\S+\s+(\d+)\s*$")
RE_UPDATE = re.compile(r"GESTURE_SWIPE_UPDATE\s+(?:\d+\s+)?\+\S+\s+(\d+)\s+(-?[\d.]+)/\s*(-?[\d.]+)")
RE_END = re.compile(r"GESTURE_SWIPE_END\s+\+\S+\s+(\d+)\s*$")
RE_CANCEL = re.compile(r"GESTURE_SWIPE_CANCELLED|GESTURE_CANCELLED")
def send_key(key: str) -> None:
env = dict(os.environ)
env.setdefault("XDG_RUNTIME_DIR", "/run/user/1000")
env.setdefault("WAYLAND_DISPLAY", "wayland-0")
subprocess.run(
[WTYPE, "-M", "ctrl", "-M", "logo", "-k", key, "-m", "logo", "-m", "ctrl"],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
def decide(dx: float, dy: float) -> str | None:
"""Return wtype key (Left/Right) or None if below threshold."""
if abs(dx) >= abs(dy) and abs(dx) >= THRESHOLD:
return "Right" if dx < 0 else "Left" # swipe left -> next
if abs(dy) > abs(dx) and abs(dy) >= THRESHOLD:
return "Right" if dy < 0 else "Left" # swipe up -> next
return None
def main() -> int:
fingers = 0
dx = dy = 0.0
tracking = False
last_action = 0.0
proc = subprocess.Popen(
["libinput", "debug-events"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
bufsize=1,
)
assert proc.stdout is not None
print("labwc-4finger-gestures: listening (threshold=%.0f)" % THRESHOLD, flush=True)
for line in proc.stdout:
if RE_CANCEL.search(line):
tracking = False
continue
m = RE_BEGIN.search(line)
if m:
fingers = int(m.group(1))
dx = dy = 0.0
tracking = fingers == 4
continue
m = RE_UPDATE.search(line)
if m and tracking and int(m.group(1)) == 4:
dx += float(m.group(2))
dy += float(m.group(3))
continue
m = RE_END.search(line)
if m and tracking:
if int(m.group(1)) == 4:
# This libinput version reports no totals on END;
# use dx/dy accumulated from UPDATE lines.
key = decide(dx, dy)
now = time.monotonic()
if key and now - last_action >= COOLDOWN:
last_action = now
print(f"swipe dx={dx:.0f} dy={dy:.0f} -> {key}", flush=True)
send_key(key)
tracking = False
return proc.wait()
if __name__ == "__main__":
sys.exit(main())
Make it executable:
chmod +x ~/.local/bin/labwc-4finger-gestures
Two details in this script come from real debugging, so keep them. The END lines in libinput 1.32 carry no movement totals, only the finger count, which is why the script accumulates UPDATE deltas instead of reading the totals. And the patterns anchor on the timestamp format (+0.095s), because an early version matched digits inside the timestamp and dropped every gesture.
Step 4: Run it as a user service
Create ~/.config/systemd/user/labwc-4finger-gestures.service:
[Unit]
Description=4-finger touchpad swipe to switch labwc workspace (libinput + wtype)
After=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=simple
Environment=XDG_RUNTIME_DIR=/run/user/1000
Environment=WAYLAND_DISPLAY=wayland-0
ExecStart=%h/.local/bin/labwc-4finger-gestures
Restart=always
RestartSec=2
[Install]
WantedBy=default.target
Enable and start it:
systemctl --user daemon-reload
systemctl --user enable --now labwc-4finger-gestures.service
systemctl --user status labwc-4finger-gestures.service
The service survives reboots and restarts the daemon if it crashes. If your Wayland socket has a different name, check ls /run/user/1000/wayland* and adjust WAYLAND_DISPLAY accordingly.
Step 5: Test
Swipe 4 fingers left or right on the touchpad with a deliberate motion. The workspace should switch, same as pressing Ctrl+Super+Left/Right.
Watch the log to confirm the daemon sees your gestures:
journalctl --user -u labwc-4finger-gestures -f
A working swipe prints a line like:
swipe dx=-186 dy=-8 -> Right
If lines appear but the workspace does not switch, the problem is on the wtype side. Test it directly:
XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 \
wtype -M ctrl -M logo -k Left -m logo -m ctrl
If no lines appear when you swipe, the problem is detection. Run libinput debug-events in a terminal and swipe; you should see GESTURE_SWIPE_BEGIN lines with a 4 at the end. If you only see 3, your touchpad caps out at 3 fingers and you need to change fingers == 4 to fingers == 3 in the script.
Tuning
Two environment variables adjust the feel. Set them with systemctl --user edit labwc-4finger-gestures.service under [Service]:
Environment=LABWC_GESTURE_THRESHOLD=50.0
Environment=LABWC_GESTURE_COOLDOWN=0.6
LABWC_GESTURE_THRESHOLD (default 70) is the minimum swipe distance in libinput units. On my touchpad a full swipe measures 150 to 350, and accidental brushes stay under 50, so 70 ignores mistakes without missing real swipes. Lower it if light flicks do nothing, raise it if workspaces switch when you did not mean it.
LABWC_GESTURE_COOLDOWN (default 0.6 seconds) blocks double triggers from one long swipe.
To flip the direction (swipe left goes to the previous workspace instead of the next), swap "Right" and "Left" in the decide function.
Troubleshooting
Gestures stop working after a labwc restart
Restart the service so its libinput debug-events child reopens the touchpad device with fresh file descriptors:
systemctl --user restart labwc-4finger-gestures.service
Service is running but nothing happens
Check that the child process exists and the socket name matches:
pgrep -af "4finger|debug-events"
ls -la /run/user/1000/wayland*
If the socket is wayland-1 instead of wayland-0, update the service file and restart.
Swipes work but also fire twice
Raise the cooldown:
Environment=LABWC_GESTURE_COOLDOWN=1.0
I want 3-finger swipes instead
Change both fingers == 4 comparisons to fingers == 3 in the script and restart the service. Keep in mind labwc has no 3-finger conventions, so pick whatever feels right.
Summary
labwc has no built-in touchpad gestures, and touchegg cannot fill the gap on pure Wayland because its client requires X11. A short Python daemon reading libinput debug-events plus wtype for key emulation covers exactly the missing piece: 4-finger swipes trigger the GoToDesktop keybinds you already have. The whole setup is one script, one systemd user unit, and no extra packages beyond wtype.