use ratatui::style::Color;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct Theme {
pub name: &'static str,
pub crust: Color,
pub mantle: Color,
pub base: Color,
pub surface0: Color,
pub surface1: Color,
pub surface2: Color,
pub overlay: Color,
pub overlay0: Color,
pub text: Color,
pub subtext: Color,
pub blue: Color,
pub lavender: Color,
pub sapphire: Color,
pub sky: Color,
pub teal: Color,
pub green: Color,
pub yellow: Color,
pub peach: Color,
pub maroon: Color,
pub red: Color,
pub mauve: Color,
pub pink: Color,
}
impl Theme {
pub const NAMES: [&'static str; 2] = ["frappe", "latte"];
pub fn from_name(name: &str) -> Option<Self> {
match name.trim().to_lowercase().as_str() {
"frappe" | "frappé" => Some(Self::frappe()),
"latte" => Some(Self::latte()),
_ => None,
}
}
pub fn frappe() -> Self {
Self {
name: "frappe",
crust: Color::Rgb(0x23, 0x26, 0x34),
mantle: Color::Rgb(0x29, 0x2c, 0x3c),
base: Color::Rgb(0x30, 0x34, 0x46),
surface0: Color::Rgb(0x41, 0x45, 0x59),
surface1: Color::Rgb(0x51, 0x57, 0x6d),
surface2: Color::Rgb(0x62, 0x68, 0x80),
overlay: Color::Rgb(0x94, 0x9c, 0xbb),
overlay0: Color::Rgb(0x73, 0x79, 0x94),
text: Color::Rgb(0xc6, 0xd0, 0xf5),
subtext: Color::Rgb(0xb5, 0xbf, 0xe2),
blue: Color::Rgb(0x8c, 0xaa, 0xee),
lavender: Color::Rgb(0xba, 0xbb, 0xf1),
sapphire: Color::Rgb(0x85, 0xc1, 0xdc),
sky: Color::Rgb(0x99, 0xd1, 0xdb),
teal: Color::Rgb(0x81, 0xc8, 0xbe),
green: Color::Rgb(0xa6, 0xd1, 0x89),
yellow: Color::Rgb(0xe5, 0xc8, 0x90),
peach: Color::Rgb(0xef, 0x9f, 0x76),
maroon: Color::Rgb(0xea, 0x99, 0x9c),
red: Color::Rgb(0xe7, 0x82, 0x84),
mauve: Color::Rgb(0xca, 0x9e, 0xe6),
pink: Color::Rgb(0xf4, 0xb8, 0xe4),
}
}
pub fn latte() -> Self {
Self {
name: "latte",
crust: Color::Rgb(0xdc, 0xe0, 0xe8),
mantle: Color::Rgb(0xe6, 0xe9, 0xef),
base: Color::Rgb(0xef, 0xf1, 0xf5),
surface0: Color::Rgb(0xcc, 0xd0, 0xda),
surface1: Color::Rgb(0xbc, 0xc0, 0xcc),
surface2: Color::Rgb(0xac, 0xb0, 0xbe),
overlay: Color::Rgb(0x7c, 0x7f, 0x93),
overlay0: Color::Rgb(0x9c, 0xa0, 0xb0),
text: Color::Rgb(0x4c, 0x4f, 0x69),
subtext: Color::Rgb(0x5c, 0x5f, 0x77),
blue: Color::Rgb(0x1e, 0x66, 0xf5),
lavender: Color::Rgb(0x72, 0x87, 0xfd),
sapphire: Color::Rgb(0x20, 0x9f, 0xb5),
sky: Color::Rgb(0x04, 0xa5, 0xe5),
teal: Color::Rgb(0x17, 0x92, 0x99),
green: Color::Rgb(0x40, 0xa0, 0x2b),
yellow: Color::Rgb(0xdf, 0x8e, 0x1d),
peach: Color::Rgb(0xfe, 0x64, 0x0b),
maroon: Color::Rgb(0xe6, 0x45, 0x53),
red: Color::Rgb(0xd2, 0x0f, 0x39),
mauve: Color::Rgb(0x88, 0x39, 0xef),
pink: Color::Rgb(0xea, 0x76, 0xcb),
}
}
pub fn tone(&self, tone: &str) -> Color {
match tone {
"ok" | "healthy" | "valid" => self.green,
"warn" | "wait" => self.yellow,
"fail" | "crit" | "expired" => self.red,
"info" => self.sky,
_ => self.subtext,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::style::Color;
#[test]
fn frappe_core_colors_match_mockup() {
let t = Theme::frappe();
assert_eq!(t.base, Color::Rgb(0x30, 0x34, 0x46));
assert_eq!(t.text, Color::Rgb(0xc6, 0xd0, 0xf5));
assert_eq!(t.mauve, Color::Rgb(0xca, 0x9e, 0xe6));
assert_eq!(t.green, Color::Rgb(0xa6, 0xd1, 0x89));
assert_eq!(t.red, Color::Rgb(0xe7, 0x82, 0x84));
assert_eq!(t.lavender, Color::Rgb(0xba, 0xbb, 0xf1));
}
#[test]
fn tone_maps_states_to_colors() {
let t = Theme::frappe();
assert_eq!(t.tone("ok"), t.green);
assert_eq!(t.tone("warn"), t.yellow);
assert_eq!(t.tone("fail"), t.red);
assert_eq!(t.tone("info"), t.sky);
assert_eq!(t.tone("unknown"), t.subtext);
}
#[test]
fn latte_core_colors_match_official_palette() {
let t = Theme::latte();
assert_eq!(t.base, Color::Rgb(0xef, 0xf1, 0xf5));
assert_eq!(t.text, Color::Rgb(0x4c, 0x4f, 0x69));
assert_eq!(t.mauve, Color::Rgb(0x88, 0x39, 0xef));
assert_eq!(t.green, Color::Rgb(0x40, 0xa0, 0x2b));
assert_eq!(t.red, Color::Rgb(0xd2, 0x0f, 0x39));
assert_eq!(t.lavender, Color::Rgb(0x72, 0x87, 0xfd));
assert_eq!(t.crust, Color::Rgb(0xdc, 0xe0, 0xe8));
}
#[test]
fn from_name_accepts_known_names_case_insensitively() {
assert_eq!(Theme::from_name("frappe").map(|t| t.name), Some("frappe"));
assert_eq!(Theme::from_name("Frappé").map(|t| t.name), Some("frappe"));
assert_eq!(Theme::from_name("FRAPPÉ").map(|t| t.name), Some("frappe"));
assert_eq!(Theme::from_name("latte").map(|t| t.name), Some("latte"));
assert_eq!(Theme::from_name("LATTE").map(|t| t.name), Some("latte"));
assert_eq!(Theme::from_name(" latte ").map(|t| t.name), Some("latte"));
}
#[test]
fn from_name_rejects_unknown_names() {
assert!(Theme::from_name("mocha").is_none());
assert!(Theme::from_name("macchiato").is_none());
assert!(Theme::from_name("").is_none());
assert!(Theme::from_name("dark").is_none());
}
#[test]
fn latte_differs_from_frappe_and_names_are_canonical() {
let f = Theme::frappe();
let l = Theme::latte();
assert_eq!(f.name, "frappe");
assert_eq!(l.name, "latte");
assert_ne!(f.base, l.base);
assert_ne!(f.text, l.text);
assert_ne!(f.mantle, l.mantle);
}
}