mod builtin;
use ratatui::prelude::Color;
#[derive(Debug, Clone)]
pub struct Theme {
pub accent: Color,
pub accent_dim: Color,
pub success: Color,
pub error: Color,
pub warning: Color,
pub info: Color,
pub text: Color,
pub text_dim: Color,
pub text_muted: Color,
pub bg: Color,
pub bg_surface: Color,
pub border: Color,
pub border_busy: Color,
pub user: Color,
pub assistant: Color,
pub system: Color,
pub md_header: Color,
pub md_code: Color,
pub md_code_bg: Color,
pub md_bullet: Color,
pub status_fg: Color,
pub status_bg: Color,
pub status_busy_fg: Color,
pub status_busy_bg: Color,
pub status_line2_fg: Color,
pub status_line2_bg: Color,
pub spinner: Color,
pub spinner_dim: Color,
pub diff_add_fg: Color,
pub diff_add_bg: Color,
pub diff_remove_fg: Color,
pub diff_remove_bg: Color,
pub is_dark: bool,
}
pub struct ThemePalette {
pub bg: Color,
pub surface: Color,
pub fg: Color,
pub fg_dim: Color,
pub fg_muted: Color,
pub accent: Color,
pub accent2: Color,
pub green: Color,
pub red: Color,
pub yellow: Color,
pub blue: Color,
pub orange: Color,
pub is_dark: bool,
}
pub struct ThemeFamily {
pub name: &'static str,
pub dark_id: &'static str,
pub light_id: Option<&'static str>,
}
impl Theme {
pub fn from_palette(p: &ThemePalette) -> Self {
Self {
accent: p.accent,
accent_dim: p.fg_muted,
success: p.green,
error: p.red,
warning: p.yellow,
info: p.blue,
text: p.fg,
text_dim: p.fg_dim,
text_muted: p.fg_muted,
bg: p.bg,
bg_surface: p.surface,
border: p.fg_muted,
border_busy: p.orange,
user: p.green,
assistant: p.accent,
system: p.yellow,
md_header: p.accent2,
md_code: p.blue,
md_code_bg: p.surface,
md_bullet: p.accent,
status_fg: p.bg,
status_bg: p.accent,
status_busy_fg: p.bg,
status_busy_bg: p.orange,
status_line2_fg: p.fg_muted,
status_line2_bg: p.surface,
spinner: p.accent2,
spinner_dim: p.fg_muted,
diff_add_fg: p.green,
diff_add_bg: if p.is_dark {
Color::Rgb(23, 52, 30)
} else {
Color::Rgb(215, 250, 215)
},
diff_remove_fg: p.red,
diff_remove_bg: if p.is_dark {
Color::Rgb(59, 20, 25)
} else {
Color::Rgb(253, 220, 220)
},
is_dark: p.is_dark,
}
}
pub fn from_name(name: &str) -> Self {
match name {
"dracula" => Self::dracula(),
"catppuccin" | "catppuccin-mocha" => Self::catppuccin_mocha(),
"catppuccin-latte" => Self::catppuccin_latte(),
"tokyo-night" | "tokyonight" => Self::tokyo_night(),
"tokyo-day" => Self::tokyo_day(),
"nord" => Self::nord(),
"gruvbox" => Self::gruvbox(),
"gruvbox-light" => Self::gruvbox_light(),
"solarized-dark" => Self::solarized_dark(),
"solarized-light" => Self::solarized_light(),
"monokai" => Self::monokai(),
"one-dark" => Self::one_dark(),
"one-light" => Self::one_light(),
"everforest-dark" => Self::everforest_dark(),
"everforest-light" => Self::everforest_light(),
"rose-pine" => Self::rose_pine(),
"rose-pine-dawn" => Self::rose_pine_dawn(),
"material-dark" => Self::material_dark(),
"material-light" => Self::material_light(),
"ayu-dark" => Self::ayu_dark(),
"ayu-light" => Self::ayu_light(),
"palenight" => Self::palenight(),
"cobalt2" => Self::cobalt2(),
"horizon" => Self::horizon(),
_ => Self::default_theme(),
}
}
pub fn families() -> &'static [ThemeFamily] {
&[
ThemeFamily {
name: "Default",
dark_id: "default",
light_id: None,
},
ThemeFamily {
name: "Dracula",
dark_id: "dracula",
light_id: None,
},
ThemeFamily {
name: "Catppuccin",
dark_id: "catppuccin-mocha",
light_id: Some("catppuccin-latte"),
},
ThemeFamily {
name: "Tokyo Night",
dark_id: "tokyo-night",
light_id: Some("tokyo-day"),
},
ThemeFamily {
name: "Nord",
dark_id: "nord",
light_id: None,
},
ThemeFamily {
name: "Gruvbox",
dark_id: "gruvbox",
light_id: Some("gruvbox-light"),
},
ThemeFamily {
name: "Solarized",
dark_id: "solarized-dark",
light_id: Some("solarized-light"),
},
ThemeFamily {
name: "Monokai",
dark_id: "monokai",
light_id: None,
},
ThemeFamily {
name: "One",
dark_id: "one-dark",
light_id: Some("one-light"),
},
ThemeFamily {
name: "Everforest",
dark_id: "everforest-dark",
light_id: Some("everforest-light"),
},
ThemeFamily {
name: "Rose Pine",
dark_id: "rose-pine",
light_id: Some("rose-pine-dawn"),
},
ThemeFamily {
name: "Material",
dark_id: "material-dark",
light_id: Some("material-light"),
},
ThemeFamily {
name: "Ayu",
dark_id: "ayu-dark",
light_id: Some("ayu-light"),
},
ThemeFamily {
name: "Palenight",
dark_id: "palenight",
light_id: None,
},
ThemeFamily {
name: "Cobalt2",
dark_id: "cobalt2",
light_id: None,
},
ThemeFamily {
name: "Horizon",
dark_id: "horizon",
light_id: None,
},
]
}
pub fn thinking_verbs(&self) -> &'static [&'static str] {
match self.spinner {
Color::Rgb(255, 121, 198) => &[
"Conjuring",
"Hexing",
"Casting",
"Brewing",
"Enchanting",
"Summoning",
],
Color::Rgb(180, 190, 254) => &[
"Purring",
"Steeping",
"Noodling",
"Pondering",
"Daydreaming",
"Simmering",
],
Color::Rgb(114, 135, 253) => &[
"Sipping", "Blending", "Steeping", "Whipping", "Brewing", "Frothing",
],
Color::Rgb(187, 154, 247) => &[
"Jacking in",
"Compiling",
"Overclocking",
"Syncing",
"Executing",
"Infiltrating",
],
Color::Rgb(46, 125, 233) => &[
"Sightseeing",
"Wandering",
"Exploring",
"Mapping",
"Illuminating",
"Discovering",
],
Color::Rgb(143, 188, 187) => &[
"Crystallizing",
"Drifting",
"Sculpting",
"Mapping",
"Tracing",
"Chiseling",
],
Color::Rgb(254, 128, 25) => &[
"Forging",
"Concocting",
"Stoking",
"Smelting",
"Hammering",
"Kindling",
],
Color::Rgb(214, 93, 14) => &[
"Crafting",
"Shaping",
"Burnishing",
"Carving",
"Tempering",
"Polishing",
],
Color::Rgb(42, 161, 152) => &[
"Observing",
"Illuminating",
"Clarifying",
"Focusing",
"Resolving",
"Rendering",
],
Color::Rgb(38, 139, 210) => &[
"Brightening",
"Polishing",
"Refining",
"Clarifying",
"Composing",
"Articulating",
],
Color::Rgb(166, 226, 46) => &[
"Parsing",
"Lexing",
"Tokenizing",
"Compiling",
"Transpiling",
"Optimizing",
],
Color::Rgb(198, 120, 221) => &[
"Thinking",
"Drafting",
"Revising",
"Composing",
"Reviewing",
"Refining",
],
Color::Rgb(64, 120, 242) => &[
"Considering",
"Outlining",
"Composing",
"Polishing",
"Finalizing",
"Presenting",
],
Color::Rgb(167, 192, 128) => &[
"Growing",
"Rooting",
"Branching",
"Leafing",
"Blooming",
"Flourishing",
],
Color::Rgb(141, 161, 1) => &[
"Sprouting",
"Tending",
"Cultivating",
"Harvesting",
"Grafting",
"Ripening",
],
Color::Rgb(196, 167, 231) => &[
"Blossoming",
"Dreaming",
"Weaving",
"Flowing",
"Whispering",
"Blooming",
],
Color::Rgb(144, 122, 169) => &[
"Awakening",
"Unfurling",
"Glowing",
"Dawning",
"Flowering",
"Emerging",
],
Color::Rgb(128, 203, 196) => &[
"Building",
"Designing",
"Structuring",
"Layering",
"Rendering",
"Composing",
],
Color::Rgb(97, 130, 184) => &[
"Blueprinting",
"Sketching",
"Drafting",
"Prototyping",
"Finalizing",
"Deploying",
],
Color::Rgb(255, 143, 64) => &[
"Igniting", "Burning", "Fueling", "Firing", "Blazing", "Glowing",
],
Color::Rgb(250, 141, 62) => &[
"Warming",
"Brightening",
"Sunning",
"Radiating",
"Illuminating",
"Gleaming",
],
Color::Rgb(199, 146, 234) => &[
"Moonlighting",
"Stargazing",
"Drifting",
"Floating",
"Wandering",
"Dreaming",
],
Color::Rgb(158, 255, 255) => &[
"Scanning", "Probing", "Pinging", "Tracing", "Mapping", "Routing",
],
Color::Rgb(233, 86, 120) => &[
"Dusking",
"Fading",
"Glimmering",
"Twilighting",
"Settling",
"Merging",
],
_ => &[
"Synthesizing",
"Analyzing",
"Reasoning",
"Computing",
"Processing",
"Evaluating",
],
}
}
pub fn available() -> &'static [&'static str] {
&[
"default",
"dracula",
"catppuccin-mocha",
"catppuccin-latte",
"tokyo-night",
"tokyo-day",
"nord",
"gruvbox",
"gruvbox-light",
"solarized-dark",
"solarized-light",
"monokai",
"one-dark",
"one-light",
"everforest-dark",
"everforest-light",
"rose-pine",
"rose-pine-dawn",
"material-dark",
"material-light",
"ayu-dark",
"ayu-light",
"palenight",
"cobalt2",
"horizon",
]
}
pub fn default_theme() -> Self {
let p = ThemePalette {
bg: Color::Reset,
surface: Color::Rgb(38, 38, 38),
fg: Color::White,
fg_dim: Color::Gray,
fg_muted: Color::Rgb(128, 128, 128),
accent: Color::Cyan,
accent2: Color::Magenta,
green: Color::Green,
red: Color::Red,
yellow: Color::Yellow,
blue: Color::Blue,
orange: Color::Yellow,
is_dark: true,
};
Self::from_palette(&p)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_name_default() {
let t = Theme::from_name("default");
assert!(matches!(t.accent, Color::Cyan));
}
#[test]
fn test_from_name_unknown_falls_back() {
let t = Theme::from_name("nonexistent");
assert!(matches!(t.accent, Color::Cyan)); }
#[test]
fn test_all_themes_construct() {
for name in Theme::available() {
let _ = Theme::from_name(name);
}
}
#[test]
fn test_families_cover_all_ids() {
let available: std::collections::HashSet<&str> =
Theme::available().iter().copied().collect();
for family in Theme::families() {
assert!(
available.contains(family.dark_id),
"dark_id '{}' not in available()",
family.dark_id
);
if let Some(light_id) = family.light_id {
assert!(
available.contains(light_id),
"light_id '{}' not in available()",
light_id
);
}
}
}
#[test]
fn test_thinking_verbs_all_themes() {
for name in Theme::available() {
let t = Theme::from_name(name);
let verbs = t.thinking_verbs();
assert!(!verbs.is_empty(), "no verbs for theme '{name}'");
}
}
}