hyprsaver 0.3.1

A Wayland-native screensaver for Hyprland — fractal shaders on wlr-layer-shell overlays
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
# hyprsaver

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

[![CI](https://img.shields.io/github/actions/workflow/status/maravexa/hyprsaver/ci.yml?label=CI)](https://github.com/maravexa/hyprsaver/actions)
[![Crates.io](https://img.shields.io/crates/v/hyprsaver)](https://crates.io/crates/hyprsaver)
[![AUR](https://img.shields.io/aur/version/hyprsaver)](https://aur.archlinux.org/packages/hyprsaver)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

---

## What is hyprsaver?

hyprsaver is a GPU-accelerated screensaver for [Hyprland](https://hyprland.org). It renders GLSL fragment shaders as fullscreen overlays on every connected monitor using the [wlr-layer-shell](https://wayland.app/protocols/wlr-layer-shell-unstable-v1) Wayland protocol -- a proper Wayland citizen, not a window hack.

It is designed to complement [hyprlock](https://github.com/hyprwm/hyprlock) and [hypridle](https://github.com/hyprwm/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
```bash
# Download the .deb from the latest release
sudo dpkg -i hyprsaver_0.3.0_amd64.deb
```

### Fedora / RHEL / openSUSE
```bash
# Download the .rpm from the latest release
sudo rpm -i hyprsaver-0.3.0-1.x86_64.rpm
```

### Arch Linux
```bash
yay -S hyprsaver
```

### Cargo Install
```bash
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`):
   ```ini
   listener {
       timeout = 600
       on-timeout = hyprsaver
       on-resume = hyprsaver --quit
   }
   ```

4. Customize (`~/.config/hypr/hyprsaver.toml`):
   ```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`).

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

Or manually:

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

To install to a custom prefix:

```sh
make install PREFIX=/usr
```

To uninstall:

```sh
make uninstall
```

### AUR

```sh
paru -S hyprsaver  # or yay, or manual makepkg
```

### Nix / NixOS

A Nix flake is included in the repository root.

**Run without installing:**

```sh
nix run github:maravexa/hyprsaver
```

**Add to your NixOS / Home Manager flake:**

```nix
# 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):

```sh
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:
> ```sh
> 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/`](examples/) directory.

### hypridle.conf

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

```ini
# ~/.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)

```ini
# 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`](examples/hyprsaver.toml).

### Minimal Config

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

### Full Reference

```toml
[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:

```toml
[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:

```toml
[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]`:

```toml
# 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/](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:

```glsl
#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

```glsl
#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

```sh
# 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`:

```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:
   ```bash
   hyprsaver --preview julia --palette autumn
   ```

2. Edit your palette values in `~/.config/hypr/hyprsaver.toml`:
   ```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](https://iquilezles.org/articles/palettes/).

---

## 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:**

```sh
# 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:

<details>
<summary>Architecture</summary>

```mermaid
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
```

</details>

`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:
```sh
cargo fmt
cargo clippy -- -D warnings
cargo test
```

---

## License

MIT -- see [LICENSE](LICENSE).

---

## Acknowledgments

- **[Inigo Quilez]https://iquilezles.org/** -- for the cosine gradient palette technique and for [Shadertoy]https://www.shadertoy.com, the best shader playground in existence. The smooth iteration coloring in `mandelbrot.frag` is also his technique.
- **[Hyprland]https://hyprland.org** and the [hyprwm]https://github.com/hyprwm ecosystem (hyprlock, hypridle) -- for building a compositor worth building screensavers for.
- **[wlr-protocols]https://gitlab.freedesktop.org/wlroots/wlr-protocols** -- for `zwlr_layer_shell_v1`, which makes proper Wayland screensavers possible.
- **[smithay]https://github.com/Smithay/smithay** -- for smithay-client-toolkit, the best Rust Wayland client toolkit.
- **[glow]https://github.com/grovesNL/glow** -- for a sane OpenGL abstraction that doesn't require unsafe everywhere.