Wallpaper Engine workshop wallpapers on KDE Plasma 6 / Wayland, with the quality-of-life pieces the bare renderer does not have: a graphical library and properties editor, per-monitor assignment, profiles, a slideshow, global shortcuts, and — most importantly — automatic pausing so the renderer stops burning GPU power whenever nobody can see the wallpaper.
Three rendering backends, switchable in the GUI: the own renderer built on linux-wallpaperengine, wallpaper-engine-kde-plugin, and — new in 0.3 — waywallen with Open Wallpaper Engine, which renders inside Plasma's own wallpaper layer, so desktop icons stay visible for scene, video and web wallpapers. This project only drives them.
Status: built for one machine (Nobara 44, Plasma 6.7 on Wayland, NVIDIA RTX 4080, a 32:9 ultrawide plus a 4K second monitor). Every feature below was tested there;
we-wallpaper doctortells you what your machine is missing. The UI is English by default and German under a German locale. Code comments are German.
Measured on the reference machine with a scene wallpaper at 5120×1440:
| state | GPU power | GPU util | CPU |
|---|---|---|---|
| rendering at 60 fps | ~61 W | 37 % | 6.5 % |
| rendering at 30 fps | ~53 W | 35 % | 2.5 % |
frozen (SIGSTOP) |
~45 W | 25 % | 0 % |
The wallpaper is invisible while the screen is locked, while a game runs
fullscreen, or while a maximized window covers every monitor. we-wallpaper-watch
freezes the renderer in exactly those situations and thaws it afterwards.
own renderer (linux-wallpaperengine) |
KDE plugin (wallpaper-engine-kde-plugin) |
waywallen + Open Wallpaper Engine (owe) |
|
|---|---|---|---|
| desktop icons | hidden (layer-shell surface sits above Plasma's desktop, on every --layer) |
visible | visible (renders inside Plasma's wallpaper layer) |
| rendering | scene, video, web | video and web (its scene backend crashed plasmashell; refused unless forced) | scene, video, web — the scene that broke the KDE plugin renders fine here |
| per-wallpaper properties, live preview, profiles, slideshow | yes | properties via the plugin's own dialog | slideshow and profiles yes; properties forwarded to waywallen (we-wallpaper owe prop) |
| automatic pausing (fullscreen / lock / maximized) | yes, with ~19 W GPU saved | the plugin's own pause modes | waywallen's own policy, set from your pause_on_* config |
| global shortcuts Meta+Shift+W/N/P | yes | — | yes (W and P toggle waywallen's pause) |
| multi-monitor | per output and spans | per desktop | per display, matched by resolution; no spans |
| GPU with the Blue Nebula scene | ~53 W at 30 fps | — | ~51 W, ~47 W paused (the rest is Plasma compositing) |
Switch in the GUI (Backend box) or on the command line:
we-wallpaper backend # show which one is active
we-wallpaper backend owe # waywallen + OWE: icons stay, every wallpaper type
we-wallpaper backend kde-plugin # stop own renderer, activate the plugin with the assigned wallpaper
we-wallpaper backend native # back to the own rendererpackaging/install-owe.sh # Flatpak from Flathub, Plasma extension, OWE bundle (~150 MB), then switchesThe script installs everything into your home (no root): the
org.waywallen.waywallen Flatpak, the org.waywallen.kde Plasma wallpaper
extension via kpackagetool6, and the OWE plugin bundle into the Flatpak's
plugin directory, verifying the SHA-256 of both downloads. It restarts
plasmashell once so the QML extension is picked up.
we-wallpaper backend owe then stops the own renderer, starts the daemon if
needed, enables its autostart (through the XDG background portal), sets every
desktop's wallpaper plugin to org.waywallen.kde, copies your
pause_on_fullscreen/maximized/lock settings into waywallen's pause policy,
and applies the wallpaper you assigned — looked up in waywallen's catalogue by
its Workshop ID. The GUI's Apply button, the slideshow and we-wallpaper next
keep working; they re-apply through waywallen.
In this mode the pause daemon does not freeze anything itself: waywallen pauses
its renderer on fullscreen, maximized and locked screens. The daemon only keeps
the shortcuts (Meta+Shift+P forwards the manual pause) and records the reason
for status. Talking to waywallen needs no extra Python packages — the control
script speaks its WebSocket/Protobuf protocol with a small built-in codec
(field numbers documented in share/wwproto/).
we-wallpaper owe status # daemon version, current wallpaper, renderers
we-wallpaper owe list # waywallen's catalogue with Workshop IDs
we-wallpaper owe apply [workshop-id] # re-apply the config, or one wallpaper on every display
we-wallpaper owe pause [on|off|toggle]
we-wallpaper owe scan # rescan the library after subscribing to new wallpapers
we-wallpaper owe prop <workshop-id> <key> <value>Switching uses Plasma's scripting interface (org.kde.PlasmaShell evaluateScript) to set the wallpaper plugin of every desktop and write the
plugin's configuration (WallpaperSource in the plugin's packed
<folder>/<file>+<type> form, WallpaperWorkShopId, SteamLibraryPath,
Fps, Volume, MuteAudio, MouseInput).
Installing the plugin: build it from one source tree with
packaging/build-kde-plugin.sh (clones with submodules, builds, prints the
sudo cmake --install step). Do not mix the COPR package with an upstream QML
package — the COPR RPM ships only the QML library, and a QML package from a
different commit renders black.
Upstream status: both catsout/wallpaper-engine-kde-plugin and its scene
renderer are archived (read-only) since 2026; development moved to
waywallen/open-wallpaper-engine,
which is a standalone Wayland wallpaper daemon — not a Plasma wallpaper plugin,
so it shares this project's desktop-icon limitation. The plugin patches in
packaging/kde-plugin-patches/ therefore cannot go upstream; the same fix
lives in the fork
pbaetz99/wallpaper-scene-renderer.
Scene wallpapers: the plugin's scene backend does not render every scene
(unknown render targets such as _rt_shadowAtlas), and unpatched it aborted
plasmashell with an uncaught std::out_of_range. build-kde-plugin.sh
applies packaging/kde-plugin-patches/ so a broken scene is logged and
dropped instead of taking the desktop down — verified: same scene, same parser
errors, plasmashell keeps running. we-wallpaper backend kde-plugin still
refuses scenes by default (--force / "kde_plugin_allow_scene": true); use
the own renderer for scenes, Meta+Shift+W frees the icons.
Fedora/Nobara quirk: the QtWebSockets QML module the plugin imports
lives in qt6-qtwebsockets-devel. dnf remove wallpaper-engine-kde-plugin
autoremoves it and the plugin stops loading ("module QtWebSockets is not
installed") — we-wallpaper doctor checks for it.
Library with search filter, monitor list with the Backend box, properties editor for the selected wallpaper.
| file | role |
|---|---|
bin/we-wallpaper |
control script: doctor, start/stop/restart/status, profiles, slideshow, toggle, next, thaw, backend, systemd install |
bin/we-wallpaper-gui |
PySide6 application: library grid, filters, favourites, properties editor, live preview, profiles, tray icon |
bin/we-wallpaper-watch |
pause daemon: freezes the renderer on fullscreen / lock / maximized / manual; owns the KWin script and the global shortcuts |
share/fullscreen-watch.js |
KWin script: detects fullscreen and maximized windows, registers the shortcuts, reports over D-Bus |
share/wwproto/ |
waywallen's control.proto (MIT) for reference — the control script implements the few messages it needs by field number |
packaging/install-owe.sh |
installs waywallen, its Plasma extension and Open Wallpaper Engine into the user's home, then switches to the owe backend |
systemd/*.service |
user units (auto-restart on crash, bound to the Plasma session) |
packaging/ |
RPM spec, build-rpm.sh, build-kde-plugin.sh (builds the KDE plugin from one source tree), install-owe.sh |
tests/ |
unit tests (tests/run.sh), run by CI on every push |
contrib/we-ambient-guard |
unrelated to wallpapers: works around an OpenRGB Effects-plugin deadlock and duplicate OpenRGB starts; see below |
- KDE Plasma 6 on Wayland (KWin must support
wlr-layer-shell; it does). - A built
linux-wallpaperengine(default path~/.local/src/linux-wallpaperengine/build/output/linux-wallpaperengine, override withWE_RENDERER=/path/to/binary). - Wallpaper Engine installed through Steam (via Proton) — the renderer needs
its
assets/folder — plus subscribed Workshop wallpapers in~/.local/share/Steam/steamapps/workshop/content/431960/. - Python 3 with
PySide6(Fedora:python3-pyside6),kscreen-doctor,ffprobe(for real video resolutions). - For the
owebackend:flatpak,kpackagetool6,unzip,curl—packaging/install-owe.shfetches the rest.
git clone https://github.com/pbaetz99/we-wallpaper.git
cd we-wallpaper
./install.sh # copies to ~/.local/bin, ~/.local/share, adds the menu entry
we-wallpaper doctor # checks every prerequisite and says what is missing
we-wallpaper list # shows your subscribed wallpapers
we-wallpaper-gui # assign wallpapers, click "Apply"Or build an RPM (rpm-build, rpmdevtools, systemd-rpm-macros):
packaging/build-rpm.sh # writes ~/rpmbuild/RPMS/noarch/we-wallpaper-*.rpmTo run as systemd user services (recommended — auto-restart, session-bound):
we-wallpaper install-serviceThis removes any ~/.config/autostart/we-wallpaper.desktop so nothing starts
twice. we-wallpaper uninstall-service reverts it.
we-wallpaper doctor # prerequisites check with hints
we-wallpaper start|stop|restart|status|list
we-wallpaper backend [native|kde-plugin|owe]
we-wallpaper owe <status|list|apply|pause|scan|prop|start|stop>
we-wallpaper toggle # renderer off/on (owe: pause toggle), keeps the watcher alive
we-wallpaper next # advance the slideshow now
we-wallpaper thaw # emergency: wake a frozen renderer
we-wallpaper profile <name> # apply a saved profile
we-wallpaper profiles
Global shortcuts (registered by the KWin script; change them in System Settings → Shortcuts → KWin):
| shortcut | action |
|---|---|
| Meta+Shift+W | wallpaper off / on (frees the desktop icons) |
| Meta+Shift+N | next wallpaper from the slideshow pool |
| Meta+Shift+P | manual pause / resume |
~/.config/we-wallpaper.json is the single source of truth; the GUI writes it.
Per-wallpaper properties are the ones the wallpaper author exposes in
project.json; the GUI renders them as switches, sliders, colour pickers and
combos, honouring the author's condition rules. They are passed to the
renderer as --set-property name=value.
KWin on Wayland does not implement wlr-foreign-toplevel-management, so the
renderer cannot detect fullscreen windows by itself. fullscreen-watch.js runs
inside KWin, watches window state, and calls we-wallpaper-watch over D-Bus
(org.wewallpaper.Watch). The daemon freezes the renderer with SIGSTOP and
resumes it with SIGCONT.
With "per_output": true there is one renderer process per monitor (spans stay
one process) and a supervisor inside the systemd unit restarts a crashed one.
The daemon then freezes only the process whose monitor is covered — a
fullscreen game on one screen no longer stops the wallpaper on the other. In
the default single-process mode the rule is "every monitor covered". A span is
one surface and cannot be paused by halves; "span_pause_any": true pauses it
as soon as one of its monitors is covered (saves GPU), the default waits for
all of them (the visible half keeps moving).
A frozen process depends on somebody waking it up, so there are several safety
nets: the daemon wakes the renderer on start and on exit, a watchdog checks
every 5 s that the process state matches the pause reasons, we-wallpaper status
frees a frozen renderer when no daemon is alive (the GUI polls status every
5 s), and we-wallpaper thaw exists for emergencies. The current reason is
written to ~/.local/state/we-wallpaper-pause.json and shown by status.
Not about wallpapers. On the reference machine the OpenRGB Effects plugin's
"Ambient" effect died every time the display powered off: both the Effects and
VisualMap plugins hit a Qt deadlock (Dead lock detected in BlockingQueuedConnection) and the effect thread never recovered. The guard
follows the journal for that message and restarts OpenRGB through its systemd
unit. It also removes duplicate OpenRGB instances that Plasma's session restore
starts on top of the autostart entry (excludeApps in ksmserverrc did not
prevent that on the Wayland path).
Set WE_OPENRGB_UNIT if your autostart unit is not
app-OpenRGB@autostart.service. Note that a restarted OpenRGB only brings the
effect back if an effect profile with AutoStart is saved in the plugin.
pkill -f namematches the shell that runs it. Every process lookup here goes through/proc/<pid>/exeor an exact argv basename instead.- Own systemd user units must not be
After=plasma-workspace.targetandWantedBy=plasma-workspace.target— that is an ordering cycle and systemd silently deletes the start job. UseAfter=plasma-core.target. QDBusConnection.connect(...)in PySide6 wants the slot inSLOT()form:"1onLockChanged(bool)", with the leading digit.- Steam's workshop preview images are square crops; they say nothing about a wallpaper's aspect ratio. Only video wallpapers have a measurable resolution.
- Qt style sheets cannot embed image data or draw CSS triangles; the combo-box arrows are PNGs generated at runtime.
- COPR / distribution packages. The spec builds locally; publishing to a COPR is the next step.
- Scene wallpapers with desktop icons are solved by the
owebackend (waywallen draws inside Plasma's wallpaper layer). The own renderer still hides the icons: Plasma's desktop window is composited opaque, so nothing below it can show through — that is a compositor fact, not a bug here. owebackend gaps: spans (one wallpaper across several monitors) are not mapped yet, displays are matched to your monitors by resolution because waywallen does not expose connector names, and waywallen's own playlists are not surfaced in the GUI.- More translations than German/English.
MIT — see LICENSE. linux-wallpaperengine has its own license; this project
only launches it as a separate process.

{ "screens": { "DP-1": { "id": "2941300170", "scaling": "fit", "clamp": "clamp", "properties": { "schemecolor": "0.9 0.1 0.1" } } }, "spans": [], // [{outputs:[...], id, scaling, clamp, properties}] "layer": "bottom", // background|bottom|top|overlay "renderer": "", // path to linux-wallpaperengine; empty = default, WE_RENDERER wins "backend": "native", // native | kde-plugin | owe "per_output": false, // one renderer process per monitor -> pausing per monitor "span_pause_any": false, // a span pauses when ONE of its monitors is covered (default: all) "fps": 30, "volume": 15, "silent": false, "noautomute": true, "disable_particles": false, "disable_mouse": false, "disable_parallax": false, "pause_on_fullscreen": true, "pause_on_lock": true, "pause_on_maximized": true, "rotation": { "enabled": false, "minutes": 30, "pool": [], "order": "random" }, "profiles": {}, "favorites": [], "hidden": [] }