Async wallpaper daemon with animation support and native gar integration
garbg is the wallpaper management component of the gardesk ecosystem. Built in Rust with async Tokio runtime, it handles static images, animated GIFs, WebP, and APNG files with configurable frame rates. Supports multiple wallpaper sources including local files, HTTP URLs, and GitHub repositories. Features per-workspace and per-monitor wallpapers, slideshow rotation with shuffle, and comprehensive IPC control via garbgctl.
GIF, WebP, and APNG support with configurable max FPS
Local files, directories, HTTP URLs, GitHub repositories
fill, fit, stretch, center, tile with per-source override
Different wallpaper for each gar workspace via config
Per-monitor wallpapers or span across all displays
Automatic rotation with configurable interval and shuffle
Load wallpapers directly from GitHub repos via github:// URL
garbgctl CLI and JSON over Unix socket with event subscription
garbg requires the following runtime dependencies:
Set a wallpaper immediately with the set command:
Control how wallpapers are scaled to fit your screen:
Run garbg as a daemon for persistent wallpaper management and slideshow support:
When using gar, garbg starts automatically via systemd user services:
# ~/.config/systemd/user/garbg.service
[Unit]
Description=garbg wallpaper daemon
PartOf=gar-session.target
After=gar.service
[Service]
ExecStart=/usr/bin/garbg daemon
Restart=on-failure
[Install]
WantedBy=gar-session.target Use garbgctl to control the running daemon:
garbg configuration uses TOML format stored at ~/.config/garbg/config.toml. The configuration file is optional - garbg works with sensible defaults and command-line arguments.
| Option | Type | Default | Description |
|---|---|---|---|
default_mode | - | "fill" | Default scaling mode when not specified |
default_source | - | "" | Default wallpaper source on startup |
socket_path | - | $XDG_RUNTIME_DIR/garbg.sock | Override IPC socket location |
pid_file | - | $XDG_RUNTIME_DIR/garbg.pid | Override PID file location |
| Option | Type | Default | Description |
|---|---|---|---|
enabled | - | true | Enable animated wallpaper support |
max_fps | - | 60 | Maximum frames per second for animations |
frame_skip | - | true | Allow frame skipping when behind schedule |
pause_on_battery | - | false | Pause animations when on battery power |
| Option | Type | Default | Description |
|---|---|---|---|
enabled | - | true | Enable image caching to disk |
directory | - | ~/.cache/garbg | Cache directory path |
max_size_mb | - | 500 | Maximum cache size in megabytes |
ttl_days | - | 30 | Cache entry time-to-live in days |
| Option | Type | Default | Description |
|---|---|---|---|
interval | - | 300 | Seconds between wallpaper changes |
shuffle | - | false | Randomize wallpaper order |
transition | - | "none" | Transition effect (none, fade, slide) |
transition_duration | - | 500 | Transition duration in milliseconds |
# ~/.config/garbg/config.toml
[general]
default_mode = "fill"
default_source = "~/Pictures/Wallpapers"
[animation]
enabled = true
max_fps = 60
[slideshow]
interval = 300
shuffle = true Define different wallpapers for each gar workspace. When you switch workspaces, garbg automatically changes the wallpaper.
| Option | Type | Default | Description |
|---|---|---|---|
id | - | - | Workspace number (1-indexed) |
source | - | - | Wallpaper source for this workspace |
mode | - | default_mode | Scaling mode override for this workspace |
# Per-workspace wallpapers
[[workspaces]]
id = 1
source = "~/Pictures/workspace1.jpg"
mode = "fill"
[[workspaces]]
id = 2
source = "~/Pictures/workspace2.jpg"
mode = "fit"
[[workspaces]]
id = 3
source = "github://catppuccin/wallpapers/minimalistic"
mode = "fill"
[[workspaces]]
id = 4
source = "~/Pictures/animated-workspace.gif"
mode = "fill" Configure different wallpapers for each monitor in a multi-monitor setup. Monitor names match the output names from xrandr.
| Option | Type | Default | Description |
|---|---|---|---|
name | - | - | Monitor name from xrandr (e.g., DP-1, HDMI-1) |
source | - | - | Wallpaper source for this monitor |
mode | - | default_mode | Scaling mode override for this monitor |
slideshow | - | false | Enable slideshow on this monitor |
interval | - | slideshow.interval | Slideshow interval override for this monitor |
# Per-monitor wallpapers
[[monitors]]
name = "DP-1"
source = "~/Pictures/main-monitor.jpg"
mode = "fill"
slideshow = true
interval = 600
[[monitors]]
name = "HDMI-1"
source = "~/Pictures/secondary-monitor.jpg"
mode = "fit"
[[monitors]]
name = "eDP-1"
source = "~/Pictures/laptop-screen.jpg"
mode = "fill" # ~/.config/garbg/config.toml
[general]
default_mode = "fill"
default_source = "~/Pictures/Wallpapers"
# socket_path = "/run/user/1000/garbg.sock"
# pid_file = "/run/user/1000/garbg.pid"
[animation]
enabled = true
max_fps = 60
frame_skip = true
pause_on_battery = true
[cache]
enabled = true
directory = "~/.cache/garbg"
max_size_mb = 500
ttl_days = 30
[slideshow]
interval = 300
shuffle = true
transition = "fade"
transition_duration = 500
# Workspace-specific wallpapers
[[workspaces]]
id = 1
source = "~/Pictures/coding-wallpaper.jpg"
mode = "fill"
[[workspaces]]
id = 2
source = "~/Pictures/browser-wallpaper.jpg"
mode = "fill"
[[workspaces]]
id = 3
source = "github://dharmx/walls/anime"
mode = "fill"
# Monitor-specific wallpapers (overrides workspace settings)
[[monitors]]
name = "DP-1"
source = "~/Pictures/ultrawide.jpg"
mode = "fill"
slideshow = true
interval = 600
[[monitors]]
name = "HDMI-1"
source = "~/Pictures/portrait-display.jpg"
mode = "fit" When multiple configuration sources apply, garbg uses this priority order:
garbg is an async wallpaper daemon built in Rust for the gardesk desktop environment. It manages X11 root window pixmaps to display wallpapers, with support for static images, animated GIFs, WebP, and APNG files. garbg integrates with the gar window manager to provide per-workspace wallpapers and multi-monitor support.
Unlike simple wallpaper setters that exit after setting the pixmap, garbg runs as a daemon to support animated wallpapers, slideshow rotation, and runtime control via IPC. The daemon uses async Tokio for non-blocking I/O when fetching remote wallpapers and managing multiple sources.
In daemon mode, garbg runs persistently and manages wallpaper lifecycle:
// Daemon event loop structure
loop (
tokio::select! (
// Handle IPC commands from garbgctl
cmd = ipc_rx.recv() => handle_command(cmd),
// Handle workspace change events from gar
ws = workspace_rx.recv() => handle_workspace_change(ws),
// Slideshow timer tick
_ = slideshow_interval.tick() => advance_slideshow(),
// Animation frame timer
_ = frame_timer.tick() => render_next_frame(),
// Unix signals (SIGTERM, SIGHUP)
sig = signal_rx.recv() => handle_signal(sig),
)
) When invoked without the daemon subcommand, garbg sets the wallpaper and exits. In standalone mode:
garbg manages X11 pixmaps carefully to prevent black screens:
// Set CloseDownMode::RetainPermanent so pixmaps survive daemon exit
xcb::set_close_down_mode(conn, CloseDownMode::RetainPermanent);
// Clean up old pixmap before setting new one
fn cleanup_old_pixmap(conn: &Connection) (
// Query current root window pixmap
let old_pixmap = get_root_pixmap(conn);
if old_pixmap != 0 (
// Free old pixmap to avoid memory leaks
xcb::free_pixmap(conn, old_pixmap);
)
)
// Set new pixmap as root window background
fn set_root_pixmap(conn: &Connection, pixmap: Pixmap) (
xcb::change_window_attributes(conn, root, CW_BACK_PIXMAP, &[pixmap]);
xcb::clear_area(conn, false, root, 0, 0, 0, 0);
// Set _XROOTPMAP_ID and ESETROOT_PMAP_ID atoms for compositors
set_root_pixmap_atoms(conn, pixmap);
) The daemon uses a single-threaded Tokio runtime for efficient async I/O:
While garbg is not a full compositor, it provides wallpaper rendering for the gar window manager. For transparency effects in garbar or other applications, garbg sets the _XROOTPMAP_ID atom that compositors like picom read to composite the desktop background.
garbg integrates with gar's workspace system to provide per-workspace wallpapers:
garbg queries monitors via RandR and supports two multi-monitor modes:
// Per-monitor wallpaper setting
fn set_per_monitor(image: &Image, monitors: &[Monitor], mode: ScaleMode) (
for monitor in monitors (
let scaled = scale_image(image, monitor.width, monitor.height, mode);
let pixmap = create_pixmap_from_image(&scaled);
set_monitor_pixmap(monitor, pixmap);
)
)
// Span mode - single pixmap across all monitors
fn set_span(image: &Image, monitors: &[Monitor], mode: ScaleMode) (
let total_width = monitors.iter().map(|m| m.width).sum();
let max_height = monitors.iter().map(|m| m.height).max();
let scaled = scale_image(image, total_width, max_height, mode);
let pixmap = create_pixmap_from_image(&scaled);
set_root_pixmap(pixmap);
) garbg is managed by systemd as part of the gar session:
garbg provides full animated GIF support with frame timing:
// Animation loop for GIF wallpapers
fn run_animation_loop(gif: &GifData, max_fps: u32) (
let min_frame_time = Duration::from_secs_f64(1.0 / max_fps as f64);
loop (
for frame in &gif.frames (
let frame_start = Instant::now();
// Render this frame to the root pixmap
render_frame(frame);
// Calculate actual delay (respect GIF timing but cap at max_fps)
let gif_delay = Duration::from_millis(frame.delay_ms as u64);
let actual_delay = gif_delay.max(min_frame_time);
// Sleep until next frame
let elapsed = frame_start.elapsed();
if elapsed < actual_delay (
thread::sleep(actual_delay - elapsed);
)
)
)
) Animated WebP and APNG are also supported:
garbg uses adaptive frame timing to balance smoothness and CPU usage:
Animation performance optimizations:
garbg supports five scaling modes to handle images of any size:
Scales the image to completely fill the screen, cropping if necessary. The image aspect ratio is preserved. Best for wallpapers that are close to your screen's aspect ratio.
// Fill: scale to cover, crop excess
fn scale_fill(image: &Image, screen_w: u32, screen_h: u32) -> Image (
let scale = f64::max(
screen_w as f64 / image.width() as f64,
screen_h as f64 / image.height() as f64,
);
let scaled = image.resize((image.width() as f64 * scale) as u32,
(image.height() as f64 * scale) as u32);
// Center crop to screen dimensions
scaled.crop_center(screen_w, screen_h)
) Scales the image to fit entirely within the screen, adding letterbox/pillarbox bars if needed. The image aspect ratio is preserved. Best when you want to see the entire image.
Stretches the image to exactly fill the screen dimensions. The image aspect ratio is not preserved, which may cause distortion. Best for abstract or pattern-based wallpapers.
Displays the image at its original size, centered on screen. If the image is larger than the screen, it is cropped. If smaller, it is surrounded by the background color. Best for pixel-perfect display of specific-sized artwork.
Tiles the image across the screen at its original size. Best for seamless patterns or textures designed to be tiled.
garbg supports multiple source types for loading wallpapers. Sources can be single files, directories (for slideshows), HTTP URLs, or GitHub repositories.
The simplest source type - a path to an image file on disk:
Supported formats: JPEG, PNG, WebP, GIF, BMP, TIFF, APNG
A directory path enables slideshow mode with all images in the directory:
Directory scanning is recursive by default. Images are sorted alphabetically unless --shuffle is specified.
Load wallpapers directly from HTTP/HTTPS URLs:
HTTP images are cached locally after first download. Cache location is ~/.cache/garbg/http/.
If an HTTP URL returns an HTML page with directory listing, garbg parses the links to find images:
garbg has native support for loading wallpapers from GitHub repositories using the github:// protocol:
github://owner/repo/path/to/directory
# Examples:
github://catppuccin/wallpapers/landscapes
github://dharmx/walls/anime
github://Gingeh/wallpapers garbg also accepts standard GitHub web URLs and converts them internally:
GitHub API has rate limits for unauthenticated requests (60/hour). garbg caches API responses to minimize requests. For heavy usage, set the GITHUB_TOKEN environment variable.
Remote sources are cached to ~/.cache/garbg/ for faster subsequent access:
Slideshow state is persisted to ~/.local/state/garbg/playlist.json so position is retained across daemon restarts:
// ~/.local/state/garbg/playlist.json
(
"source": "/home/user/Pictures/Wallpapers",
"source_type": "directory",
"images": [
"forest-morning.jpg",
"mountain-sunset.png",
"ocean-waves.jpg"
],
"current_index": 12,
"mode": "fill",
"shuffle": true,
"shuffle_order": [5, 2, 12, 0, 8, ...]
) garbgctl is the command-line control tool for the garbg daemon. It sends commands over a Unix domain socket using JSON protocol.
| Option | Type | Default | Description |
|---|---|---|---|
set SOURCE | - | - | Set wallpaper from file, directory, URL, or GitHub |
next | - | - | Next wallpaper in slideshow playlist |
prev | - | - | Previous wallpaper in slideshow playlist |
random | - | - | Random wallpaper from current source |
pause | - | - | Pause animations and slideshow |
resume | - | - | Resume animations and slideshow |
toggle | - | - | Toggle pause state |
status | - | - | Get current wallpaper status as JSON |
reload | - | - | Reload configuration from disk |
list SOURCE | - | - | List wallpapers from a source |
query monitors | - | - | List connected monitors |
query current | - | - | Get current wallpaper info |
clear-cache | - | - | Clear the image cache |
Bind garbgctl commands to keys in gar's init.lua:
gar.keybinds = (
-- Cycle wallpapers with Super+W
( mods = ( "Mod4" ), key = "w", action = "spawn", args = "garbgctl next" ),
-- Previous wallpaper with Super+Shift+W
( mods = ( "Mod4", "Shift" ), key = "w", action = "spawn", args = "garbgctl prev" ),
-- Random wallpaper with Super+Alt+W
( mods = ( "Mod4", "Mod1" ), key = "w", action = "spawn", args = "garbgctl random" ),
-- Toggle pause with Super+P
( mods = ( "Mod4" ), key = "p", action = "spawn", args = "garbgctl toggle" ),
) garbg's IPC uses JSON over a Unix domain socket at $XDG_RUNTIME_DIR/garbg.sock. Each message is a single JSON object terminated by a newline.
$XDG_RUNTIME_DIR/garbg.sock
# Typically: /run/user/1000/garbg.sock Commands are JSON objects with a "command" field (snake_case) and command-specific fields:
// Set wallpaper with full options
(
"command": "set",
"source": "/home/user/Pictures/wallpaper.jpg",
"mode": "fill",
"monitor": null,
"interval_secs": null,
"shuffle": false,
"animate": true,
"max_fps": 60,
"span": false
)
// Set workspace-specific wallpaper
(
"command": "set_workspace",
"workspace": 3,
"source": "/home/user/Pictures/workspace3.jpg",
"mode": "fill"
)
// Set monitor-specific wallpaper
(
"command": "set_monitor",
"monitor": "DP-1",
"source": "/home/user/Pictures/main-monitor.jpg",
"mode": "fill"
)
// Navigation commands
("command": "next", "monitor": null)
("command": "prev", "monitor": null)
("command": "random", "monitor": null)
// Playback control
("command": "pause")
("command": "resume")
("command": "toggle")
// Status and queries
("command": "status")
("command": "reload")
("command": "list", "source": "/home/user/Pictures")
("command": "clear_cache")
("command": "query_monitors")
("command": "query_current")
// Event subscription
("command": "subscribe", "events": ["wallpaper_changed", "slideshow_advanced"])
("command": "unsubscribe", "events": ["wallpaper_changed"]) | Option | Type | Default | Description |
|---|---|---|---|
set | - | - | Set wallpaper with source, mode, monitor, interval, shuffle, animate, max_fps, span options |
set_workspace | - | - | Set wallpaper for specific workspace (workspace, source, mode) |
set_monitor | - | - | Set wallpaper for specific monitor (monitor, source, mode) |
next | - | - | Next wallpaper in playlist (optional monitor filter) |
prev | - | - | Previous wallpaper in playlist (optional monitor filter) |
random | - | - | Random wallpaper from source (optional monitor filter) |
pause | - | - | Pause animations and slideshow |
resume | - | - | Resume animations and slideshow |
toggle | - | - | Toggle pause state |
status | - | - | Get current status |
reload | - | - | Reload configuration |
list | - | - | List wallpapers from source |
clear_cache | - | - | Clear image cache |
subscribe | - | - | Subscribe to events (array of event types) |
unsubscribe | - | - | Unsubscribe from events |
query_monitors | - | - | Get list of connected monitors |
query_current | - | - | Get current wallpaper information |
// Success response
("success": true)
// Success with data
(
"success": true,
"data": (
"source": "/home/user/Pictures/Wallpapers",
"current": "forest.jpg",
"index": 5,
"total": 47
)
)
// Error response
("success": false, "error": "Source not found: /invalid/path") Clients can subscribe to events for reactive updates:
| Option | Type | Default | Description |
|---|---|---|---|
wallpaper_changed | - | - | Fired when wallpaper changes (monitor, source, workspace) |
source_updated | - | - | Fired when source content changes (source, count) |
animation_state | - | - | Fired when animation state changes (playing boolean) |
slideshow_advanced | - | - | Fired when slideshow advances (current, total, source) |
error | - | - | Fired on error (message, optional context) |
// WallpaperChanged event
(
"event": "wallpaper_changed",
"monitor": "DP-1",
"source": "/home/user/Pictures/mountain.jpg",
"workspace": 1
)
// SlideshowAdvanced event
(
"event": "slideshow_advanced",
"current": 13,
"total": 47,
"source": "forest-morning.jpg"
)
// AnimationState event
(
"event": "animation_state",
"playing": true
)
// Error event
(
"event": "error",
"message": "Failed to load image",
"context": "HTTP 404: Not Found"
) #!/usr/bin/env python3
import socket
import json
import os
sock_path = os.path.join(os.environ.get('XDG_RUNTIME_DIR', '/tmp'), 'garbg.sock')
def send_command(cmd):
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(sock_path)
sock.sendall((json.dumps(cmd) + '\n').encode())
response = sock.recv(4096).decode()
return json.loads(response)
# Set wallpaper
result = send_command((
"command": "set",
"source": "/home/user/Pictures/wallpaper.jpg",
"mode": "fill"
))
print("Success:", result["success"])
# Get status
status = send_command(("command": "status"))
print("Current:", status["data"]["current"])
# Next wallpaper
send_command(("command": "next")) garbg responds to Unix signals for lifecycle management:
Check if another instance is running and verify the socket/PID file:
Verify the root window pixmap is set correctly:
This usually means the pixmap was freed prematurely. In daemon mode, pixmaps should persist:
Check if the animation is too complex or max_fps is too high:
Check API rate limits and network connectivity:
Verify gar is sending workspace events and garbg is subscribed:
Check RandR monitor configuration and verify garbg sees all monitors:
If remote images are outdated or corrupted, clear the cache:
Enable verbose logging to diagnose issues:
Verify the image format is supported and not corrupted:
journalctl --user -u garbg.serviceRUST_LOG=debug garbg daemon