hyprsaver
A Wayland-native screensaver for Hyprland -- fractal shaders on wlr-layer-shell overlays
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
Fedora / RHEL / openSUSE
# Download the .rpm from the latest release
Arch Linux
# Download the .tar.zst from the latest release
Manual Installation
-
Build and install:
git clone https://github.com/maravexa/hyprsaver cd hyprsaver make install -
Test it (launches screensaver immediately):
hyprsaverPress any key or move the mouse to dismiss.
-
Add to your hypridle config (
~/.config/hypr/hypridle.conf):listener { timeout = 600 on-timeout = hyprsaver on-resume = hyprsaver --quit } -
Customize (
~/.config/hyprsaver/config.toml):[] = "julia" = "vapor" [] = 800 = 400 # Per-monitor overrides (run `hyprctl monitors` for output names) [[]] = "DP-1" = "raymarcher" = "frost"
Features (v0.2.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/hyprsaver/shaders/-- edit, save, see the change instantly -
Built-in shader collection (10 shaders):
Name Description mandelbrotMandelbrot set with animated zoom juliaJulia set with animated parameter plasmaClassic plasma effect tunnelInfinite tunnel flythrough voronoiAnimated Voronoi cells snowfallFive-layer parallax snowfall with palette dot glow starfieldHyperspace zoom tunnel with motion-blur tracers kaleidoscope6-fold kaleidoscope driven by domain-warped FBM flow_fieldCurl-noise flow field with 8-step particle tracing raymarcherRaymarched torus with Phong lighting and fog lissajousThree overlapping Lissajous curves with glow -
Built-in palette collection: electric, autumn, vapor, frost, ember, ocean, monochrome
-
Configurable FPS and dismiss triggers
-
Preview mode for shader authoring (
--preview <shader>) -
PID file based instance management (
--quitto 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).
Or manually:
To install to a custom prefix:
To uninstall:
AUR
Nix / NixOS
A Nix flake is included in the repository root.
Run without installing:
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):
NixOS note:
libGLandlibEGLare dlopen'd at runtime. The flake'sdevShellsetsLD_LIBRARY_PATHautomatically. If you run the installed binary outside the dev shell, wrap it with:LD_LIBRARY_PATH=/lib:or use
programs.hyprsaver.enableonce 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/hyprsaver/config.toml. It is entirely optional -- hyprsaver runs with built-in defaults if no file exists.
A full annotated example is provided at config.example.toml and examples/config.toml.
Minimal Config
[]
= "julia"
= "vapor"
= 60
Full Reference
[]
= 30 # render frame rate
= "mandelbrot" # or "random", "cycle", or a custom name
= "electric" # or "random", "cycle", or a custom name
= 300 # seconds per shader when shader = "cycle"
= ["frost", "ocean", "electric", "ember"] # month-indexed
[]
= 800 # fade-in duration
= 400 # fade-out duration
= ["key", "mouse_move", "mouse_click", "touch"]
# Custom palettes are defined as top-level [palettes.<name>] sections
[]
= [0.5, 0.5, 0.5]
= [0.5, 0.5, 0.5]
= [1.0, 1.0, 1.0]
= [0.00, 0.33, 0.67]
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/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.0andu_zoom_scale = 1.0unless you add those uniforms to your own shader logic.
Hot-Reload Workflow
# Open a live preview window
# In another terminal, edit the shader -- changes appear within one second
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:
[]
= [0.5, 0.4, 0.3] # midpoint brightness per channel
= [0.5, 0.4, 0.3] # oscillation amplitude
= [1.0, 1.0, 0.5] # frequency (0.5 = half a cycle for blue)
= [0.00, 0.15, 0.30] # phase offset (shifts each channel's hue)
Tips for palette design:
- Keep
a + b <= 1.0per channel to avoid clipping d = [0.00, 0.33, 0.67]evenly spaces RGB phases -> classic rainbowc = [1.0, 1.0, 1.0]means one full color cycle per sweep oft- Low
bvalues (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, use the included test shader:
-
Copy the test shader to your user shader directory:
-
Launch hyprsaver with the test shader and your target palette:
The top portion of the screen shows the full palette as a horizontal gradient. The bottom shows the palette applied to a ring pattern, simulating how it looks on fractal geometry.
-
Edit your palette values in
~/.config/hyprsaver/config.toml:[] = [0.5, 0.3, 0.2] = [0.5, 0.4, 0.3] = [1.0, 1.0, 1.0] = [0.0, 0.1, 0.2] -
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")
--list-shaders Print all available shader names and exit
--list-palettes Print all available palette names 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
# Preview a custom shader while editing it
# See what's available
# Dismiss the running screensaver (e.g. from a hotkey)
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/hyprsaver/config.toml")]
shaderfiles[("~/.config/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
v0.3.0
- Audio reactivity via PipeWire (FFT frequency bands -> shader uniforms)
- Interactive mode: mouse position -> shader uniforms (for ambient desktop use, not just screensaver)
- MPRIS integration: album art dominant color extraction -> automatic palette
v0.4.0
- wgpu backend option (Vulkan via wgpu for broader GPU compatibility and future-proofing)
- Shader parameter GUI (small GTK4 or Slint panel for live-tuning palette vectors)
- Community shader and palette repository integration
v1.0.0
- AUR and Nix packages, stable install story
- Stable config format -- no breaking changes after this
- Comprehensive curated shader library (20+ shaders)
- Full Shadertoy uniform support:
iChanneltextures,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:
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.fragis 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.