use ratatui::style::Color;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Theme {
pub slug: &'static str,
pub label: &'static str,
pub accent: Color,
pub accent_dim: Color,
pub dim: Color,
pub fg: Color,
pub on_accent: Color,
pub selection: Color,
pub running: Color,
pub sleeping: Color,
pub pending: Color,
}
pub const THEMES: &[Theme] = &[
Theme {
slug: "railway",
label: "Railway",
accent: Color::Rgb(0xc4, 0xa8, 0xff),
accent_dim: Color::Rgb(0x6b, 0x5b, 0x8f),
dim: Color::Rgb(0x8b, 0x84, 0x9c),
fg: Color::Rgb(0xef, 0xec, 0xf6),
on_accent: Color::Rgb(0x18, 0x12, 0x28),
selection: Color::Rgb(0x37, 0x2a, 0x50),
running: Color::Rgb(0x6e, 0xe7, 0xa8),
sleeping: Color::Rgb(0x7d, 0x77, 0x8f),
pending: Color::Rgb(0xf5, 0xc0, 0x6b),
},
Theme {
slug: "terminal",
label: "Terminal (ANSI)",
accent: Color::Cyan,
accent_dim: Color::DarkGray,
dim: Color::DarkGray,
fg: Color::White,
on_accent: Color::Black,
selection: Color::DarkGray,
running: Color::Green,
sleeping: Color::Gray,
pending: Color::Yellow,
},
Theme {
slug: "ember",
label: "Ember",
accent: Color::Rgb(0xf5, 0xa5, 0x24),
accent_dim: Color::Rgb(0x7a, 0x54, 0x1c),
dim: Color::Rgb(0x93, 0x86, 0x76),
fg: Color::Rgb(0xf6, 0xf1, 0xe8),
on_accent: Color::Rgb(0x24, 0x18, 0x06),
selection: Color::Rgb(0x45, 0x2d, 0x10),
running: Color::Rgb(0x9a, 0xd8, 0x6a),
sleeping: Color::Rgb(0x86, 0x7c, 0x6e),
pending: Color::Rgb(0xf5, 0xd2, 0x6b),
},
Theme {
slug: "mono",
label: "Mono",
accent: Color::Rgb(0xe6, 0xe6, 0xe6),
accent_dim: Color::Rgb(0x6a, 0x6a, 0x6a),
dim: Color::Rgb(0x8a, 0x8a, 0x8a),
fg: Color::Rgb(0xf2, 0xf2, 0xf2),
on_accent: Color::Rgb(0x10, 0x10, 0x10),
selection: Color::Rgb(0x33, 0x33, 0x33),
running: Color::Rgb(0xe6, 0xe6, 0xe6),
sleeping: Color::Rgb(0x77, 0x77, 0x77),
pending: Color::Rgb(0xb4, 0xb4, 0xb4),
},
];
impl Theme {
pub fn default_theme() -> &'static Theme {
&THEMES[0]
}
pub fn from_slug(slug: Option<&str>) -> &'static Theme {
slug.and_then(|s| THEMES.iter().find(|t| t.slug == s))
.unwrap_or_else(Self::default_theme)
}
pub fn index(&self) -> usize {
THEMES.iter().position(|t| t.slug == self.slug).unwrap_or(0)
}
pub fn next(&self) -> &'static Theme {
&THEMES[(self.index() + 1) % THEMES.len()]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slugs_are_unique_and_resolvable() {
let mut seen = std::collections::HashSet::new();
for theme in THEMES {
assert!(seen.insert(theme.slug), "duplicate slug {}", theme.slug);
assert_eq!(Theme::from_slug(Some(theme.slug)).slug, theme.slug);
}
}
#[test]
fn an_unknown_or_missing_slug_falls_back_to_railway() {
assert_eq!(Theme::from_slug(None).slug, "railway");
assert_eq!(Theme::from_slug(Some("nonsense")).slug, "railway");
assert_eq!(Theme::default_theme().slug, "railway");
}
#[test]
fn cycling_visits_every_theme_and_wraps() {
let mut theme = Theme::default_theme();
let mut seen = vec![theme.slug];
for _ in 1..THEMES.len() {
theme = theme.next();
seen.push(theme.slug);
}
assert_eq!(seen.len(), THEMES.len());
assert_eq!(theme.next().slug, "railway", "cycling wraps to the first");
}
#[test]
fn on_accent_contrasts_with_accent() {
for theme in THEMES {
assert_ne!(theme.on_accent, theme.accent, "{}", theme.slug);
assert_ne!(theme.fg, theme.selection, "{}", theme.slug);
}
}
}