theme: react to file changes instead of polling

the Theme service waits on amane::watch_file (inotify on the folder, so
matugen's rename-over saves count) for both ~/.config/amane/theme and
shell.conf, settles 50 ms for multi-write saves, and skips writes that
change nothing. measured on the desktop: rename-over, in-place and sed -i
writes picked up in 55-68 ms (was up to 500 ms), identical rewrites
ignored, no activity while idle (was 4 reads a second). deleting the
theme file takes effect at the next start
This commit is contained in:
server2
2026-10-09 04:44:02 +02:00
parent d04cbac8d4
commit 036b326b04
5 changed files with 42 additions and 17 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ Notes for agents continuing work on the Magic Island shell. Read README.md first
| `spectrum.rs` `Spectrum` | Service, 200 ms check | While the shown player plays: `pw-record --target <its stream> \| cava -p $XDG_RUNTIME_DIR/magic-island/cava.conf` → 24 levels. Stops when it pauses. |
| `art.rs` `Art` | Service, 250 ms poll | Cover URL → local file (curl for https), copied to `palette-cover`, which `Palette` (opened in `main.rs`, 8 colours) watches. |
| `parse.rs` | plain functions, no amane | pactl/cava/proc parsing, filter-chain config, `pw-cli` params, EQ store, middle ellipsis, `key = value` settings and `#rrggbb`. Unit-tested on its own. |
| `theme.rs` | `Theme` Service, 500 ms poll | The shell's 10 colours from `~/.config/amane/theme` (rendered by Skwd-wall from `matugen/amane.colors`) and `shell.conf` (`cover_colors`, `transition_ms`), faded together on one `Animation<Colors>`. Every view starts with `theme::begin()` (reads Theme, so it redraws on a change; returns `cover_colors`) and draws with `theme::now()`. No theme file: the original pink on purple. |
| `theme.rs` | `Theme` Service, `amane::watch_file` (inotify on the folder, so rename-over saves count) | The shell's 10 colours from `~/.config/amane/theme` (rendered by Skwd-wall from `matugen/amane.colors`) and `shell.conf` (`cover_colors`, `transition_ms`), faded together on one `Animation<Colors>`. Every view starts with `theme::begin()` (reads Theme, so it redraws on a change; returns `cover_colors`) and draws with `theme::now()`. No theme file: the original pink on purple. |
| `workspaces.rs` | view + input-only `Hover` Service | Workspace pill per monitor (namespace `magic-workspaces`, top left): `Workspaces` filtered by `output() == monitor.name`, special workspaces skipped, click = `hyprctl dispatch 'hl.dsp.focus({ workspace = N })'` (Hyprland 0.56 with a Lua config rejects the `dispatch workspace N` that amane's `Workspaces::focus` sends). Draws with `theme::now()`, reuses the island's `backdrop()` and `HOVER_MS`. Redraws only on Hyprland events and hover. |
The equalizer: one `setsid -f pipewire -c eq-<key>.conf` process per player with `libpipewire-module-filter-chain`, 10 biquads (low shelf 31 Hz, peaking 63 Hz–8 kHz, high shelf 16 kHz). The player's streams are moved into sink `magic_island_eq.<key>`; gains change live with `pw-cli set-param magic_island_eq.<key> Props '{ params = [ "b1:Gain" 6.0 ... ] }'`. A player only gets one when its curve or output is first changed, or it has a saved non-flat curve.
+1 -1
View File
@@ -15,7 +15,7 @@ A Dynamic-Island-style media widget for Amane (Rust Wayland shell library), runn
## Colours
The shell takes its colours from the wallpaper through matugen. Skwd-wall v2 runs matugen on every new wallpaper and renders `matugen/amane.colors` into `~/.config/amane/theme`; the shell reads that file twice a second and cross-fades to the new colours. Without the file it keeps its original pink on purple.
The shell takes its colours from the wallpaper through matugen. Skwd-wall v2 runs matugen on every new wallpaper and renders `matugen/amane.colors` into `~/.config/amane/theme`; the shell picks the file up the moment it changes (inotify through `amane::watch_file`) and cross-fades to the new colours. Without the file it keeps its original pink on purple; deleting it takes effect at the next start.
Register the template in Skwd-wall as a matugen integration:
+1 -1
View File
@@ -1,5 +1,5 @@
# matugen template for amane: skwd-wall renders it on every new wallpaper into ~/.config/amane/theme,
# which the shell reads twice a second. key=value, one colour per line; a missing line keeps the default
# which the shell picks up the moment it changes. key=value, one colour per line; a missing line keeps the default
accent={{colors.primary.default.hex}}
on_accent={{colors.on_primary.default.hex}}
accent_soft={{colors.primary_container.default.hex}}
+1 -1
View File
@@ -1,4 +1,4 @@
# amane shell settings, read twice a second: changes apply without a restart.
# amane shell settings, picked up the moment this file is saved: no restart needed.
# the colours themselves come from the wallpaper (matugen/amane.colors, written to ./theme)
# true: the open media card and equalizer take the song cover's colours as their background
+35 -10
View File
@@ -1,5 +1,6 @@
use std::cell::Cell;
use std::path::PathBuf;
use std::sync::{Mutex, PoisonError};
use std::time::Duration;
use std::{env, fs, thread};
@@ -82,23 +83,47 @@ impl Service for Theme {
}
}
// two small files read twice a second: matugen writes a new file each time, which a file watch can lose
/*
* waits on the kernel instead of polling: amane::watch_file watches the folder, so
* matugen writing a new file and renaming it over the old one is seen too.
* shell.conf on its own thread, the theme here; either change re-reads both
*/
fn listen() {
let mut last = None;
reload();
loop {
thread::spawn(|| follow("shell.conf"));
follow("theme");
}
}
// what was read last, (theme, shell.conf); none until the first read
static LAST: Mutex<Option<(String, String)>> = Mutex::new(None);
fn follow(name: &str) {
for _ in amane::watch_file(&config_file(name).to_string_lossy()) {
// one save can arrive as several writes: let it finish, then read once
thread::sleep(Duration::from_millis(50));
reload();
}
}
// a deleted theme file sends no event: the defaults come back at the next start
fn reload() {
let read = (read("theme"), read("shell.conf"));
if last.as_ref() != Some(&read) {
let mut last = LAST.lock().unwrap_or_else(PoisonError::into_inner);
// a write that changes nothing redraws nothing
if last.as_ref() == Some(&read) {
return;
}
// the colours at startup show at once, later changes cross-fade
apply(&read.0, &read.1, last.is_none());
last = Some(read);
}
thread::sleep(Duration::from_millis(500));
}
}
*last = Some(read);
}
fn read(name: &str) -> String {