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::{config, parse}; /* * the shell's colours. they come from the wallpaper: skwd-wall runs matugen with * matugen/amane.colors, which writes ~/.config/amane/theme. config.toml (config.rs) says * whether the open media card takes the song cover's colours instead, how long a change * cross-fades, and the colours used until there is a theme file. Theme also stands for * config.toml: every view reads it, so a saved setting redraws the whole shell */ #[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, } // the colours without a theme file, from config.toml fn fallback() -> Colors { use config::appearance::fallback as colors; Colors { accent: colors::accent(), on_accent: colors::on_accent(), accent_soft: colors::accent_soft(), flame: colors::flame(), text: colors::text(), dim: colors::dim(), outline: colors::outline(), surface: colors::surface(), surface_high: colors::surface_high(), surface_low: colors::surface_low(), } } 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, // where the colours are going; a reread that lands on them again starts no fade target: Colors, } impl Service for Theme { fn new() -> Self { Self { colors: Animation::new(fallback()), target: fallback() } } /* * 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. * config.toml on its own thread, the theme here. a changed config.toml redraws * everything; the colours fade only if it changed them (its fallback colours) */ fn listen() { reload(true); thread::spawn(|| { for _ in amane::watch_file(&config_file("config.toml").to_string_lossy()) { // one save can arrive as several writes: let it finish, then read once thread::sleep(Duration::from_millis(50)); if config::reread() { touch(); reload(true); } } }); for _ in amane::watch_file(&config_file("theme").to_string_lossy()) { thread::sleep(Duration::from_millis(50)); reload(false); } } } // redraws every view, for a setting changed in the config window pub fn touch() { let _ = Theme::write(); } // the theme file as read last; none until the first read static LAST: Mutex> = Mutex::new(None); /* * the theme file, read anew; `again` also when it's unchanged, for new fallback colours. * a deleted theme file sends no event: the defaults come back at the next start */ fn reload(again: bool) { let theme = fs::read_to_string(config_file("theme")).unwrap_or_default(); let mut last = LAST.lock().unwrap_or_else(PoisonError::into_inner); if last.as_ref() == Some(&theme) && !again { return; } // the colours at startup show at once, later changes cross-fade apply(&theme, last.is_none()); *last = Some(theme); } // $XDG_CONFIG_HOME, or ~/.config pub fn config_home() -> PathBuf { 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/amane/, or under $XDG_CONFIG_HOME pub fn config_file(name: &str) -> PathBuf { config_home().join("amane").join(name) } fn apply(theme: &str, startup: bool) { let mut colors = fallback(); // 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, _ => {} } } if !startup && Theme::read().target == colors { return; } let fade = if startup { 0 } else { config::appearance::colors::transition_ms() }; let mut theme = Theme::write(); theme.target = colors; // 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 = Cell::new(fallback()); } /* * a view's first line: everything it draws this frame uses these colours (and * reading Theme redraws it when they or config.toml change). says whether the media * card takes the cover's */ pub fn begin() -> bool { let theme = Theme::read(); NOW.set(theme.colors.value()); config::appearance::colors::cover_colors() } pub fn now() -> Colors { NOW.get() }