Skip to content

AsafSaar/wlmatrix

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wlmatrix

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.

status license platform

wlmatrix demo

The animation above is rendered by wlmatrix itself — wlmatrix --gif docs/demo.gif (and --shot for a still). No screen capture involved.


What it does

  • 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.

Two looks

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 neo

neo mode — bloom + katakana

Add 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 3

depth/parallax — neo + 3 layers

Hidden 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

a figure condensing out of the rain — the operator view

(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.40.6 is a good range.

wlmatrix --mask docs/operator-mask.png --mask-contrast 0.6

hidden image with negative-space contrast


Architecture

Three 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

The renderer (src/main.rs)

  • winit — creates a native Wayland window and puts it fullscreen (Fullscreen::Borderless), delivers input events.
  • softbuffer — gives a raw &mut [u32] framebuffer backed by wl_shm; we write 0x00RRGGBB pixels and present(). 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 rand dependency).
  • ~30 fps loop driven by ControlFlow::WaitUntil.

Requirements

  • 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 needs gdbus (ships with glib) and a systemd user session.
  • macOS / Windows: nothing extra — see Platforms below.

Platforms

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.

macOS: the .app and the Gatekeeper warning

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.appOpen, 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 + .icns icon) in CI; run it on a Mac to build the .app locally.

Windows may likewise show a SmartScreen warning ("Windows protected your PC") for the unsigned .exe/.scr — click More info → Run anyway.

Windows: installing the screensaver

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 copy wlmatrix.scr into C:\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 .scr isn't code-signed yet, so SmartScreen warns on first run — More info → Run anyway.


Build & install

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.sh

That'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.service

Run 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 GIF

Check the service: systemctl --user status wl-screensaver.service


Configuration

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

CLI overrides

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.toml

Run wlmatrix --help for the full list.

Quick test without waiting

SAVER_IDLE_MS=5000 ~/.local/bin/wl-screensaver   # 5-second idle; Ctrl-C to stop

Repository layout

wlmatrix/
├── 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 with sed -E 's/.*uint64 ([0-9]+).*/\1/', not grep -oE '[0-9]+' — the latter grabs the 64 from uint64.


Troubleshooting

  • 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 to FONT_CANDIDATES in src/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).

Why this exists

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.
  • cmatrix in 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.


Contributing

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.

License

MIT © 2026 Asaf Saar

About

Native Wayland Matrix-rain screensaver, CPU-rendered via wl_shm (no GPU)

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors