pub mod builders;
mod custom;
mod themes;
use ratatui::style::Color;
pub use themes::ThemeData;
pub use themes::{
BUILTIN_THEMES, CATPPUCCIN_MOCHA, DRACULA, GRUVBOX_DARK, NO_COLOR, NORD, ONE_DARK, ROSE_PINE,
SOLARIZED_DARK, TAILWIND_DARK, TERMINAL_NATIVE, TOKYO_NIGHT,
};
#[allow(non_upper_case_globals, missing_docs, clippy::wildcard_imports)]
mod aliases {
use super::themes::*;
pub const CatppuccinMocha: ThemeData = CATPPUCCIN_MOCHA;
pub const Dracula: ThemeData = DRACULA;
pub const Nord: ThemeData = NORD;
pub const GruvboxDark: ThemeData = GRUVBOX_DARK;
pub const OneDark: ThemeData = ONE_DARK;
pub const SolarizedDark: ThemeData = SOLARIZED_DARK;
pub const TailwindDark: ThemeData = TAILWIND_DARK;
pub const TokyoNight: ThemeData = TOKYO_NIGHT;
pub const RosePine: ThemeData = ROSE_PINE;
pub const TerminalNative: ThemeData = TERMINAL_NATIVE;
pub const NoColor: ThemeData = NO_COLOR;
}
pub use aliases::*;
pub use custom::CustomTheme;
pub use builders::ThemeExt;
pub trait Theme: Send + Sync {
fn name(&self) -> &str;
fn id(&self) -> &str;
fn accent(&self) -> Color;
fn accent_dim(&self) -> Color;
fn text(&self) -> Color;
fn text_dim(&self) -> Color;
fn text_bright(&self) -> Color;
fn success(&self) -> Color;
fn error(&self) -> Color;
fn warning(&self) -> Color;
fn info(&self) -> Color;
fn diff_added(&self) -> Color;
fn diff_removed(&self) -> Color;
fn diff_context(&self) -> Color;
fn border(&self) -> Color;
fn surface(&self) -> Color;
fn block_file_read(&self) -> Color {
self.text_dim()
}
fn block_file_edit(&self) -> Color {
self.diff_added()
}
fn block_command(&self) -> Color {
self.text_bright()
}
fn block_thinking(&self) -> Color {
self.text_dim()
}
fn block_pass(&self) -> Color {
self.success()
}
fn block_fail(&self) -> Color {
self.error()
}
fn block_system(&self) -> Color {
self.text_dim()
}
fn indicator_pending(&self) -> Color {
self.text_dim()
}
fn indicator_running(&self) -> Color {
self.warning()
}
fn indicator_passed(&self) -> Color {
self.success()
}
fn indicator_failed(&self) -> Color {
self.error()
}
fn indicator_skipped(&self) -> Color {
self.text_dim()
}
}
#[must_use]
pub fn resolve_theme(id: &str) -> Box<dyn Theme> {
if no_color_active() {
return Box::new(NO_COLOR);
}
for theme in BUILTIN_THEMES {
if theme.id == id {
return Box::new(theme.clone());
}
}
Box::new(CATPPUCCIN_MOCHA)
}
#[must_use]
pub fn builtin_themes() -> Vec<Box<dyn Theme>> {
BUILTIN_THEMES
.iter()
.map(|t| Box::new(t.clone()) as Box<dyn Theme>)
.collect()
}
#[must_use]
pub fn available_theme_ids() -> Vec<&'static str> {
BUILTIN_THEMES.iter().map(|t| t.id).collect()
}
#[must_use]
pub fn no_color_active() -> bool {
std::env::var_os("NO_COLOR").is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_known_theme() {
let theme = resolve_theme("dracula");
assert_eq!(theme.id(), "dracula");
assert_eq!(theme.name(), "Dracula");
}
#[test]
fn resolve_unknown_falls_back() {
let theme = resolve_theme("nonexistent");
assert_eq!(theme.id(), "catppuccin");
}
#[test]
fn resolve_empty_string_falls_back() {
let theme = resolve_theme("");
assert_eq!(theme.id(), "catppuccin");
}
#[test]
fn all_builtin_themes_have_unique_ids() {
let ids: Vec<&str> = BUILTIN_THEMES.iter().map(|t| t.id).collect();
let mut dedup = ids.clone();
dedup.sort_unstable();
dedup.dedup();
assert_eq!(ids.len(), dedup.len(), "duplicate theme IDs found");
}
#[test]
fn available_ids_match_builtins() {
let ids = available_theme_ids();
let themes = builtin_themes();
assert_eq!(ids.len(), themes.len());
for (id, theme) in ids.iter().zip(themes.iter()) {
assert_eq!(*id, theme.id());
}
}
#[test]
fn resolve_all_known_ids() {
for id in available_theme_ids() {
let theme = resolve_theme(id);
assert_eq!(theme.id(), id, "resolve_theme({id}) returned wrong theme");
}
}
#[test]
fn derived_methods_use_core_slots() {
let t = CatppuccinMocha;
assert_eq!(t.block_pass(), t.success());
assert_eq!(t.block_fail(), t.error());
assert_eq!(t.block_file_read(), t.text_dim());
assert_eq!(t.indicator_passed(), t.success());
assert_eq!(t.indicator_failed(), t.error());
}
#[test]
fn no_color_theme_is_all_reset() {
let t = NoColor;
assert_eq!(t.accent(), Color::Reset);
assert_eq!(t.text(), Color::Reset);
assert_eq!(t.error(), Color::Reset);
assert_eq!(t.border(), Color::Reset);
}
#[test]
fn every_builtin_has_non_empty_name_and_id() {
for theme in BUILTIN_THEMES {
assert!(!theme.name.is_empty());
assert!(!theme.id.is_empty());
assert!(!theme.id.contains(' '), "id '{}' has spaces", theme.id);
}
}
#[test]
fn every_builtin_has_distinct_status_colors() {
for theme in BUILTIN_THEMES {
assert_ne!(
theme.success, theme.error,
"theme '{}': success == error",
theme.id
);
}
}
#[test]
fn every_builtin_has_distinct_diff_colors() {
for theme in BUILTIN_THEMES {
assert_ne!(
theme.diff_added, theme.diff_removed,
"theme '{}': added == removed",
theme.id
);
}
}
#[test]
fn every_builtin_surface_differs_from_text() {
for theme in BUILTIN_THEMES {
assert_ne!(
theme.surface, theme.text,
"theme '{}': surface == text",
theme.id
);
}
}
#[test]
fn terminal_native_uses_named_colors() {
assert_eq!(TerminalNative.success, Color::Green);
assert_eq!(TerminalNative.error, Color::Red);
assert_eq!(TerminalNative.accent, Color::Blue);
}
#[test]
fn tokyo_night_has_blue_accent() {
assert_eq!(TokyoNight.accent, Color::Rgb(122, 162, 247));
assert_ne!(TokyoNight.accent, TokyoNight.error);
}
#[test]
fn rose_pine_has_rose_accent() {
assert_eq!(RosePine.accent, Color::Rgb(235, 188, 186));
assert_ne!(RosePine.accent, RosePine.error);
}
#[test]
fn custom_theme_implements_trait() {
let custom = CustomTheme {
name: "Test".to_owned(),
id: "test".to_owned(),
accent: Color::Magenta,
accent_dim: Color::DarkGray,
text: Color::White,
text_dim: Color::Gray,
text_bright: Color::White,
success: Color::Green,
error: Color::Red,
warning: Color::Yellow,
info: Color::Cyan,
diff_added: Color::Green,
diff_removed: Color::Red,
diff_context: Color::DarkGray,
border: Color::DarkGray,
surface: Color::Black,
};
let theme: &dyn Theme = &custom;
assert_eq!(theme.name(), "Test");
assert_eq!(theme.accent(), Color::Magenta);
assert_eq!(theme.block_pass(), theme.success());
}
#[test]
fn builtin_count_is_correct() {
assert_eq!(BUILTIN_THEMES.len(), 10, "expected 10 built-in themes");
assert_eq!(available_theme_ids().len(), 10);
}
}