Skip to content

Beta · 1.0.0-beta.1

veil: A wallpaper daemon for Wayland compositors.

veil shows a picture or a looping video on each monitor, can give every monitor a wallpaper of its own, pauses a video nobody can see, and brings back what each monitor showed after a restart.

It is written in Rust, plays videos with GStreamer, and is controlled from the command line, a configuration file, or a socket that scripts and status bars can use.

Version
1.0.0-beta.1
License
PolyForm Strict
Written in
Rust
Needs
wlr-layer-shell
veil · quick start
# draws on every monitor (black until a wallpaper is set)
veil daemon &

# a video or a picture; saved, and shown again after a restart
veil set --input ~/Videos/waves.mp4

# what each monitor shows
veil status

Once veil is installed, in your Wayland session. To start it with your session, use its systemd user unit instead of veil daemon &.

Pictures and video, on every monitor.

Supported means implemented, covered by automated tests and checked in a compositor. Experimental means it works, with a limit you should know about.

  • Pictures

    Supported

    PNG, JPEG, WebP, BMP, TIFF, GIF and the other formats of the image crate. A picture is drawn at the monitor’s physical resolution, then the decoded copy is freed.

  • Video, looped

    Supported

    Without audio, in any format the installed GStreamer plugins play. Checked: H.264, HEVC, VP8, VP9 and AV1, and ten containers.

  • Every monitor its own

    Supported

    Every monitor, or the ones you name, each with a wallpaper of its own, and monitors plugged in later. A monitor keeps its wallpaper by name.

    with a video, a monitor change is experimental

  • Pauses what nobody sees

    Supported

    Behind a fullscreen window a video keeps its frame on screen and decodes nothing. A locked screen or a monitor that is off stops it after two seconds.

    sway · experimental on Hyprland

  • Remembers

    Supported

    The wallpaper of each monitor is saved, and shown again after a restart. veil set --no-persist skips that.

  • Never a blank in between

    Supported

    A new wallpaper replaces the old one only once it is ready. A video takes over when its first frame is on screen; a file that cannot be shown leaves the old wallpaper alone.

  • HiDPI and fractional scaling

    Supported

    Pictures are drawn at the monitor’s physical resolution. Checked pixel by pixel at scales 1, 1.25, 1.5 and 2.

  • Controlled from anywhere

    Supported

    set, clear, pause, resume, toggle, status, outputs, quit and ping, with stable exit statuses, plus a control socket for scripts and status bars.

Five ways to fit a picture to a monitor.

--mode says how a wallpaper is fitted to its monitor, for pictures and videos alike. The daemon’s --mode and --bg-color are also the defaults of veil set.

  • --mode fill

    Covers the whole monitor, keeping the aspect ratio. The overflow is cropped evenly from both sides. The default.

  • --mode fit

    The whole picture is visible, keeping the aspect ratio. Bars of --bg-color fill the rest.

  • --mode stretch

    Scaled to exactly the monitor’s size; the aspect ratio is not kept.

  • --mode center

    Native size, one pixel per device pixel, centred. Cropped if larger than the monitor, surrounded by --bg-color if smaller.

  • --mode tile

    Native size, repeated from the top-left corner. Videos fall back to center.

One command for every monitor, or for one.

Run the commands on two sample monitors. What the monitors show and what veil status prints are an illustration, set in the format veil uses.

Samples: ~/Pictures/sunset.jpg, ~/Pictures/dawn.jpg and the video ~/Videos/sunrise.mp4.

DP-3
DP-3 · 1920x1080 at scale 1
HDMI-A-1
HDMI-A-1 · 2048x1152 at scale 1.25
veil status
veil 1.0.0-beta.1 (pid 4218), running for 7s; videos pause on: fullscreen
default: ~/Pictures/sunset.jpg (fill, #000000)
DP-3: playing ~/Videos/sunrise.mp4 (fill, #000000); 1920x1080 at scale 1; video 1920x1080, 25 fps
HDMI-A-1: showing ~/Pictures/sunset.jpg (fill, #000000); 2048x1152 at scale 1.25

A video nobody can see costs next to nothing.

Under Hyprland and sway, veil follows the compositor’s IPC and pauses a video nobody can see. On any compositor, a video whose frames the compositor does not take is stopped and its pipeline freed.

DP-3
What is in front of the video
CPU of the daemon
19.5 %
veil status says
playing

The video plays and decodes every frame.

CPU of the daemon
≈ 0 %
veil status says
paused (hidden by a window)

Hidden for a second, the video keeps its frame on screen and decodes nothing. It plays on from the same frame when it can be seen again.

CPU of the daemon
≈ 0 %
veil status says
stopped while the compositor does not show it

The compositor takes no frames for 2 seconds, so veil lets go of the video. It starts again from its beginning.

CPU is the daemon’s share of one core for a 1080p, 30 fps H.264 video decoded on an NVIDIA card. Behind a fullscreen window it measured at most one tick of CPU time in 5 seconds.

What a wallpaper costs.

Measured on the build of 2026-10-19 in a headless sway, one 1920x1080 output in fill, 60 seconds after a 5-second warm-up, on an Intel Core i5-11400F with an NVIDIA GeForce RTX 3060 Ti. Nothing has been measured in a real session yet.

CPU, frames shown, memory and threads of the daemon per kind of wallpaper. The dashed line in the bars is one full core.
WallpaperCPU, share of one coreFrames shown per secondMemory (RSS)Threads
A 1920x1080 PNGno decoder
0.02 %
—20 MiB1
H.264, 1080p, 30 fpsnvh264dec (GPU)
19.5 %
30.0 of 30203 MiB15
H.264, 1080p, 60 fpsnvh264dec (GPU)
39.7 %
59.8 of 60203 MiB15
VP9, 1080p, 60 fpsnvvp9dec (GPU)
37.9 %
59.9 of 60251 MiB15
H.264, 2160p, 60 fpsnvh264dec (GPU)
136 %
60.2 of 60297 MiB15
H.264, 1080p, 30 fpsavdec_h264 (software)
24.5 %
30.0 of 30161 MiB22
H.264, 2160p, 60 fpsavdec_h264 (software)
174 %
60.1 of 60458 MiB22
  • A still picture costs next to nothing after it is drawn: one thread, and one tick of CPU time in a minute.
  • A video costs CPU in proportion to its pixels per second, whatever the codec. On NVIDIA the decoder on the GPU costs about 1 %; the rest is converting every frame from NV12 to RGB on the CPU.
  • veil’s design goal of under 1 % for a video is not met on this hardware. On Intel and AMD, VA-API could avoid that conversion; that has not been measured.
  • Memory, threads and file descriptors stay flat over ten minutes of looping and 500 wallpaper switches, with one allocator arena (MALLOC_ARENA_MAX=1, which the Nix package and the systemd unit set).

The command line, a file, or a socket.

Every veil command is a client of the daemon’s socket, and so can be anything else: a status bar, a key binding, a script in any language.

Command line

Every command takes --help and has a man page. veil set returns once the wallpaper is on screen, and exits with status 1, leaving the old one, if the file cannot be shown.

terminal
# every monitor
veil set --input ~/Videos/waves.mp4

# one monitor; the others keep theirs
veil set --input ~/Pictures/photo.jpg --output DP-3

# pause videos until `veil resume`
veil pause

# what each monitor shows, as JSON for scripts
veil status --json
Usage

Configuration file

~/.config/veil/config.toml holds the daemon’s options and the wallpaper of each monitor. veil config check reads it as the daemon would, and reports a wrong key with its line and column.

config.toml
[defaults]
mode = "fill"           # --mode
bg_color = "#000000"    # --bg-color
pause_on = "covered"    # --pause-on

[output."DP-3"]
input = "~/Videos/waves.mp4"
mode = "fit"

[output."*"]            # every monitor without a section of its own
input = "~/Pictures/wallpaper.png"
Configuration

Control socket

One JSON request and one JSON response per connection, only for processes of your own user. Fields, commands and error codes are only ever added, never renamed or removed.

socket
sock="$XDG_RUNTIME_DIR/veil-${WAYLAND_DISPLAY##*/}.sock"
echo '{"cmd":"ping"}' | socat - UNIX-CONNECT:"$sock"
# {"version":1,"status":"ok"}
echo '{"cmd":"toggle"}' | socat - UNIX-CONNECT:"$sock"
# {"version":1,"status":"ok","result":{"paused":true}}
Control socket

What was run, and by whom.

From veil’s own support matrix. Untested means nobody has tried it yet, not that it is known to fail.

Compositors

  • sway 1.12 (headless)

    Every check of the headless tests and the measurements, 2026-10-19.

    Verified
  • Hyprland 0.56.2

    Images, H.264 video on two monitors and the automatic pause, 2026-10-20. Smoothness, lock, DPMS, logout and crash recovery are untested.

    Reported working
  • river, niri, Wayfire, labwc and other wlr-layer-shell compositors

    Should work. No automatic pause: every video counts as visible.

    Untested
  • KDE Plasma (KWin)

    Has wlr-layer-shell; nobody has tried.

    Untested
  • GNOME (Mutter)

    No wlr-layer-shell.

    Unsupported

Graphics

  • NVIDIA (NVDEC)

    RTX 3060 Ti, driver 595.91.07, sway and Hyprland. Every video frame is converted on the CPU.

    Reported working
  • Intel and AMD (VA-API)

    The zero-copy path is not verified on any hardware.

    Untested
  • Software decoding

    In the tests, and by taking the hardware decoders out.

    Reported working

Architectures

  • x86_64

    The tarball in a container of six distributions, 2026-10-19.

    Reported working
  • aarch64

    The release workflow builds it and runs the tests there; it was never run with a compositor.

    Untested

Distributions

  • Debian 12 and 13, Ubuntu 22.04 and 24.04, Fedora 44, Arch Linux

    The prebuilt tarball in a headless sway, 2026-10-19. Animated GIFs need GStreamer 1.26 or newer.

    Reported working
  • NixOS (the flake)

    nix run and nix profile, with sway and Hyprland. 2026-10-20 and 2026-11-04.

    Verified

The rough edges, stated up front.

veil is a beta. This is what is known to be wrong today; the details, the measurements and the options considered are in the known issues.

  • A monitor change while a video plays crashes the daemon

    The bug is in GStreamer’s waylandsink: it segfaults when the compositor reports a change of any monitor. Pictures are not affected. Run veil under its systemd unit: it is back with its wallpapers within about three seconds.

    Known issue 1

  • A video costs CPU on NVIDIA

    About 20 % of a core for 1080p at 30 fps, because every frame is converted to RGB on the CPU.

    Known issue 7

  • Memory grows with video switches without one allocator arena

    The Nix package and the systemd unit set MALLOC_ARENA_MAX=1; set it yourself when you start a binary built from source by hand.

    Known issue 3

Prebuilt binaries, Nix, or from source.

veil needs a Wayland compositor with wlr-layer-shell, and GStreamer 1.x with the plugin sets base, good, bad and libav at run time. XDG_RUNTIME_DIR must be set, as it is in any desktop session.

Prebuilt

Each release has a tarball for x86_64 and one for aarch64 Linux: the binary, the systemd user unit, the man pages, the shell completions and an install script. Prebuilt binaries are still experimental.

prebuilt
version=1.0.0-beta.1
tarball=veil-$version-$(uname -m)-unknown-linux-gnu.tar.gz
curl -LO https://github.com/ordreth/veil/releases/download/v$version/$tarball
curl -LO https://github.com/ordreth/veil/releases/download/v$version/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
tar -xzf "$tarball" && cd "${tarball%.tar.gz}"
./install.sh --prefix ~/.local

Each tarball has a build-provenance attestation: gh attestation verify "$tarball" --repo ordreth/veil.

Nix

The flake’s package is wrapped with its own GStreamer plugins and a GStreamer registry of its own, and includes the systemd user unit, the completions and the man pages.

nix
nix run github:ordreth/veil -- daemon     # try it without installing
nix profile add github:ordreth/veil       # install it

Because of the license, nixpkgs counts veil as unfree. The flake’s own packages allow it by themselves; with the overlay, allow it in your configuration.

From source

You need Rust 1.89 or newer, a C compiler and pkg-config, and the development files of Wayland, libxkbcommon and GStreamer. The install guide lists the packages for Arch, Debian, Ubuntu and Fedora.

terminal
git clone https://github.com/ordreth/veil && cd veil
cargo build --release --locked
install -Dm755 target/release/veil ~/.local/bin/veil

Started by hand, give a binary you built yourself MALLOC_ARENA_MAX=1, as in MALLOC_ARENA_MAX=1 veil daemon.

Free to use, source-available, not open source.

veil is licensed under the PolyForm Strict License 1.0.0, with additional permissions. You may read the code and use veil for any noncommercial purpose.

You may

  • Use veil for any noncommercial purpose.
  • Build veil from its source for your own use.
  • Redistribute the release tarballs, unchanged, byte for byte.
  • Quote up to 50 lines of the source, with attribution.
  • Use the name and the icon to refer to veil.

You may not

  • Distribute the source code, or a build of your own.
  • Change it, or make new works from it.
  • Use it commercially.
  • Use the name or the icon for another product or a modified version.

veil takes no pull requests, patches or issues. Reports of how it works on your machine are welcome in GitHub Discussions, and security problems privately, as SECURITY.md says.

The icon of veil: a window onto a dusk

Beta · 1.0.0-beta.1

Free to use, source-available, and not finished. What is known to be wrong is on this page; what you find on your machine is welcome in the discussions.