Keep a floating picture-in-picture window (browser PiP, mpv, a webcam, etc.) visible on every workspace you switch to — the "pin on all workspaces" behaviour other compositors have, for niri.
niri has no built-in sticky-window mode yet (niri#932). This is a ~30-line daemon that reaches the same result: it listens to niri's JSON event stream and moves the PiP window to whatever workspace you focus. No polling, no CPU while you sit still.
niri-pip-follow reads niri's event stream (niri msg -j event-stream):
- On a
WorkspacesChangedevent it grabs the now-focused workspace id and, if the tracked PiP window isn't already there, runsniri msg action move-window-to-workspace. - On a
WindowsChangedevent it re-discovers the PiP's window id (so the move keeps targeting the right window as you open/close/resize it).
Because it reacts to events instead of polling, it costs nothing when idle.
niri(IPC enabled — the default)jqbash(the script uses process substitution; plain POSIXsh/dash is not enough)
Add a window rule to your niri config (~/.config/niri/config.kdl). This one
targets the Zen browser PiP — change app-id/title for Firefox, mpv, etc.
window-rule {
match app-id="zen" title="^Picture-in-Picture$"
open-floating true
default-column-width { fixed 480; }
default-window-height { fixed 270; }
default-floating-position x=32 y=32 relative-to="bottom-left"
}install -m755 niri-pip-follow ~/.local/bin/niri-pip-followStart it at login by adding this to your niri config:
spawn-at-startup "~/.local/bin/niri-pip-follow"(On older configs the key is spawn-sh-at-startup.)
That's it — the PiP now follows you across workspaces.
The matcher lives in two env vars at the top of the script:
| App | PIP_APP_ID |
PIP_TITLE |
|---|---|---|
| Zen Browser | zen |
Picture-in-Picture |
| Firefox | firefox |
Picture-in-Picture |
| mpv | mpv |
(match by app-id only — see below) |
Override per launch:
PIP_APP_ID=firefox PIP_TITLE=Picture-in-Picture niri-pip-followFor mpv (where the window title is the filename), match by app-id only: in the
WindowsChanged branch change the select(...) to
select(.app_id == $app).
niri-pull-pip is a one-shot version — pull the PiP to the current workspace on
demand instead of always-on:
binds {
Mod+P { spawn-sh "~/.local/bin/niri-pull-pip"; }
}Useful if you only want it sometimes, or as a fallback to the daemon.
- The PiP follows the focused workspace; it does not render above fullscreen windows. Showing a pinned window over fullscreen is a separate niri feature (tracked in #932).
- The window briefly disappears/reappears during the move (that's niri moving the window, not a flicker in the video itself).
MIT. Do whatever.