hyprsaver 0.3.1

A Wayland-native screensaver for Hyprland — fractal shaders on wlr-layer-shell overlays
hyprsaver-0.3.1 is not a library.

hyprsaver

A Wayland-native screensaver for Hyprland -- fractal shaders on wlr-layer-shell overlays

CI Crates.io AUR License: MIT


What is hyprsaver?

hyprsaver is a GPU-accelerated screensaver for Hyprland. It renders GLSL fragment shaders as fullscreen overlays on every connected monitor using the wlr-layer-shell Wayland protocol -- a proper Wayland citizen, not a window hack.

It is designed to complement hyprlock and hypridle. Screensaver and lock screen are separate concerns: hyprsaver blankets your monitor with beautiful fractals, hypridle triggers it after a configurable idle timeout, and hyprlock handles authentication when you want to resume. Each tool does one thing well.


Quick Start

Debian / Ubuntu

# Download the .deb from the latest release
sudo dpkg -i hyprsaver_0.3.0_amd64.deb

Fedora / RHEL / openSUSE

# Download the .rpm from the latest release
sudo rpm -i hyprsaver-0.3.0-1.x86_64.rpm

Arch Linux

yay -S hyprsaver

Cargo Install

cargo install hyprsaver

Manual Installation

  1. Build and install:

    git clone https://github.com/maravexa/hyprsaver
    cd hyprsaver
    make install
    
  2. Test it (launches screensaver immediately):

    hyprsaver
    

    Press any key or move the mouse to dismiss.

  3. Add to your hypridle config (~/.config/hypr/hypridle.conf):

    listener {
        timeout = 600
        on-timeout = hyprsaver
        on-resume = hyprsaver --quit
    }
    
  4. Customize (~/.config/hypr/hyprsaver.toml):

    [general]
    shader = "julia"
    palette = "vapor"
    
    [behavior]
    fade_in_ms = 800
    fade_out_ms = 400
    
    # Per-monitor overrides (run `hyprctl monitors` for output names)
    [[monitor]]
    name = "DP-1"
    shader = "raymarcher"
    palette = "frost"
    

Features (v0.3.0)

  • Wayland-native via wlr-layer-shell -- not a window, a proper overlay surface

  • GPU-accelerated GLSL fragment shaders via OpenGL ES (glow crate)

  • Multi-monitor support -- one surface per output, with per-monitor shader/palette assignment via [[monitor]] config blocks

  • Cosine gradient palettes -- 12 floats define smooth, infinite color ramps. Any shader x any palette

  • Shadertoy-compatible shader format -- paste Shadertoy code with minimal edits, it just works

  • Hot-reload shaders from ~/.config/hypr/hyprsaver/shaders/ -- edit, save, see the change instantly

  • Cycle mode for shaders and palettes -- rotate through all or a named playlist on a configurable interval

  • Built-in shader collection (17 shaders):

    Name Description
    mandelbrot Mandelbrot set with animated zoom
    julia Julia set with animated parameter
    plasma Classic plasma effect
    tunnel Infinite tunnel flythrough
    voronoi Animated Voronoi cells
    snowfall Five-layer parallax snowfall with palette dot glow
    starfield Hyperspace zoom tunnel with motion-blur tracers
    kaleidoscope 6-fold kaleidoscope driven by domain-warped FBM
    flow_field Curl-noise flow field with 8-step particle tracing
    raymarcher Raymarched torus with Phong lighting and fog
    lissajous Three overlapping Lissajous curves with glow
    geometry Wireframe polyhedron morphing (cube→icosahedron→...)
    hypercube Rotating 4D tesseract projected to 2D, neon glow
    network Neural network node graph with glowing connections
    matrix Classic Matrix digital rain with procedural glyphs
    fire Roiling procedural flames with ember particles
    caustics Underwater caustic light patterns
  • Built-in palette collection: electric, autumn, vapor, frost, ember, ocean, monochrome, sunset, aurora, midnight

  • Configurable FPS and dismiss triggers

  • Preview mode for shader authoring (--preview <shader>) with speed/zoom control panel

  • PID file based instance management (--quit to signal a running instance)

  • Zero-config: works with no config file, sensible defaults throughout

  • Clean integration with hypridle and hyprlock


Installation

Build from Source

Requires the Rust stable toolchain, development headers for Wayland (wayland-devel / libwayland-dev), and EGL (mesa-libEGL-devel / libegl-dev).

git clone https://github.com/maravexa/hyprsaver
cd hyprsaver
make install          # builds release and installs to /usr/local/bin

Or manually:

cargo build --release
sudo install -Dm755 target/release/hyprsaver /usr/local/bin/hyprsaver

To install to a custom prefix:

make install PREFIX=/usr

To uninstall:

make uninstall

AUR

paru -S hyprsaver  # or yay, or manual makepkg

Nix / NixOS

A Nix flake is included in the repository root.

Run without installing:

nix run github:maravexa/hyprsaver

Add to your NixOS / Home Manager flake:

# flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    hyprsaver.url = "github:maravexa/hyprsaver";
  };

  outputs = { self, nixpkgs, hyprsaver, ... }: {
    # NixOS system config:
    nixosConfigurations.myhostname = nixpkgs.lib.nixosSystem {
      modules = [
        ({ pkgs, ... }: {
          environment.systemPackages = [
            hyprsaver.packages.${pkgs.system}.default
          ];
        })
      ];
    };
  };
}

Development shell (includes Rust stable + rust-analyzer + clippy):

nix develop github:maravexa/hyprsaver

NixOS note: libGL and libEGL are dlopen'd at runtime. The flake's devShell sets LD_LIBRARY_PATH automatically. If you run the installed binary outside the dev shell, wrap it with:

LD_LIBRARY_PATH=$(nix eval --raw 'nixpkgs#mesa')/lib:$LD_LIBRARY_PATH hyprsaver

or use programs.hyprsaver.enable once a NixOS module is added (planned for v1.0.0).


Integration with Hyprland

Example configuration files for hypridle and hyprland are provided in the examples/ directory.

hypridle.conf

The recommended setup: hypridle triggers hyprsaver after 10 minutes of idle, then hyprlock after 20 minutes.

# ~/.config/hypridle/hypridle.conf

general {
    lock_cmd = hyprlock          # run hyprlock when the session is locked
    ignore_dbus_inhibit = false  # respect Wayland idle inhibitors (video players, etc.)
}

listener {
    timeout = 600                # 10 minutes -> start screensaver
    on-timeout = hyprsaver
    on-resume = hyprsaver --quit # dismiss screensaver when activity resumes
}

listener {
    timeout = 1200               # 20 minutes -> lock screen
    on-timeout = hyprlock
}

Note: hypridle respects org.freedesktop.ScreenSaver.Inhibit (set by most video players and browsers during full-screen playback), so hyprsaver is automatically suppressed while you watch a film.

hyprland.conf (optional hotkey)

# Start/stop the screensaver manually
bind = $mod, F12, exec, hyprsaver
bind = , escape, exec, hyprsaver --quit

Configuration

The config file lives at ~/.config/hypr/hyprsaver.toml. It is entirely optional -- hyprsaver runs with built-in defaults if no file exists.

Upgrading from v0.1.x? The config path moved from ~/.config/hyprsaver/config.toml to ~/.config/hypr/hyprsaver.toml and the shader directory from ~/.config/hyprsaver/shaders/ to ~/.config/hypr/hyprsaver/shaders/. The old paths are still recognised with a deprecation warning — move your files at your convenience.

A full annotated example is provided at examples/hyprsaver.toml.

Minimal Config

[general]
shader = "julia"
palette = "vapor"
fps = 30

Full Reference

[general]
fps = 30                          # render frame rate
shader = "mandelbrot"             # a shader name, "random", or "cycle"
palette = "electric"              # a palette name, "random", or "cycle"
shader_cycle_interval = 300       # seconds per shader when shader = "cycle"
palette_cycle_interval = 60       # seconds per palette when palette = "cycle"
# shader_playlist = "my_favorites"  # restrict cycle to a named playlist
# palette_playlist = "warm_tones"   # restrict palette cycle to a named playlist

[behavior]
fade_in_ms = 800               # fade-in duration
fade_out_ms = 400              # fade-out duration
dismiss_on = ["key", "mouse_move", "mouse_click", "touch"]

# Custom palettes are defined as top-level [palettes.<name>] sections
[palettes.my_palette]
a = [0.5, 0.5, 0.5]
b = [0.5, 0.5, 0.5]
c = [1.0, 1.0, 1.0]
d = [0.00, 0.33, 0.67]

Cycle Mode

Set shader = "cycle" (or palette = "cycle") to rotate through shaders automatically:

[general]
shader = "cycle"
shader_cycle_interval = 300   # advance every 5 minutes

palette = "cycle"
palette_cycle_interval = 60   # advance every minute

To cycle only a subset, define a playlist and reference it:

[general]
shader = "cycle"
shader_playlist = "chill"

[shader_playlists.chill]
shaders = ["snowfall", "starfield", "tunnel", "plasma"]

On startup, cycle mode begins at a random position in the playlist so each session looks different. Use --list-shader-playlists or --list-palette-playlists to inspect defined playlists.

Playlists

Playlists are named subsets used with cycle mode. Define them in the config and reference by name in [general]:

# Shader playlists
[shader_playlists.my_favorites]
shaders = ["mandelbrot", "julia", "fire", "caustics"]

[shader_playlists.chill]
shaders = ["snowfall", "starfield", "tunnel", "plasma"]

# Palette playlists
[palette_playlists.warm_tones]
palettes = ["ember", "autumn", "sunset"]

[palette_playlists.cool_vibes]
palettes = ["frost", "ocean", "vapor"]

Unknown shader or palette names in a playlist are skipped with a warning. If a playlist resolves to empty, all available shaders/palettes are cycled instead.

Cosine Gradient Palettes

Palettes use Inigo Quilez's cosine gradient technique. The formula is:

color(t) = a + b * cos(2pi * (c * t + d))

where a, b, c, d are RGB vectors and t is in [0, 1].

  • a -- average brightness (midpoint of the oscillation)
  • b -- amplitude/contrast of each channel
  • c -- frequency (1.0 = one hue cycle; 2.0 = two cycles)
  • d -- phase shift (rotates each channel's hue independently)

Full mathematical background: https://iquilezles.org/articles/palettes/


Writing Custom Shaders

Drop .frag files in ~/.config/hypr/hyprsaver/shaders/. They are available immediately by filename stem (e.g. my_effect.frag -> --shader my_effect).

Shader Format

hyprsaver shaders are GLSL ES 3.20 fragment shaders with these uniforms available:

#version 320 es
precision highp float;

uniform float u_time;        // seconds since screensaver started
uniform vec2  u_resolution;  // physical pixel dimensions of the surface
uniform vec2  u_mouse;       // last mouse position (window-space pixels)
uniform int   u_frame;       // frame counter, starts at 0

// Cosine gradient palette -- set by the active palette config (v0.2.0+ names)
uniform vec3  u_palette_a_a;   // brightness
uniform vec3  u_palette_a_b;   // amplitude
uniform vec3  u_palette_a_c;   // frequency
uniform vec3  u_palette_a_d;   // phase
// LUT palette (texture units 1/2) and blend factor are also injected automatically
uniform sampler2D u_lut_a;
uniform int       u_use_lut;   // 0 = cosine, 1 = LUT
uniform float     u_palette_blend;

// Speed/zoom controls (preview panel drives these; daemon always sends 1.0)
uniform float u_speed_scale;
uniform float u_zoom_scale;

out vec4 fragColor;

// Palette helper -- included automatically, always available
// Signature unchanged from v0.1.x; implementation handles cosine + LUT modes
vec3 palette(float t);

Minimal Example Shader

#version 320 es
precision highp float;

uniform float u_time;
uniform vec2  u_resolution;
uniform float u_speed_scale;

out vec4 fragColor;

// palette() is injected automatically — no need to declare it yourself

void main() {
    vec2 uv = gl_FragCoord.xy / u_resolution;
    float t = length(uv - 0.5) * 3.0 - u_time * u_speed_scale * 0.5;
    fragColor = vec4(palette(fract(t)), 1.0);
}

Shadertoy Compatibility

hyprsaver accepts shaders written in Shadertoy's convention. The following remappings are applied automatically:

Shadertoy uniform hyprsaver uniform
iTime u_time
iResolution vec3(u_resolution, 0.0)
iMouse vec4(u_mouse, 0.0, 0.0)
iFrame u_frame

If your shader contains void mainImage(out vec4 fragColor, in vec2 fragCoord), a void main() wrapper is appended automatically. You can paste most Shadertoy shaders directly (note: iChannel texture uniforms are not yet supported -- v1.0.0).

Preview Mode — Control Panel

--preview opens a desktop window split into two regions:

  • Left: live shader viewport
  • Right: 280-px egui control panel

The panel provides:

Control Description
Shader ComboBox Switch to any built-in or user shader instantly
Palette ComboBox Switch palette without restarting
Speed slider 0.1× – 3.0× time multiplier (default 1.0)
Zoom slider 0.1× – 3.0× zoom depth (fractal / starfield shaders)
▶ Preview button Apply selected shader, palette, speed, and zoom

Keyboard shortcuts always active in the preview window:

Key Action
Q / Esc Quit preview
R Force-reload current shader from disk

Note: Speed and zoom sliders only affect the preview window — the daemon always uses u_speed_scale = 1.0 and u_zoom_scale = 1.0 unless you add those uniforms to your own shader logic.

Hot-Reload Workflow

# Open a live preview window
hyprsaver --preview my_shader

# In another terminal, edit the shader -- changes appear within one second
$EDITOR ~/.config/hypr/hyprsaver/shaders/my_shader.frag

Compile errors are logged to stderr; the last working shader continues running.


Writing Custom Palettes

A palette is just four RGB vectors in TOML. Add them to config.toml:

[palettes.my_palette]
a = [0.5, 0.4, 0.3]   # midpoint brightness per channel
b = [0.5, 0.4, 0.3]   # oscillation amplitude
c = [1.0, 1.0, 0.5]   # frequency (0.5 = half a cycle for blue)
d = [0.00, 0.15, 0.30] # phase offset (shifts each channel's hue)

Tips for palette design:

  • Keep a + b <= 1.0 per channel to avoid clipping
  • d = [0.00, 0.33, 0.67] evenly spaces RGB phases -> classic rainbow
  • c = [1.0, 1.0, 1.0] means one full color cycle per sweep of t
  • Low b values (e.g. [0.2, 0.2, 0.2]) produce subtle, pastel gradients
  • a = [0.8, 0.7, 0.6], b = [0.2, 0.2, 0.2] -> warm cream with gentle color hints

Palettes are tiny and easy to share -- post them as four TOML lines.

Palette Tuning Workflow

For fast iteration when designing or tweaking palettes:

  1. Launch hyprsaver in preview mode with any shader and your target palette:

    hyprsaver --preview julia --palette autumn
    
  2. Edit your palette values in ~/.config/hypr/hyprsaver.toml:

    [palettes.my_custom_palette]
    a = [0.5, 0.3, 0.2]
    b = [0.5, 0.4, 0.3]
    c = [1.0, 1.0, 1.0]
    d = [0.0, 0.1, 0.2]
    
  3. Hot-reload picks up config changes automatically — save the file and the palette updates live on screen. No restart needed.

The cosine palette formula is color(t) = a + b × cos(2π × (c × t + d)). Each channel ranges from a - b (minimum) to a + b (maximum). Adjust d values to control where each color channel peaks relative to the others. For a deeper explanation, see Inigo Quilez's palette article.


CLI Reference

hyprsaver [OPTIONS]

OPTIONS:
    -c, --config <PATH>              Path to config file (overrides XDG default)
    -s, --shader <NAME>              Shader to use (name, "random", or "cycle")
    -p, --palette <NAME>             Palette to use (name, "random", or "cycle")
        --shader-cycle-interval <N>  Override shader cycle interval (seconds)
        --palette-cycle-interval <N> Override palette cycle interval (seconds)
        --list-shaders               Print all available shader names and exit
        --list-palettes              Print all available palette names and exit
        --list-shader-playlists      Print all defined shader playlists and exit
        --list-palette-playlists     Print all defined palette playlists and exit
        --quit                       Send SIGTERM to the running hyprsaver instance
        --preview <SHADER>           Open a windowed preview of the named shader
    -v, --verbose                    Enable debug logging (RUST_LOG=hyprsaver=debug)
    -h, --help                       Print help
    -V, --version                    Print version

Examples:

# Start with a specific shader and palette
hyprsaver --shader julia --palette vapor

# Cycle through all shaders every 2 minutes
hyprsaver --shader cycle --shader-cycle-interval 120

# Preview a custom shader while editing it
hyprsaver --preview my_shader

# See what's available
hyprsaver --list-shaders
hyprsaver --list-palettes
hyprsaver --list-shader-playlists

# Dismiss the running screensaver (e.g. from a hotkey)
hyprsaver --quit

Architecture

hyprsaver is structured as four independent layers that communicate through clean interfaces:

graph TD
    subgraph core ["Core Modules"]
        main["main.rs<br/>CLI · clap · signal-hook · PID file guard"]
        config["config.rs<br/>TOML + serde · XDG paths · zero-config"]
        wayland["wayland.rs<br/>layer-shell surfaces · output hotplug · input events"]
        renderer["renderer.rs<br/>glow · fullscreen quad · uniform uploads"]
        shaders["shaders.rs<br/>load/compile · hot-reload · Shadertoy shim"]
        palette["palette.rs<br/>cosine gradient · 12 floats"]
    end

    subgraph ext ["External Protocols"]
        layershell(["wlr-layer-shell"])
        egl(["EGL / GLES2"])
        calloop(["calloop"])
    end

    subgraph files ["User File Paths"]
        configfile[("~/.config/hypr/hyprsaver.toml")]
        shaderfiles[("~/.config/hypr/hyprsaver/shaders/*.frag")]
        pidfile[("$XDG_RUNTIME_DIR/hyprsaver.pid")]
    end

    main --> config
    main --> wayland
    main --> renderer
    config -->|SurfaceConfig| wayland
    shaders -->|compiled program| renderer
    palette -->|uniform vec3s| renderer
    wayland -->|frame callbacks| renderer

    wayland --- layershell
    renderer --- egl
    main --- calloop

    configfile --> config
    shaderfiles --> shaders
    pidfile --> main

renderer.rs knows nothing about Wayland. wayland.rs knows nothing about OpenGL. shaders.rs knows nothing about palettes at upload time -- it only prepends the GLSL palette() function. This makes each layer independently testable and replaceable (the wgpu backend in v0.4.0 only needs to replace renderer.rs).


Roadmap

Shipped in v0.3.0

  • 6 new built-in shaders: geometry, hypercube, network, matrix, fire, caustics
  • Cycle mode for shaders and palettes with configurable intervals (shader_cycle_interval, palette_cycle_interval)
  • Named playlists for shader and palette cycling ([shader_playlists.*], [palette_playlists.*])
  • CLI flags: --shader-cycle-interval, --palette-cycle-interval, --list-shader-playlists, --list-palette-playlists
  • Shader descriptions in --list-shaders output
  • Cycle mode starts at a random position; both monitors stay in sync during transitions

v0.4.0

  • Per-monitor shader/palette assignment
  • Screencopy texture pipeline (sample the desktop as a shader input)
  • Rain-on-glass shader with real blurred desktop background
  • Palette crossfade transitions on cycle

v1.0.0

  • Stable install story
  • Stable config format -- no breaking changes after this
  • Comprehensive curated shader library (20+ shaders)
  • Full Shadertoy uniform support: iChannel textures, iDate, iSampleRate
  • Comprehensive documentation and shader authoring guide

Contributing

Contributions are welcome. Fork, create a branch, submit a pull request.

Shader and palette contributions have the lowest barrier to entry -- a new built-in shader is just a .frag file plus an entry in shaders.rs. A new palette is four lines of TOML and a constant in palette.rs. If you've made something beautiful, please share it.

For larger contributions, open an issue first to discuss the approach.

Before submitting:

cargo fmt
cargo clippy -- -D warnings
cargo test

License

MIT -- see LICENSE.


Acknowledgments

  • Inigo Quilez -- for the cosine gradient palette technique and for Shadertoy, the best shader playground in existence. The smooth iteration coloring in mandelbrot.frag is also his technique.
  • Hyprland and the hyprwm ecosystem (hyprlock, hypridle) -- for building a compositor worth building screensavers for.
  • wlr-protocols -- for zwlr_layer_shell_v1, which makes proper Wayland screensavers possible.
  • smithay -- for smithay-client-toolkit, the best Rust Wayland client toolkit.
  • glow -- for a sane OpenGL abstraction that doesn't require unsafe everywhere.