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, // the open media card and the equalizer take the cover's colours as their background pub cover_colors: bool, // how opaque the shell's surfaces are, 0.05 to 1; below 1 the desktop shows through glass: f32, } impl Service for Theme { fn new() -> Self { Self { colors: Animation::new(PINK_ON_PURPLE), cover_colors: false, glass: 1.0, } } /* * 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> = 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/, 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"); theme.glass = setting("glass").and_then(|glass| glass.parse::().ok()).map_or(1.0, |glass| glass.clamp(0.05, 1.0)); // 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 and glass of the frame being drawn, set by the view that draws it thread_local! { static NOW: Cell = const { Cell::new(PINK_ON_PURPLE) }; static GLASS: Cell = const { Cell::new(1.0) }; } /* * 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()); GLASS.set(theme.glass); theme.cover_colors } pub fn now() -> Colors { NOW.get() } // the surfaces' opacity this frame, 0.05 to 1 pub fn glass() -> f32 { GLASS.get() }