Reactive X11 window watcher that moves new windows to the workspace where they were launched from. Zero CPU usage when idle — listens to xprop events instead of polling.
A background tracker monitors window focus changes and records which workspace you are on. When a new window appears from a watched application, window-watcher moves it to the workspace you were on before the new window appeared. It also handles existing windows that get activated from another workspace (e.g., when opening a file activates an application that lives on a different workspace).
This handles applications that reuse a single process or are spawned by other processes — regardless of how the window was created, it lands on the right workspace.
A companion tool ww-open is included as a workspace-aware replacement for xdg-open. It opens files or URLs on your current workspace — if no matching window exists there, it launches a new one instead of routing to a window on another workspace.
The DISPLAY variable is auto-detected at runtime, so the service works regardless of which display number your X11 session uses (:0, :1, etc.).
- X11 session (works with XWayland)
wmctrlxprop(fromx11-utils)systemd(user service)
Install dependencies with your package manager:
# Debian/Ubuntu
sudo apt-get install wmctrl x11-utils
# Fedora
sudo dnf install wmctrl xprop
# Arch
sudo pacman -S wmctrl xorg-xpropThen run the installer:
bash install.shThis will:
- Copy
window-watcher.shandww-opento~/.local/bin/ - Enable and start a systemd user service
Workspace-aware replacement for xdg-open. Use it to open files or URLs on your current workspace:
ww-open dashboard.html
ww-open https://example.comIf a matching window already exists on your current workspace, it reuses it. If not, it launches a new one.
bash uninstall.shEdit WATCH_CLASSES in ~/.local/bin/window-watcher.sh to declare which windows should be managed. Add WM_CLASS substrings to the array:
WATCH_CLASSES=("your-app-class" "another-class")To find the WM_CLASS of any window:
xprop | grep WM_CLASS
# then click on the windowEnable verbose logging:
systemctl --user set-environment DEBUG=1
systemctl --user restart window-watcher
journalctl --user -u window-watcher -f# View logs
journalctl --user -u window-watcher -f
# Stop the service
systemctl --user stop window-watcher
# Restart the service
systemctl --user restart window-watcher
# Check status
systemctl --user status window-watcherThe project uses a Bash testing framework vendored as a git submodule.
After cloning:
git submodule update --init --recursiveRun all tests:
./test/run.shRun only unit or integration tests:
./test/bats-core/bin/bats test/unit/
./test/bats-core/bin/bats test/integration/Tests cover:
- Unit: pure helper functions (
normalize_wid,is_watched,is_valid_ws,get_window_class) - Integration:
ww-openbranches with mocked external commands
