A native Wayland Matrix digital-rain screensaver for GNOME, rendered entirely
on the CPU via wl_shm (software framebuffer) — no OpenGL, no GPU, no
XWayland, no terminal.
It exists because GNOME on Wayland removed animated screensavers, and on this machine (4K display + NVIDIA + XWayland) every conventional option failed to render fullscreen. Going native + CPU-only was the only reliable path. See Why this exists for the full story.
The animation above is rendered by wlmatrix itself —
wlmatrix --gif docs/demo.gif(and--shotfor a still). No screen capture involved.
- Fills the screen with falling green "Matrix" rain — bright white leaders, green trails that fade out.
- True fullscreen as a real Wayland surface (no resize/compositor glitches).
- Dismisses instantly on any keypress, mouse movement, or click.
- Hidden mouse cursor while running (it's a fullscreen surface that exits the moment you move).
The screensaver itself is just the renderer. A small companion daemon
(wl-screensaver) decides when to show it, using GNOME/Mutter's idle timer.
classic (default) is green ASCII rain. neo swaps in half-width katakana
and adds real bloom — a glow only possible because we render true pixels
(a terminal/CSS Matrix can't). Enable it with style = "neo" in the config, or:
wlmatrix --style neoAdd depth for parallax — 2–3 rain layers at different sizes, speeds and
brightness composite into a volumetric "falling through code" effect:
wlmatrix --style neo --depth 3Hidden images ("operator view")
Hide a picture or a word in the rain — the Matrix "operator view", where a
face or message condenses out of the green flow. Point mask at any PNG (its
luminance × alpha becomes the image) or set mask_text to a line of text:
wlmatrix --mask docs/operator-mask.png --mask-contrast 0.6 # any PNG
wlmatrix --mask-text "WAKE UP" # or a line of text(The figure above is the bundled docs/operator-mask.png —
an original silhouette; point mask at any PNG of your own.)
The image is laid down as faint glyphs under the rain; because compositing is
additive, the bright heads brighten the masked cells as they fall through, so
the picture shimmers into view in motion rather than sitting there as a
still. Tune mask_intensity (0–1, default 0.85) for how strongly it reads —
lower it toward 0.5 for a subtler ghost on plain rain, or keep it high so it
survives the brighter neo look (bloom + depth raise the brightness floor).
For the full operator-view drama, add mask_contrast (0–1): it dims the rain
outside the figure so the silhouette reads as true figure-ground rather than
just brighter glyphs. 0.4–0.6 is a good range.
wlmatrix --mask docs/operator-mask.png --mask-contrast 0.6Three pieces work together:
| Component | Location | Role |
|---|---|---|
wlmatrix |
~/wlmatrix/ (this repo) → binary at ~/.local/bin/wlmatrix |
The renderer. Native Rust/Wayland app, CPU-rendered Matrix rain. Runs fullscreen, exits on input. |
wl-screensaver |
~/.local/bin/wl-screensaver |
Idle daemon (bash). Polls Mutter's idle time; launches the renderer after N ms idle and kills it when you return. |
wl-screensaver.service |
~/.config/systemd/user/wl-screensaver.service |
systemd user unit. Runs the daemon at login. |
user goes idle
│
▼
wl-screensaver (daemon) ──polls──▶ org.gnome.Mutter.IdleMonitor (D-Bus)
│ idle ≥ SAVER_IDLE_MS
▼
wlmatrix ──renders──▶ wl_shm framebuffer ──▶ Wayland compositor (fullscreen)
│ any input → app exits / daemon kills it
▼
back to desktop
winit— creates a native Wayland window and puts it fullscreen (Fullscreen::Borderless), delivers input events.softbuffer— gives a raw&mut [u32]framebuffer backed bywl_shm; we write0x00RRGGBBpixels andpresent(). No GPU involved.ab_glyph— rasterizes monospace glyphs once into a coverage cache; each frame we blit cached glyphs in the right color/brightness.- A tiny built-in xorshift RNG (no
randdependency). - ~30 fps loop driven by
ControlFlow::WaitUntil.
- Rust toolchain (
cargo). - A monospace font installed system-wide — found automatically via the OS font
database (preferring DejaVu/Liberation/Noto on Linux, Menlo/SF Mono on macOS,
Consolas/Cascadia on Windows). Override with
font = "/path"if you like. - Linux: a Wayland or X11 session (
echo $XDG_SESSION_TYPE). The idle daemon/service additionally needsgdbus(ships with glib) and a systemd user session. - macOS / Windows: nothing extra — see Platforms below.
The renderer is cross-platform. winit, softbuffer and ab_glyph all support
Linux (Wayland/X11), macOS and Windows, and wlmatrix deliberately uses no GPU
/ OpenGL, so it builds and runs on all three. CI builds a binary for each
(.github/workflows/build.yml); tagged releases
attach wlmatrix-x86_64-linux, wlmatrix-aarch64-macos,
wlmatrix-macos.app.zip (double-clickable bundle), and
wlmatrix-x86_64-windows.exe.
| Renders the rain | Exits on input | Auto-launch on idle | |
|---|---|---|---|
| Linux (Wayland/X11) | ✅ | ✅ | ✅ wl-screensaver daemon + systemd |
| Windows | ✅ | ✅ | ✅ wlmatrix.scr — a real screensaver |
| macOS | ✅ | ✅ | ⏳ not yet — run the app manually |
On Windows it's a proper screensaver: the same binary, renamed to
wlmatrix.scr, handles /s (run), /c (settings) and /p (the Settings-dialog
preview), and Windows launches it on idle — see below.
On macOS it runs as a fullscreen app for now; first-class integration (a
.saver bundle or a launchd idle agent) is the remaining piece.
Releases ship a double-clickable wlmatrix-macos.app.zip (Apple-Silicon).
Unzip it to get wlmatrix.app — drop it in /Applications or run it in place.
The app is not yet code-signed or notarized, so on first launch macOS Gatekeeper blocks it ("…cannot be opened because it is from an unidentified developer", or it offers to move it to the Trash). This is expected for any unsigned app — nothing is wrong with the file. Two ways past it:
- Right-click
wlmatrix.app→ Open, then confirm Open in the dialog (only needed once). On recent macOS, instead open it once, then go to System Settings → Privacy & Security and click "Open Anyway". - Terminal: clear the download quarantine, then open it:
xattr -dr com.apple.quarantine ~/Downloads/wlmatrix.app open ~/Downloads/wlmatrix.app
Prefer a bare CLI binary? wlmatrix-aarch64-macos is also attached — same
quarantine rule applies (xattr -d com.apple.quarantine wlmatrix-aarch64-macos,
then chmod +x).
Intel Mac? The downloads are
aarch64; build from source instead (cargo build --release). Source builds are never quarantined. The real fix is signing + notarization with an Apple Developer ID — not set up yet.The bundle is assembled by
dist/macos/make-app.sh(Info.plist +.icnsicon) in CI; run it on a Mac to build the.applocally.
Windows may likewise show a SmartScreen warning ("Windows protected your PC") for the unsigned
.exe/.scr— click More info → Run anyway.
Download wlmatrix.scr from the latest release, then either:
- Right-click
wlmatrix.scr→ Install. This opens the Screen Saver settings with wlmatrix selected — set the idle wait time and click OK. (To keep it permanently, first copywlmatrix.scrintoC:\Windows\System32\, or just run it from wherever it lives.) - Or right-click → Test to preview it fullscreen immediately.
Once selected, Windows launches it automatically on idle and dismisses it on any
input — no background daemon needed (the OS does the idle timing). The
Settings button shows where the config.toml lives (wlmatrix is configured
by that file, same keys as every other platform). wlmatrix.exe is also
attached if you just want to run it from a terminal.
Like the macOS app, the
.scrisn't code-signed yet, so SmartScreen warns on first run — More info → Run anyway.
The one-liner does everything — builds the binary, installs the renderer + idle daemon, and enables the user service:
git clone https://github.com/AsafSaar/wlmatrix
cd wlmatrix
./install.shThat's it; the screensaver kicks in after the idle timeout (default 5 min).
Manual steps (what install.sh does)
# 1. Build the renderer
cargo build --release
# 2. Install binary (symlink), daemon, and service
mkdir -p ~/.local/bin ~/.config/systemd/user
ln -sf "$PWD/target/release/wlmatrix" ~/.local/bin/wlmatrix
install -m 0755 dist/wl-screensaver ~/.local/bin/wl-screensaver
install -m 0644 dist/wl-screensaver.service ~/.config/systemd/user/wl-screensaver.service
# 3. Enable the user service
systemctl --user daemon-reload
systemctl --user enable --now wl-screensaver.serviceRun the renderer directly to preview it (grabs the whole screen; move the mouse to quit):
wlmatrix # run fullscreen
wlmatrix --help # usage
wlmatrix --version # version
wlmatrix --shot out.png # render one frame to a PNG (no window)
wlmatrix --gif out.gif # render an animated looping GIFCheck the service: systemctl --user status wl-screensaver.service
All settings live in one file — ~/.config/wlmatrix/config.toml (respects
$XDG_CONFIG_HOME). Edit it and the change takes effect the next time the
screensaver runs; no rebuild, no restart (the idle daemon re-reads idle_ms
each run too). install.sh drops a commented default if you don't have one.
style = "classic" # classic (ASCII) | neo (bloom + katakana)
glow = false # additive bloom on bright pixels
depth = 1 # parallax rain layers: 1 = flat, 2-3 = volumetric
idle_ms = 300000 # idle before it starts, in ms (300000 = 5 min)
fps = 30 # animation frame rate
font_size = 26 # glyph size; bigger = sparser rain
speed_min = 6 # fall-speed range, rows/sec
speed_max = 24
color = "green" # green|amber|cyan|red|purple|white|"#RRGGBB"
charset = "ascii" # ascii|alnum|binary|digits|katakana|"<literal>"
# mask = "/path/to/pic.png" # hide an image in the rain (luminance × alpha)
# mask_text = "WAKE UP" # ...or a line of text (used if `mask` unset)
mask_intensity = 0.85 # how strongly the hidden image glows, 0–1
mask_contrast = 0.0 # dim rain outside the figure (0 = off; try 0.4–0.6)
# font = "/path/to/Mono.ttf" # optional; else a system mono is found| Key | Default | Notes |
|---|---|---|
style |
classic |
neo turns on glow + katakana in one switch |
glow |
false |
additive bloom (CPU, ~1/4-res) |
depth |
1 |
parallax layers (1–4); far layers smaller/slower/dimmer |
idle_ms |
300000 |
read by the daemon; ms before the saver starts |
fps |
30 |
1–240 |
font_size |
26 |
pixels; controls rain density |
speed_min / speed_max |
6 / 24 |
rows per second |
color |
green |
preset name or #RRGGBB |
charset |
ascii |
preset or literal; katakana auto-loads a CJK font |
mask |
(none) | PNG hidden in the rain; any size/format, luminance × alpha |
mask_text |
(none) | text hidden in the rain (used only if mask is unset) |
mask_intensity |
0.85 |
0–1; how strongly the hidden image glows (lower ≈ subtler) |
mask_contrast |
0.0 |
0–1; dim rain outside the figure for figure-ground (0 = off) |
font |
(auto) | force a specific .ttf/.ttc |
Any flag overrides the file — handy for trying things or baking a variant into
the service’s SAVER_CMD:
wlmatrix --color amber --speed 4-20 --fps 60
wlmatrix --config /path/to/other.tomlRun wlmatrix --help for the full list.
SAVER_IDLE_MS=5000 ~/.local/bin/wl-screensaver # 5-second idle; Ctrl-C to stopwlmatrix/
├── src/main.rs # the renderer
├── Cargo.toml
├── install.sh # build + install + enable everything
├── dist/
│ ├── wl-screensaver # idle daemon (bash) → ~/.local/bin/
│ ├── wl-screensaver.service # systemd user unit → ~/.config/systemd/user/
│ └── config.toml # default config → ~/.config/wlmatrix/
├── README.md
└── LICENSE
Idle-parsing note (in
dist/wl-screensaver): the Mutter idle value is parsed withsed -E 's/.*uint64 ([0-9]+).*/\1/', notgrep -oE '[0-9]+'— the latter grabs the64fromuint64.
- Nothing happens on idle. Is the service running?
systemctl --user status wl-screensaver. Is the binary on PATH?command -v wlmatrix. Watch logs:journalctl --user -u wl-screensaver -f. - It flashes and disappears. The renderer exited immediately. Run it directly to see errors:
~/.local/bin/wlmatrix. (It also ignores input for the first 700 ms so the startup event doesn't dismiss it.) - Font looks wrong / app won't start. No monospace font was found; install one (
sudo apt install fonts-dejavu-core) or add its path toFONT_CANDIDATESinsrc/main.rs. - GNOME blanks the screen instead. Make sure GNOME's own blank isn't racing:
gsettings get org.gnome.desktop.session idle-delay(0 = never; the daemon owns idle).
GNOME on Wayland dropped animated screensavers (the old subsystem depended on X11). Reviving one on this particular machine (4K 3840×2160, NVIDIA, XWayland) meant discovering that every traditional approach is broken here:
- xscreensaver GL hacks (
glmatrix,glslideshow, …) abort when created at 3840×2160; and when created small then resized to fullscreen, the X window grows but the GLX backing buffer stays at the creation size, so only the top-left ~1/9 of the screen renders. - xscreensaver 2D Xlib hacks (
xmatrix) never receive the fullscreen resize event under XWayland → same top-left 1/9 problem. cmatrixin a fullscreen terminal froze itself after one frame on the large 4K grid; a curses replacement was janky.
The common thread: anything going through OpenGL or XWayland fails on this
box. A native Wayland client that renders pixels on the CPU into a wl_shm
buffer sidesteps all of it — hence this project.
Issues and PRs welcome. The renderer is a single file (src/main.rs); the idle
daemon is a single bash script (dist/wl-screensaver). Other compositors
(sway, Hyprland, KDE) should mostly work — the renderer is generic Wayland; only
the daemon's Mutter idle query is GNOME-specific, and could be swapped for
ext-idle-notify / swayidle.
MIT © 2026 Asaf Saar




