036b326b04
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
205 lines
6.2 KiB
Rust
205 lines
6.2 KiB
Rust
use std::cell::Cell;
|
|
use std::path::PathBuf;
|
|
use std::sync::{Mutex, PoisonError};
|
|
use std::time::Duration;
|
|
use std::{env, fs, thread};
|
|
|
|
use amane::{Animation, Blend, Color, Service};
|
|
|
|
use crate::parse;
|
|
|
|
/*
|
|
* the shell's colours. they come from the wallpaper: skwd-wall runs matugen with
|
|
* matugen/amane.colors, which writes ~/.config/amane/theme. ~/.config/amane/shell.conf
|
|
* says whether the open media card takes the song cover's colours instead, and how long
|
|
* a change cross-fades. without a theme file the shell keeps its original pink on purple
|
|
*/
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
pub struct Colors {
|
|
pub accent: Color,
|
|
pub on_accent: Color,
|
|
pub accent_soft: Color,
|
|
pub flame: Color,
|
|
pub text: Color,
|
|
pub dim: Color,
|
|
pub outline: Color,
|
|
pub surface: Color,
|
|
pub surface_high: Color,
|
|
pub surface_low: Color,
|
|
}
|
|
|
|
const PINK_ON_PURPLE: Colors = Colors {
|
|
accent: Color::rgb(247, 168, 200),
|
|
on_accent: Color::rgb(40, 18, 32),
|
|
accent_soft: Color::rgb(150, 82, 122),
|
|
flame: Color::rgb(255, 228, 240),
|
|
text: Color::rgb(241, 236, 246),
|
|
dim: Color::rgba(205, 192, 218, 160),
|
|
outline: Color::rgba(255, 255, 255, 26),
|
|
surface: Color::rgb(24, 18, 30),
|
|
surface_high: Color::rgb(35, 26, 44),
|
|
surface_low: Color::rgb(13, 10, 18),
|
|
};
|
|
|
|
impl Colors {
|
|
// the background gradient's three stops, light to dark
|
|
pub fn surfaces(&self) -> [Color; 3] {
|
|
[self.surface_high, self.surface, self.surface_low]
|
|
}
|
|
}
|
|
|
|
// so the colours can ride one Animation, all ten fading together
|
|
impl Blend for Colors {
|
|
fn blend(from: Self, to: Self, amount: f32) -> Self {
|
|
let mix = |from: Color, to: Color| Color::blend(from, to, amount);
|
|
|
|
Self {
|
|
accent: mix(from.accent, to.accent),
|
|
on_accent: mix(from.on_accent, to.on_accent),
|
|
accent_soft: mix(from.accent_soft, to.accent_soft),
|
|
flame: mix(from.flame, to.flame),
|
|
text: mix(from.text, to.text),
|
|
dim: mix(from.dim, to.dim),
|
|
outline: mix(from.outline, to.outline),
|
|
surface: mix(from.surface, to.surface),
|
|
surface_high: mix(from.surface_high, to.surface_high),
|
|
surface_low: mix(from.surface_low, to.surface_low),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Theme {
|
|
colors: Animation<Colors>,
|
|
|
|
// the open media card and the equalizer take the cover's colours as their background
|
|
pub cover_colors: bool,
|
|
}
|
|
|
|
impl Service for Theme {
|
|
fn new() -> Self {
|
|
Self {
|
|
colors: Animation::new(PINK_ON_PURPLE),
|
|
cover_colors: false,
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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() {
|
|
reload();
|
|
|
|
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"));
|
|
|
|
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);
|
|
}
|
|
|
|
fn read(name: &str) -> String {
|
|
fs::read_to_string(config_file(name)).unwrap_or_default()
|
|
}
|
|
|
|
// ~/.config/amane/<name>, or under $XDG_CONFIG_HOME
|
|
pub fn config_file(name: &str) -> PathBuf {
|
|
let config = env::var_os("XDG_CONFIG_HOME")
|
|
.map(PathBuf::from)
|
|
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
|
|
.unwrap_or_default();
|
|
|
|
config.join("amane").join(name)
|
|
}
|
|
|
|
fn apply(theme: &str, config: &str, startup: bool) {
|
|
let mut colors = PINK_ON_PURPLE;
|
|
|
|
// a missing or broken line keeps that colour's default
|
|
for (key, value) in parse::settings(theme) {
|
|
let Some([red, green, blue]) = parse::rgb(&value) else {
|
|
continue;
|
|
};
|
|
|
|
let color = Color::rgb(red, green, blue);
|
|
|
|
match key.as_str() {
|
|
"accent" => colors.accent = color,
|
|
"on_accent" => colors.on_accent = color,
|
|
"accent_soft" => colors.accent_soft = color,
|
|
"flame" => colors.flame = color,
|
|
"text" => colors.text = color,
|
|
"dim" => colors.dim = color,
|
|
"outline" => colors.outline = color,
|
|
"surface" => colors.surface = color,
|
|
"surface_high" => colors.surface_high = color,
|
|
"surface_low" => colors.surface_low = color,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let settings = parse::settings(config);
|
|
let setting = |name: &str| settings.iter().find(|(key, _)| key == name).map(|(_, value)| value.as_str());
|
|
|
|
let fade = if startup { 0 } else { setting("transition_ms").and_then(|ms| ms.parse().ok()).unwrap_or(400) };
|
|
|
|
let mut theme = Theme::write();
|
|
|
|
theme.cover_colors = setting("cover_colors") == Some("true");
|
|
|
|
// a change in the middle of a fade starts from the colours on screen now
|
|
let now = theme.colors.value();
|
|
|
|
theme.colors = Animation::new(now).duration(Duration::from_millis(fade));
|
|
theme.colors.to(colors);
|
|
}
|
|
|
|
// the colours of the frame being drawn, set by the view that draws it
|
|
thread_local! {
|
|
static NOW: Cell<Colors> = const { Cell::new(PINK_ON_PURPLE) };
|
|
}
|
|
|
|
/*
|
|
* a view's first line: everything it draws this frame uses these colours (and
|
|
* reading Theme redraws it when they change). says whether the media card takes the cover's
|
|
*/
|
|
pub fn begin() -> bool {
|
|
let theme = Theme::read();
|
|
|
|
NOW.set(theme.colors.value());
|
|
|
|
theme.cover_colors
|
|
}
|
|
|
|
pub fn now() -> Colors {
|
|
NOW.get()
|
|
}
|