owe plays video files, GIF, and still images as the Hyprland background.
Video containers include mp4, mkv, webm, mov, avi, mpeg, ts, wmv, flv, ogv, and 3gp.
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 while a moving background plays. It decodes video once throughlibmpvwith hardware decode and draws N outputs. It draws a poster still as a static GL texture only in battery poster mode. The daemon stops it while a still background shows and gives the layer back to the shell.owe— CLI. It controls every daemon and renderer function.owe-idle— hypridle helper. It pauses on idle and resumes on activity.
From the AUR:
omarchy pkg add owe
systemctl --user enable --now owed.serviceTo install the optional theme hook, run:
omarchy hook install theme-set /usr/share/owe/10-owe-syncThe daemon also watches the background symlink directly. The hook requests a refresh after the theme changes.
Or build from source, as below.
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 python cmake qt6-declarativeThen 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 intro <video> # Play a one-shot intro video and wait
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 daemon
owe --version # Show the build versionThe daemon accepts --socket PATH and --verbose.
The renderer accepts the same options.
The CLI accepts --socket PATH before the command.
The renderer socket and lock feed socket use the daemon socket directory.
owed --socket "$XDG_RUNTIME_DIR/owe-test/owed.sock" --verbose
owe --socket "$XDG_RUNTIME_DIR/owe-test/owed.sock" statusOne daemon can run in each runtime directory. An intro requires a still background and ends after 30 seconds at most. The CLI accepts an absolute or relative intro path.
Sockets 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":"intro","path":"/abs/file.mp4"}— play a one-shot intro video over a still background, muted. Reply means it started. Cancel withintro-stop.{"cmd":"intro-status"}— reply{"running":bool,"result":"running|ok|error"}.{"cmd":"intro-stop"}— cancel a running intro.{"cmd":"shutdown"}— stop the daemon.
Commands:
{"cmd":"hello"}— reply{"status":"ok","version":1}.{"cmd":"load","path":"/abs/file","kind":"video|still"}— load media."once":trueand"mute":trueload a one-shot intro.- Add
"async":trueto a still load for an immediate acknowledgement. Pollstatusuntilreadyis true andpathmatches the request. An asynchronous still load has a 30-second deadline. A synchronous still load has a four-second deadline. {"cmd":"cancel-load"}cancels a pending still load and retains the current 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.{"cmd":"feed"}— start muted video output to the lock feed.{"cmd":"feed-stop"}— stop the lock feed and release its buffers.{"cmd":"skip","outputs":["DP-1"]}— stop desktop swaps on the named outputs.
The daemon uses asynchronous still loads for posters and shell fallback.
Its media_ready field confirms readiness for loaded_path.
The renderer ignores transition paths that do not name readable local files.
Malformed replies terminate their connection.
Reply deadlines include all fragments of a reply.
# 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 or Hyprland session state.dpms-offapplies when every connected monitor reports a known off state. Missing DRM data falls back to Hyprland state.batteryholds or replaces playback while on battery. Off by default. Setbattery_mode = "pause"to hold the current frame, orbattery_mode = "poster"to show a still poster.battery_poster = trueis the same as poster mode.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 bypass automatic pause rules, run owe always-animate on.
Manual pause and sleep still take precedence.
A locked session can keep video moving through the muted lock feed instead
of the desktop layer. The feed honors manual pause, sleep, DPMS, idle,
blocklist and battery pause/poster settings. The animation override bypasses
the automatic rules, but the feed still stops for manual pause, sleep and DPMS.
Fullscreen and occupied desktop windows do not stop the lock feed.
A lock screen can import Owe.LockFeed and display a LockFeed item;
it should provide its own still fallback when the feed is inactive.
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. The current conversion and its cached source are retained even when
they exceed the budget; older unused entries are evicted first.
A video wallpaper plays its audio track through the default audio output. One renderer serves every monitor, so the track plays once. Generated GIF media is silent. Source files remain intact.
- Stills never start the renderer. While a still background shows,
owedenables the shell background plugin, stopsowe-render, and keeps only the 4 MiB daemon. A video or GIF starts the renderer again. - Video renders only when
libmpvsignals a new frame. The render rate tracks the media frame rate, not the display refresh rate. - A switch from a still to a video crossfades. The renderer draws the outgoing still over the incoming video and fades it out.
- Video draws straight into the window framebuffer. No intermediate copy per frame per output.
- Video redraws wait for the compositor's frame callback. A blanked output stops the draw loop instead of stalling the event loop, and an output covered by a fullscreen window stops swapping buffers.
- 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. - The renderer returns freed large allocations to the OS. glibc keeps them resident by default, which inflates RSS by about 30 MiB with media buffers.
- 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.
For a black background, follow the diagnostic steps. Software decode still uses the GPU display path. The guide covers GPU logs, swap failures, and an optional DRM DPMS diagnostic override.
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.
The daemon uses $XDG_CONFIG_HOME/owe/config.toml when XDG_CONFIG_HOME is set.
An invalid reload preserves the current configuration.
The journal identifies the file and line of a syntax error.
Unknown keys produce a warning.
| Section | Key | Default | Accepted values |
|---|---|---|---|
pause |
fullscreen |
true |
Boolean |
pause |
occupied_workspace |
false |
Boolean |
pause |
battery_poster |
false |
Boolean |
pause |
battery_mode |
"play" |
"play", "pause", "poster" |
pause |
blocklist |
[] |
At most 16 process names, at most 63 bytes per name |
transcode |
gif_fps |
20 |
5 to 50 |
transcode |
gif_crf |
20 |
0 to 51 |
transcode |
max_width |
2560 |
320 to 16384 |
transcode |
max_height |
1440 |
200 to 16384 |
transcode |
cache_max_mb |
512 |
0 to 65536 MiB |
render |
fade_ms |
250 |
0 to 2000 milliseconds |
Numeric values outside these limits use the nearest limit.
battery_poster = true takes precedence over battery_mode.
Set battery_poster = false to control battery behavior through battery_mode alone.
The blocklist accepts single-line and multiline arrays.
Quoted names can contain # and commas.
Oversized arrays and names cause a load error.
Each configuration line has a limit of 510 bytes before its newline.
To inspect the effective configuration and recent errors, run:
owe config
journalctl --user -u owed.service -n 100 --no-pagerTo remove a source installation, run:
./packaging/uninstall.shThe script restores the shell background plugin. It retains the configuration and media cache.
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. The lock feed tests cover feed restarts, client pause state, frame ownership, DPMS, and transitions to still images.
The transition test reads actual OpenGL pixels from two offscreen output buffers with libmpv video playback. It checks the blend, delayed frames, pause, cancellation, texture cleanup, unavailable outputs, and transition image timeouts.
meson test -C build transition --print-errorlogsTo build and test the QML lock feed plugin, use CMake with Qt 6 Quick and Qt 6 Test:
cmake -S qml-plugin -B build-qml -DBUILD_TESTING=ON
cmake --build build-qml
ctest --test-dir build-qml --output-on-failureThe AUR package installs the plugin to /usr/lib/qt6/qml/Owe/LockFeed.
The local installer uses ~/.local/lib/qt6/qml/Owe/LockFeed; add
~/.local/lib/qt6/qml to the consuming application's QML_IMPORT_PATH.
Installing the module does not change the lock screen's QML layout.
The plugin tests use an offscreen Qt platform and local sockets. They cover reconnects, fragmented messages, and descriptor ownership.
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, cmake, qt6-declarative.
Runtime deps: mpv, ffmpeg, wayland, libglvnd, libepoxy, systemd-libs, socat, qt6-declarative.
Docs: docs/architecture.md, docs/theme-contract.md, docs/benchmarks.md.