Rotate your desktop wallpaper using photos pulled live from your own Immich library — filtered by album or person, changed on whatever interval you like, with a system tray icon for status and quick controls.
Two portrait photos are automatically paired side-by-side into a single composite when your screen resolution can be detected; landscape/square photos are shown singly.
Only ever a handful of images sit on disk at once — the app asks Immich's API for a random photo on each rotation and keeps a small rolling window (your choice how many) rather than syncing your whole library locally.
- Settings window (
settings_window.py) — a native GTK window for entering your Immich server URL/API key, picking albums and/or people to pull from, and setting the rotation interval and how many images to keep on disk. It runs as its own small process with no server and no open port, and the API key sits in a masked field. On platforms where GTK isn't practical to install (Windows, macOS) the same settings are offered as a local web page instead (config_ui.py); it isn't installed or started on Linux. - Rotator (
rotate.py) — does the actual work: picks a random photo (or two portraits, paired), downloads it, sets it as the desktop wallpaper, and keeps a bounded back/forward history. - Tray icon (
tray_app.py) — shows live status (active / paused / error), lets you step back/forward through recently-shown photos, open the current photo in Immich's web viewer, save a copy elsewhere, refresh on demand, pause/resume, and jump to settings. - Desktop support: KDE Plasma (via D-Bus scripting — no QtWebEngine
involved) and XFCE (via
xfconf-query/xrandr). Desktop is auto-detected at runtime. - Multiple screens (Settings → Screens): with more than one monitor you can show the same photo on every screen (each drawn at that screen's own size), a different photo on each, or one picture across all screens (photos side by side filling the whole width, never cropped). You can also change only some screens and leave the others as you set them, and allow more than two photos on a very wide screen. Supported on KDE Plasma and XFCE, which set wallpapers per screen; see the notes under Known issues.
- On-image overlays (optional, off by default) — inspired by
Immich Kiosk: a discreet
caption per photo (people names, location, date taken, stacked top to
bottom) and/or today's date in the corner. Baked into the image at each
rotation rather than rendered live, since this is a static wallpaper, not
a web page. Margins automatically clear any taskbar/panel the desktop
reports via the
_NET_WORKAREAX11 property (works through XWayland on Wayland KDE sessions too), on top of a base 0.5" edge margin computed from the display's real DPI. - No syncing, no local mirror of your library — everything is fetched on-demand from Immich's REST API.
sudo apt install ./immich-wallpaper_<version>_all.deb
(or build it yourself — see packaging/debian/build.sh)
This pulls in python3-pystray, python3-pil, python3-gi,
gir1.2-ayatanaappindicator3-0.1, and x11-xserver-utils (for xrandr,
needed on XFCE) automatically, installs the app under
/usr/share/immich-wallpaper/, and registers /etc/xdg/autostart so the
tray icon starts on login for any user.
cd packaging/arch
makepkg -si
All dependencies (python-pystray and xorg-xrandr, needed on XFCE,
included) are in the official extra repo, so plain makepkg -si
resolves everything — no AUR helper needed. Verified building/installing
on Manjaro.
Needs Python 3.8+, Pillow, pystray, PyGObject with GTK 3, an AppIndicator
provider, and xrandr (on every desktop but KDE). On Debian/Ubuntu:
sudo apt install python3-pystray python3-pil python3-gi gir1.2-gtk-3.0 gir1.2-ayatanaappindicator3-0.1 x11-xserver-utils
python3 tray_app.py
The tray app drives its own periodic rotation internally (no cron/systemd timer needed) — as long as it's running, it self-paces against the interval you set in the settings window.
- Run the tray icon and choose Settings… from its menu (or run
immich-wallpaper-config, orpython3 settings_window.py). - Enter your Immich server URL and an API key. Create the key under
Account Settings → API Keys in Immich, and grant only:
asset.read,asset.download,album.read,person.read— no write/delete/admin scopes needed. (If your Immich version doesn't offer granular scopes, an all-permissions key also works.) - Test the connection, then load and pick albums and/or people to filter by (leave both empty to pull from your whole library).
- Set the rotation interval and how many images to keep on disk, save.
- From the tray icon menu: Refresh now to pull the first photo, or just wait for the interval to elapse.
On each rotation, a batch of random candidates (filtered by your
album/person selection) is fetched with EXIF data. If the first pick is
portrait-oriented and the screen resolution can be detected, a second
portrait from the same batch is paired with it into one composite image
sized exactly to your screen (via KDE's screenGeometry() scripting API,
or xrandr on XFCE). Otherwise the photo is shown singly, letterboxed
to your screen size (whole photo, black bars, never cropped) so it looks the
same whatever fill mode the desktop applies by default. Only when the screen
size can't be detected, or a file can't be decoded (e.g. HEIC without a
Pillow plugin), is the original file used as-is and the desktop's own fill
mode applies. "Save a copy" saves the wallpaper as displayed, bars included.
-
Multiple screens are new and lightly tested. They were built and tested against simulated monitors, and on KDE Plasma the per-screen setter was checked on a real session with one monitor. Real multi-screen behaviour on KDE, and the XFCE setter (checked against a recording fake only), still need confirming on real hardware, so reports are welcome. With one screen nothing changes.
-
Spanned pictures ignore each monitor's real vertical position. The screens are laid side by side as one strip, with shorter screens centred, so a photo across two screens of different heights may not line up perfectly at the join.
-
Overlay margins with several screens. Photo info and the date use the base margin only, not taskbar-aware margins, when more than one screen is in use. The date appears on the primary screen only.
-
Screens are identified by connector name (for example
HDMI-A-1), so a selection survives unplugging and replugging, but renaming a connector (for instance by moving a cable to a different port) means selecting it again. -
libayatana-appindicator is deprecated (please use libayatana-appindicator-glib in newly written code)— you may see this printed when starting the tray icon by hand. It's emitted by the system'slibayatana-appindicatorC library itself, the momentpystrayloads itsAyatanaAppIndicator3GObject-Introspection binding — not by anything in this project.pystraydoesn't yet support the newerlibayatana-appindicator-glib, which also has no widely-packaged Python/GI binding yet, so there's nothing to switch to. Harmless; the tray icon works normally.
The app holds an API key for your Immich server and downloads files from it, so it is deliberately careful:
- No network server on Linux. The settings screen is a native window, not a web page, so there is no local port for a website or another user to reach.
- Private storage. The config (which holds the API key), the state, the cached photos and the failure log are readable by you only (mode 0600, directories 0700).
- A strict Immich client. Server URLs must be plain
http(s)://; a redirect is followed only within the same host, so a server or proxy can't bounce the API key elsewhere; replies are size-limited; ids put in URLs are encoded as single path segments. - Untrusted files. Photos come from a remote server, so decoding refuses "decompression bombs", any decoder failure falls back to the original file, and the file name's extension is only trusted if it is short and alphanumeric.
- The web settings page (other platforms only) requires a per-run
access token, checks the
HostandOriginheaders, accepts only JSON posts, never sends the API key to the browser, and serves a strict Content-Security-Policy.
Security linting (ruff's bandit rules) is part of the standard checks.
python3 -m unittest discover -s tests -t . # the test suite (~13 s)
ruff check . # style, docstrings, security
mypy *.py # types
The tests run against a throwaway HOME, so they never touch your real
config, state or wallpaper, and the GTK tests skip themselves on a machine
without a display. To add support for another desktop, write its
screen-size and set-wallpaper functions and register a DesktopBackend in
desktops.py.
MIT — see LICENSE.
The Immich flower logo used in the tray icon is Immich's own trademark, used here only to indicate integration with their project — this is an independent, unofficial companion tool and isn't affiliated with or endorsed by the Immich project.