owe plays mp4, GIF, and still images as the Hyprland background.
It joins the Omarchy theme switch through existing contracts only.
It changes no packaged Omarchy file.
This project is experimental. Use it at your own risk. See the performance follow-up document for known issues and incomplete checks.
owed— policy daemon. It watches the background symlink, tracks Hyprland, lock, idle, DPMS, and battery state, and commands the renderer.owe-render— C renderer. It owns all layer-shell surfaces. It decodes video once throughlibmpvwith hardware decode and draws N outputs. It draws stills as static GL textures with no frame loop.owe— CLI. It controls every daemon and renderer function.owe-idle— hypridle helper. It pauses on idle and resumes on activity.
Install the build and runtime packages on Omarchy first. Packages already installed are skipped:
omarchy pkg add meson ninja gcc pkgconf wayland wayland-protocols libglvnd \
libepoxy mpv ffmpeg systemd-libs socat pythonThen build and install owe:
./packaging/install.sh
owe status
owe set ~/Videos/loop.mp4
owe pause
owe resume
owe always-animate onowe set <path> # Update the Omarchy background symlink
owe next # Cycle to next theme background
owe current # Show current background name
owe refresh # Re-read the background symlink now
owe pause # Pause video manually
owe resume # Clear manual pause
owe always-animate on|off # Force animation regardless of policy
owe status # Daemon status as JSON
owe config # Effective config as JSON
owe render-status # Renderer status as JSON
owe render <json> # Send raw JSON to the renderer
owe raw <json> # Send raw JSON to the daemon
owe reload-config # Reload config.toml
owe render-restart # Restart the renderer process
owe shutdown # Stop the daemonSockets live under $XDG_RUNTIME_DIR/owe/. One JSON message uses one line.
Each command gets one JSON reply line.
Commands:
{"cmd":"hello"}— reply{"status":"ok","version":1}.{"cmd":"status"}— full daemon state: source path and kind, loaded path and kind, job and failure state, pause state, reason, Hyprland flags, battery, lock, and renderer liveness.{"cmd":"config"}— effective config values.{"cmd":"set","path":"/abs/file"}validates a local file and atomically updates the Omarchy background symlink.{"cmd":"refresh"}— re-resolve the symlink and load now.{"cmd":"pause"}— set manual pause.{"cmd":"resume"}— clear manual pause.{"cmd":"idle-pause"}— set idle pause. It does not clear a manual pause.{"cmd":"idle-resume"}— clear idle pause.{"cmd":"always-animate","value":true}— force or release animation.{"cmd":"reload-config"}— reload~/.config/owe/config.toml.{"cmd":"render-status"}— proxy the renderer status reply.{"cmd":"render-restart"}— restartowe-renderand reload current media.{"cmd":"shutdown"}— stop the daemon.
Commands:
{"cmd":"hello"}— reply{"status":"ok","version":1}.{"cmd":"load","path":"/abs/file","kind":"video|still"}— load media.{"cmd":"pause"}— pause decode. The last frame stays presented.{"cmd":"resume"}— resume decode.{"cmd":"stop"}— unload all media.{"cmd":"status"}reports the path, kind, pause state, outputs,time_pos,hwdec, and playbackerror.{"cmd":"fade","ms":250}— set the still fade length.
# Status through the raw socket
printf '{"cmd":"status"}\n' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/owe/owed.sock
# Load a video direct on the renderer
printf '{"cmd":"load","path":"/tmp/loop.mp4","kind":"video"}\n' \
| socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/owe/render.sock
# Pause through the daemon
owe raw '{"cmd":"pause"}'By default the wallpaper keeps moving. The engine pauses only for real interruptions: a fullscreen window, the lock screen, monitor DPMS off, and sleep. Reasons in priority order:
manualpause fromowe pause.sleeppauses playback before suspend.always-animatebypasses automatic pause rules.idlepause from theowe-idlehelper.lockedfollows logind session state.dpms-offapplies when every connected monitor reports a known off state. Missing DRM data falls back to Hyprland state.batteryshows a still poster while on battery. Off by default, enable withbattery_poster = true.blocklistwhile a listed process runs. Off by default.fullscreenapplies when fullscreen windows cover all active outputs. Hidden workspaces do not count.occupiedapplies when all active outputs have visible windows. Off by default, enable withoccupied_workspace = trueto save power on busy desktops.
Idle pauses are optional. Wire the owe-idle helper into hypridle
and it sets the idle pause. owe resume clears only the manual pause,
so an idle resume never releases a manual pause.
To force motion regardless of policy, run owe always-animate on.
Each GIF becomes cached muted mp4 through one ffmpeg pass at first
select. Later selects serve from ~/.cache/owe/gif/. With
battery_poster = true, battery mode shows a poster frame from the
same cache.
The GIF and poster cache keeps the newest 512 MiB by default. Set
cache_max_mb in [transcode] to change the budget. 0 disables
eviction.
Playback disables audio with audio=no and aid=no.
Generated media contains only video. Source files remain intact.
- Video renders only when
libmpvsignals a new frame. The render rate tracks the media frame rate, not the display refresh rate. - Video draws straight into the window framebuffer. No intermediate copy per frame per output.
- Hardware decode is on by default (
hwdec=auto-safe). Override with theOWE_HWDECenvironment variable. libmpvruns without its lua scripts. The ytdl hook, stats overlay, console, and the other scripts only add threads and memory. Advanced mpv tuning usesOWE_MPV_OPTIONS, a semicolon separated list such asdemuxer-max-bytes=16MiB;hwdec-extra-frames=1.- Stills decode once at the size the cover crop needs, upload one texture, and then stop the frame loop. Paused video stops the loop too.
- Layer surfaces use
wp_fractional_scale_v1andwp_viewporterwhen the compositor offers them. A 1.25 scale output draws 1.25x pixels, not the 2x thatwl_output.scalealone would ask for. Without the protocols the renderer falls back to the integerwl_output.scale. - Stills and video cover the output. Aspect ratio is preserved with a
center crop, like
PreserveAspectCropin the Omarchy shell. - Poster extraction and GIF transcode run in a worker thread. The
daemon event loop never blocks on
ffmpeg. - The daemon sends pause and resume only when playback state changes.
owed supervises owe-render. A killed or crashed renderer is restarted
and the current media reloads. The daemon holds a flock on
$XDG_RUNTIME_DIR/owe/owed.lock, so a second instance refuses to start.
Copy config/config.toml to ~/.config/owe/config.toml and run
owe reload-config. The installer does this once and never overwrites
an existing file.
meson setup build
ninja -C build
meson test -C buildThe tests cover IPC framing, quoted paths, worker cancellation, atomic cache publication, display state, actual GIF conversion, still decode for PNG, JPEG, and AVIF, and pause policy after a rejected load. To run sanitizer checks, use a separate build directory:
meson setup build-sanitize -Db_sanitize=address,undefined -Db_lundef=false -Dwerror=true
meson test -C build-sanitize --print-errorlogstest/owe-live-test requires a running test daemon and a video wallpaper.
To check live playback and IPC, run build/test/owe-live-test.
Build and install deps: meson, ninja, gcc, pkgconf, wayland,
wayland-protocols, libglvnd, libepoxy, mpv, ffmpeg, systemd-libs,
socat, python.
Runtime deps: mpv, ffmpeg, socat.
Docs: docs/architecture.md, docs/theme-contract.md, docs/benchmarks.md.