//! Colours from the web themes (same palettes as the web UI, 24-bit). use ratatui::style::Color; use serde_json::Value; use std::sync::OnceLock; const PALETTES: &str = include_str!("palettes.json"); #[derive(Debug, Clone, Copy)] #[allow(dead_code)] pub struct Theme { pub bg0: Color, pub bg1: Color, pub bg2: Color, pub bg3: Color, pub fg0: Color, pub fg1: Color, pub fg2: Color, pub fg3: Color, pub muted: Color, pub accent: Color, pub accent2: Color, pub on_accent: Color, pub red: Color, pub green: Color, pub blue: Color, pub aqua: Color, pub purple: Color, pub keyword: Color, pub string: Color, pub comment: Color, } fn hex(v: &Value, key: &str, fallback: Color) -> Color { v[key] .as_str() .and_then(|s| { let s = s.trim_start_matches('#'); Some(Color::Rgb(u8::from_str_radix(s.get(0..2)?, 16).ok()?, u8::from_str_radix(s.get(2..4)?, 16).ok()?, u8::from_str_radix(s.get(4..6)?, 16).ok()?)) }) .unwrap_or(fallback) } pub fn names() -> Vec { let all: Value = serde_json::from_str(PALETTES).unwrap_or(Value::Null); let mut out: Vec = all.as_object().map(|o| o.keys().filter_map(|k| k.rsplit_once('-').map(|(b, _)| b.to_string())).collect()).unwrap_or_default(); out.dedup(); out } /// Is the terminal background dark? (COLORFGBG, else assume dark) fn terminal_dark() -> bool { std::env::var("COLORFGBG").ok().and_then(|v| v.rsplit(';').next().and_then(|b| b.parse::().ok())).map(|bg| bg < 7 || bg == 8).unwrap_or(true) } impl Theme { /// `base` e.g. "gruvbox", `mode` "light" | "dark" | "auto". pub fn load(base: &str, mode: &str) -> Self { let dark = match mode { "light" => false, "dark" => true, _ => terminal_dark(), }; let all: Value = serde_json::from_str(PALETTES).unwrap_or(Value::Null); let key = format!("{base}-{}", if dark { "dark" } else { "light" }); let p = all.get(&key).or_else(|| all.get(if dark { "gruvbox-dark" } else { "gruvbox-light" })).cloned().unwrap_or(Value::Null); let d = |k: &str, f: Color| hex(&p, k, f); Self { bg0: d("bg0", Color::Reset), bg1: d("bg1", Color::Reset), bg2: d("bg2", Color::DarkGray), bg3: d("bg3", Color::DarkGray), fg0: d("fg0", Color::White), fg1: d("fg1", Color::Reset), fg2: d("fg2", Color::Gray), fg3: d("fg3", Color::Gray), muted: d("muted", Color::DarkGray), accent: d("accent", Color::Yellow), accent2: d("accent2", Color::LightYellow), on_accent: d("on-accent", Color::Black), red: d("red", Color::Red), green: d("green", Color::Green), blue: d("blue", Color::Blue), aqua: d("aqua", Color::Cyan), purple: d("purple", Color::Magenta), keyword: d("syntax-keyword", Color::Red), string: d("syntax-string", Color::Green), comment: d("syntax-comment", Color::DarkGray), } } } static CURRENT: OnceLock> = OnceLock::new(); pub fn set(t: Theme) { let lock = CURRENT.get_or_init(|| std::sync::RwLock::new(t)); *lock.write().unwrap() = t; } pub fn get() -> Theme { *CURRENT.get_or_init(|| std::sync::RwLock::new(Theme::load("gruvbox", "auto"))).read().unwrap() }