use colored::Colorize;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy)]
pub struct Rgb(pub u8, pub u8, pub u8);
pub fn fg(s: &str, c: Rgb) -> colored::ColoredString {
s.truecolor(c.0, c.1, c.2)
}
pub fn badge(label: &str, bg: Rgb, fg_color: Rgb) -> String {
format!(
"{}",
label
.on_truecolor(bg.0, bg.1, bg.2)
.truecolor(fg_color.0, fg_color.1, fg_color.2)
.bold()
)
}
#[derive(Debug, Clone)]
pub struct Theme {
pub name: &'static str,
pub brand: Rgb,
pub green: Rgb,
pub yellow: Rgb,
pub red: Rgb,
pub blue: Rgb,
pub teal: Rgb,
pub text: Rgb,
pub subtext: Rgb,
pub overlay: Rgb,
pub surface: Rgb,
}
pub const MOCHA: Theme = Theme {
name: "mocha",
brand: Rgb(250, 179, 135), green: Rgb(166, 218, 149), yellow: Rgb(249, 226, 175), red: Rgb(243, 139, 168), blue: Rgb(137, 180, 250), teal: Rgb(148, 226, 213), text: Rgb(205, 214, 244), subtext: Rgb(147, 153, 178), overlay: Rgb(108, 112, 134), surface: Rgb(69, 71, 90), };
pub const LATTE: Theme = Theme {
name: "latte",
brand: Rgb(254, 100, 11), green: Rgb(64, 160, 43), yellow: Rgb(223, 142, 29), red: Rgb(210, 15, 57), blue: Rgb(30, 102, 245), teal: Rgb(23, 146, 153), text: Rgb(76, 79, 105), subtext: Rgb(108, 111, 133), overlay: Rgb(140, 143, 161), surface: Rgb(188, 192, 204), };
pub const FRAPPE: Theme = Theme {
name: "frappe",
brand: Rgb(239, 159, 118), green: Rgb(166, 209, 137), yellow: Rgb(229, 200, 144), red: Rgb(231, 130, 132), blue: Rgb(140, 170, 238), teal: Rgb(129, 200, 190), text: Rgb(198, 208, 245), subtext: Rgb(148, 156, 187), overlay: Rgb(115, 121, 148), surface: Rgb(65, 69, 89), };
pub const MACCHIATO: Theme = Theme {
name: "macchiato",
brand: Rgb(245, 169, 127), green: Rgb(166, 218, 149), yellow: Rgb(238, 212, 159), red: Rgb(237, 135, 150), blue: Rgb(138, 173, 244), teal: Rgb(139, 213, 202), text: Rgb(202, 211, 245), subtext: Rgb(148, 155, 187), overlay: Rgb(110, 115, 141), surface: Rgb(54, 58, 79), };
pub const TOKYO_NIGHT: Theme = Theme {
name: "tokyo-night",
brand: Rgb(255, 158, 100), green: Rgb(158, 206, 106), yellow: Rgb(224, 175, 104), red: Rgb(247, 118, 142), blue: Rgb(122, 162, 247), teal: Rgb(115, 218, 202), text: Rgb(192, 202, 245), subtext: Rgb(134, 150, 187), overlay: Rgb(86, 95, 137), surface: Rgb(52, 59, 88), };
pub const MINIMAL: Theme = Theme {
name: "minimal",
brand: Rgb(204, 102, 0), green: Rgb(80, 200, 80),
yellow: Rgb(220, 180, 50),
red: Rgb(220, 70, 70),
blue: Rgb(80, 150, 220),
teal: Rgb(80, 190, 190),
text: Rgb(220, 220, 220),
subtext: Rgb(140, 140, 140),
overlay: Rgb(80, 80, 80),
surface: Rgb(55, 55, 55),
};
static THEME: OnceLock<Theme> = OnceLock::new();
pub fn init(cli_theme: Option<&str>, config_theme: Option<&str>) {
let chosen = if let Some(t) = cli_theme {
t
} else if let Ok(val) = std::env::var("NONO_THEME") {
Box::leak(val.into_boxed_str())
} else {
config_theme.unwrap_or("mocha")
};
if !is_valid(chosen) {
tracing::warn!(
"unknown theme '{}', using mocha. available: {}",
chosen,
available_themes().join(", "),
);
}
let theme = resolve(chosen);
tracing::debug!("theme: {}", theme.name);
let _ = THEME.set(theme);
}
pub fn current() -> &'static Theme {
THEME.get().unwrap_or(&MOCHA)
}
fn resolve(name: &str) -> Theme {
match name.to_lowercase().as_str() {
"mocha" | "catppuccin-mocha" | "catppuccin" => MOCHA,
"latte" | "catppuccin-latte" => LATTE,
"frappe" | "catppuccin-frappe" => FRAPPE,
"macchiato" | "catppuccin-macchiato" => MACCHIATO,
"tokyo-night" | "tokyo" | "tokyonight" => TOKYO_NIGHT,
"minimal" | "plain" => MINIMAL,
_ => MOCHA,
}
}
pub fn available_themes() -> &'static [&'static str] {
&[
"mocha",
"latte",
"frappe",
"macchiato",
"tokyo-night",
"minimal",
]
}
pub fn is_valid(name: &str) -> bool {
matches!(
name.to_lowercase().as_str(),
"mocha"
| "catppuccin-mocha"
| "catppuccin"
| "latte"
| "catppuccin-latte"
| "frappe"
| "catppuccin-frappe"
| "macchiato"
| "catppuccin-macchiato"
| "tokyo-night"
| "tokyo"
| "tokyonight"
| "minimal"
| "plain"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_known_themes() {
assert_eq!(resolve("mocha").name, "mocha");
assert_eq!(resolve("latte").name, "latte");
assert_eq!(resolve("frappe").name, "frappe");
assert_eq!(resolve("macchiato").name, "macchiato");
assert_eq!(resolve("tokyo-night").name, "tokyo-night");
assert_eq!(resolve("minimal").name, "minimal");
}
#[test]
fn test_resolve_aliases() {
assert_eq!(resolve("catppuccin").name, "mocha");
assert_eq!(resolve("catppuccin-latte").name, "latte");
assert_eq!(resolve("tokyo").name, "tokyo-night");
assert_eq!(resolve("plain").name, "minimal");
}
#[test]
fn test_resolve_unknown_falls_back() {
assert_eq!(resolve("nonexistent").name, "mocha");
}
#[test]
fn test_current_before_init() {
assert_eq!(current().name, "mocha");
}
#[test]
fn test_available_themes_not_empty() {
let themes = available_themes();
assert!(!themes.is_empty());
for name in themes {
let t = resolve(name);
assert_eq!(t.name, *name);
}
}
#[test]
fn test_all_color_slots_used() {
let t = &MOCHA;
let _brand = t.brand;
let _green = t.green;
let _yellow = t.yellow;
let _red = t.red;
let _blue = t.blue;
let _teal = t.teal;
let _text = t.text;
let _subtext = t.subtext;
let _overlay = t.overlay;
let _surface = t.surface;
assert!(!t.name.is_empty());
}
}