The Problem
A desktop without a wallpaper feels bare. Users want to set custom images, rotate through a collection, and have different wallpapers on different monitors.
The Naive Approach
Use a static wallpaper set by the compositor:
hyprctl hyprpaper wallpaper ", ~/Pictures/wallpaper.png"No slideshow, no per-monitor images, no dynamic switching based on time of day.
Think of the wallpaper manager as a gallery curator. The curator selects which artworks (images) hang on which walls (monitors) and changes them periodically (slideshow). Some galleries rotate every month, others change daily.
The Idea
Create a WallpaperService that manages a wallpaper directory. A Timer cycles through images at a configurable interval. Per-monitor support allows different images on each screen.
Let's Build It
// WallpaperService.qml
pragma Singleton
import Quickshell
QtObject {
id: wm
property string wallpaperDir: Quickshell.configPath("wallpapers")
property var wallpapers: []
property int currentIndex: 0
property int slideshowInterval: 3600000 // 1 hour
property bool slideshowEnabled: true
function loadWallpapers() {
Qt.process(["bash", "-c", "ls " + wallpaperDir + "/*.{jpg,png,jpeg,webp} 2>/dev/null"])
.onStdout.connect(function(data) {
wallpapers = data.trim().split("\n").filter(function(f) { return f })
if (wallpapers.length > 0) setWallpaper(wallpapers[0])
})
}
function setWallpaper(path) {
// Set via compositor wallpaper utility
if (Qt.process(["which", "swww"]).stdout) {
Qt.process(["swww", "img", path, "--transition-type", "fade", "--transition-duration", "2"])
} else if (Qt.process(["which", "hyprpaper"]).stdout) {
Qt.process(["hyprctl", "hyprpaper", "wallpaper", ", " + path])
} else {
// Fallback: set root window background (X11)
Qt.process(["feh", "--bg-scale", path])
}
currentWallpaper = path
}
function nextWallpaper() {
if (wallpapers.length === 0) return
currentIndex = (currentIndex + 1) % wallpapers.length
setWallpaper(wallpapers[currentIndex])
}
function previousWallpaper() {
if (wallpapers.length === 0) return
currentIndex = (currentIndex - 1 + wallpapers.length) % wallpapers.length
setWallpaper(wallpapers[currentIndex])
}
property string currentWallpaper: ""
signal wallpaperChanged(string path)
Timer {
interval: wm.slideshowInterval
running: wm.slideshowEnabled && wm.wallpapers.length > 1
repeat: true
onTriggered: wm.nextWallpaper()
}
Component.onCompleted: loadWallpapers()
}Wallpaper picker popup:
// WallpaperPicker.qml
PopupWindow {
width: 500; height: 400
anchor.window: topBar
Rectangle {
anchors.fill: parent; radius: 8; color: "#1e1e2e"
Column {
anchors { fill: parent; margins: 12 }; spacing: 8
Text { text: "Wallpaper"; font.pixelSize: 14; font.bold: true; color: "#cdd6f4" }
GridView {
width: parent.width; height: parent.height - 80
model: WallpaperService.wallpapers
cellWidth: 150; cellHeight: 100
delegate: Rectangle {
required property string modelData
required property int index
width: 140; height: 90; radius: 6; clip: true
border.color: modelData === WallpaperService.currentWallpaper ? "#89b4fa" : "transparent"
border.width: 2
Image {
source: "file://" + modelData
anchors.fill: parent; fillMode: Image.PreserveAspectCrop
asynchronous: true
}
Text {
anchors { bottom: parent.bottom; left: parent.left; margins: 4 }
text: modelData.split("/").pop()
color: "white"; font.pixelSize: 9
style: Text.Raised; styleColor: "black"
}
MouseArea {
anchors.fill: parent
onClicked: WallpaperService.setWallpaper(modelData)
}
}
ScrollBar.vertical: ScrollBar { active: true }
}
// Slideshow controls
Row {
spacing: 8
Text { text: "Slideshow"; color: "#cdd6f4"; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter }
Rectangle {
width: 40; height: 22; radius: 11
color: WallpaperService.slideshowEnabled ? "#a6e3a1" : "#45475a"
MouseArea { anchors.fill: parent; onClicked: WallpaperService.slideshowEnabled = !WallpaperService.slideshowEnabled }
}
Text { text: "Every " + (WallpaperService.slideshowInterval / 3600000).toFixed(0) + "h"; color: "#6c7086"; font.pixelSize: 11; anchors.verticalCenter: parent.verticalCenter }
}
}
}
}The wallpaper manager scans a config directory for images. A slideshow timer cycles through them. The picker shows a GridView of wallpapers with thumbnails. Supports multiple compositor wallpaper tools (swww, hyprpaper, feh).
Let's Improve It
Add per-monitor wallpapers:
property var monitorWallpapers: ({})
function setMonitorWallpaper(monitor, path) {
monitorWallpapers[monitor] = path
if (Qt.process(["which", "swww"]).stdout) {
Qt.process(["swww", "img", path, "--outputs", monitor])
}
}Not handling compositor-specific wallpaper tools. hyprpaper requires preloading images. swww uses animations. feh only works on X11. Detect available tools at startup and use the appropriate one.
Loading large images synchronously. Wallpaper images can be 4K+ resolution. Load thumbnails at reduced resolution for the picker. Use asynchronous: true on Image elements.
Forgetting the fallback. If no wallpaper tool is installed, the wallpaper manager should still work. Provide a solid color fallback that can be set via compositor color.
Under the Hood
Wallpaper setting on Wayland uses compositor-specific protocols. swww is a standalone wallpaper daemon that works with any compositor via its own protocol. hyprpaper is Hyprland-specific. Both create a surface at the background layer that renders the image. The wallpaper manager detects which tool is available and delegates to it.
Exercises
- Wallpaper manager scans a directory and sets via compositor tools
- Detect swww, hyprpaper, or feh at startup
- GridView of thumbnails with selection highlight
- Slideshow timer with configurable interval