use ratatui::style::{Color, Modifier, Style};
use std::sync::RwLock;
#[derive(Debug, Clone)]
pub struct ThemePalette {
pub bg: Color,
pub bg_highlight: Color,
pub surface: Color,
pub text_primary: Color,
pub text_dim: Color,
pub accent: Color,
pub accent_secondary: Color,
pub highlight: Color,
pub warm: Color,
pub success: Color,
pub error: Color,
pub vol_filled: Color,
pub vol_empty: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThemeName {
Retrowave,
CatppuccinMocha,
CatppuccinMacchiato,
CatppuccinFrappe,
CatppuccinLatte,
}
impl ThemeName {
pub const ALL: &[ThemeName] = &[
ThemeName::Retrowave,
ThemeName::CatppuccinMocha,
ThemeName::CatppuccinMacchiato,
ThemeName::CatppuccinFrappe,
ThemeName::CatppuccinLatte,
];
pub fn label(self) -> &'static str {
match self {
ThemeName::Retrowave => "Retrowave",
ThemeName::CatppuccinMocha => "Catppuccin Mocha",
ThemeName::CatppuccinMacchiato => "Catppuccin Macchiato",
ThemeName::CatppuccinFrappe => "Catppuccin Frappé",
ThemeName::CatppuccinLatte => "Catppuccin Latte",
}
}
pub fn from_key(key: &str) -> Self {
match key {
"Retrowave" => ThemeName::Retrowave,
"CatppuccinMocha" => ThemeName::CatppuccinMocha,
"CatppuccinMacchiato" => ThemeName::CatppuccinMacchiato,
"CatppuccinFrappe" => ThemeName::CatppuccinFrappe,
"CatppuccinLatte" => ThemeName::CatppuccinLatte,
_ => ThemeName::Retrowave,
}
}
pub fn key(self) -> &'static str {
match self {
ThemeName::Retrowave => "Retrowave",
ThemeName::CatppuccinMocha => "CatppuccinMocha",
ThemeName::CatppuccinMacchiato => "CatppuccinMacchiato",
ThemeName::CatppuccinFrappe => "CatppuccinFrappe",
ThemeName::CatppuccinLatte => "CatppuccinLatte",
}
}
pub fn palette(self) -> ThemePalette {
match self {
ThemeName::Retrowave => palette_retrowave(),
ThemeName::CatppuccinMocha => palette_mocha(),
ThemeName::CatppuccinMacchiato => palette_macchiato(),
ThemeName::CatppuccinFrappe => palette_frappe(),
ThemeName::CatppuccinLatte => palette_latte(),
}
}
pub fn next(self) -> Self {
let all = Self::ALL;
let idx = all.iter().position(|&t| t == self).unwrap_or(0);
all[(idx + 1) % all.len()]
}
}
fn palette_retrowave() -> ThemePalette {
ThemePalette {
bg: Color::Rgb(0, 0, 0),
bg_highlight: Color::Rgb(20, 15, 40),
surface: Color::Rgb(10, 8, 18),
text_primary: Color::Rgb(224, 212, 255),
text_dim: Color::Rgb(120, 100, 160),
accent: Color::Rgb(255, 46, 151), accent_secondary: Color::Rgb(255, 106, 193), highlight: Color::Rgb(0, 240, 255), warm: Color::Rgb(255, 140, 66),
success: Color::Rgb(57, 255, 20), error: Color::Rgb(255, 60, 60),
vol_filled: Color::Rgb(0, 240, 255),
vol_empty: Color::Rgb(40, 30, 60),
}
}
fn palette_mocha() -> ThemePalette {
ThemePalette {
bg: Color::Rgb(30, 30, 46), bg_highlight: Color::Rgb(69, 71, 90), surface: Color::Rgb(49, 50, 68),
text_primary: Color::Rgb(205, 214, 244), text_dim: Color::Rgb(166, 173, 200),
accent: Color::Rgb(203, 166, 247), accent_secondary: Color::Rgb(245, 194, 231), highlight: Color::Rgb(116, 199, 236), warm: Color::Rgb(250, 179, 135),
success: Color::Rgb(166, 227, 161), error: Color::Rgb(243, 139, 168),
vol_filled: Color::Rgb(137, 180, 250), vol_empty: Color::Rgb(88, 91, 112), }
}
fn palette_macchiato() -> ThemePalette {
ThemePalette {
bg: Color::Rgb(36, 39, 58), bg_highlight: Color::Rgb(73, 77, 100), surface: Color::Rgb(54, 58, 79),
text_primary: Color::Rgb(202, 211, 245), text_dim: Color::Rgb(165, 173, 203),
accent: Color::Rgb(198, 160, 246), accent_secondary: Color::Rgb(245, 189, 230), highlight: Color::Rgb(125, 196, 228), warm: Color::Rgb(245, 169, 127),
success: Color::Rgb(166, 218, 149), error: Color::Rgb(237, 135, 150),
vol_filled: Color::Rgb(138, 173, 244), vol_empty: Color::Rgb(91, 96, 120), }
}
fn palette_frappe() -> ThemePalette {
ThemePalette {
bg: Color::Rgb(48, 52, 70), bg_highlight: Color::Rgb(81, 87, 109), surface: Color::Rgb(65, 69, 89),
text_primary: Color::Rgb(198, 208, 245), text_dim: Color::Rgb(165, 173, 206),
accent: Color::Rgb(202, 158, 230), accent_secondary: Color::Rgb(244, 184, 228), highlight: Color::Rgb(133, 193, 220), warm: Color::Rgb(239, 159, 118),
success: Color::Rgb(166, 209, 137), error: Color::Rgb(231, 130, 132),
vol_filled: Color::Rgb(140, 170, 238), vol_empty: Color::Rgb(98, 104, 128), }
}
fn palette_latte() -> ThemePalette {
ThemePalette {
bg: Color::Rgb(239, 241, 245), bg_highlight: Color::Rgb(188, 192, 204), surface: Color::Rgb(204, 208, 218),
text_primary: Color::Rgb(76, 79, 105), text_dim: Color::Rgb(108, 111, 133),
accent: Color::Rgb(136, 57, 239), accent_secondary: Color::Rgb(234, 118, 203), highlight: Color::Rgb(32, 159, 181), warm: Color::Rgb(254, 100, 11),
success: Color::Rgb(64, 160, 43), error: Color::Rgb(210, 15, 57),
vol_filled: Color::Rgb(30, 102, 245), vol_empty: Color::Rgb(172, 176, 190), }
}
static ACTIVE_PALETTE: RwLock<Option<ThemePalette>> = RwLock::new(None);
pub fn set_active(name: ThemeName) {
let palette = name.palette();
let mut lock = ACTIVE_PALETTE.write().unwrap();
*lock = Some(palette);
}
fn active() -> ThemePalette {
let lock = ACTIVE_PALETTE.read().unwrap();
lock.clone().unwrap_or_else(palette_retrowave)
}
pub fn bg() -> Color { active().bg }
pub fn surface_color() -> Color { active().surface }
pub fn accent() -> Color { active().accent }
pub fn accent_secondary() -> Color { active().accent_secondary }
pub fn highlight() -> Color { active().highlight }
pub fn warm() -> Color { active().warm }
pub fn text() -> Style {
let p = active();
Style::default().fg(p.text_primary).bg(p.bg)
}
pub fn dim() -> Style {
let p = active();
Style::default().fg(p.text_dim).bg(p.bg)
}
pub fn neon() -> Style {
let p = active();
Style::default().fg(p.accent).bg(p.bg).add_modifier(Modifier::BOLD)
}
pub fn cyan() -> Style {
let p = active();
Style::default().fg(p.highlight).bg(p.bg)
}
pub fn selected() -> Style {
let p = active();
Style::default()
.fg(p.highlight)
.bg(p.bg_highlight)
.add_modifier(Modifier::BOLD)
}
pub fn playing() -> Style {
let p = active();
Style::default().fg(p.success).bg(p.bg).add_modifier(Modifier::BOLD)
}
pub fn error() -> Style {
let p = active();
Style::default().fg(p.error).bg(p.bg)
}
pub fn border() -> Style {
let p = active();
Style::default().fg(p.accent).bg(p.bg)
}
pub fn title() -> Style {
let p = active();
Style::default().fg(p.accent).bg(p.bg).add_modifier(Modifier::BOLD)
}
pub fn scanline() -> Style {
let p = active();
Style::default().fg(p.text_primary).bg(p.surface)
}
pub fn vol_filled() -> Style {
let p = active();
Style::default().fg(p.vol_filled).bg(p.bg)
}
pub fn vol_empty() -> Style {
let p = active();
Style::default().fg(p.vol_empty).bg(p.bg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_theme_cycle_wraps_around() {
let last = *ThemeName::ALL.last().unwrap();
let first = *ThemeName::ALL.first().unwrap();
assert_eq!(last.next(), first);
}
#[test]
fn test_theme_cycle_advances() {
assert_eq!(ThemeName::Retrowave.next(), ThemeName::CatppuccinMocha);
assert_eq!(ThemeName::CatppuccinMocha.next(), ThemeName::CatppuccinMacchiato);
}
#[test]
fn test_theme_key_roundtrip() {
for &theme in ThemeName::ALL {
let key = theme.key();
let restored = ThemeName::from_key(key);
assert_eq!(theme, restored, "Key roundtrip failed for {:?}", theme);
}
}
#[test]
fn test_theme_from_key_unknown_defaults_retrowave() {
assert_eq!(ThemeName::from_key("NonExistentTheme"), ThemeName::Retrowave);
assert_eq!(ThemeName::from_key(""), ThemeName::Retrowave);
}
#[test]
fn test_all_themes_have_labels() {
for &theme in ThemeName::ALL {
assert!(!theme.label().is_empty());
}
}
}