pub mod builders;
mod custom;
mod themes;
use ratatui::style::Color;
pub use themes::ThemeData;
#[allow(unused_imports)]
pub use 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::*;
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);
}
match id {
"dracula" => Box::new(DRACULA),
"nord" => Box::new(NORD),
"gruvbox" => Box::new(GRUVBOX_DARK),
"one-dark" => Box::new(ONE_DARK),
"solarized" => Box::new(SOLARIZED_DARK),
"tailwind" => Box::new(TAILWIND_DARK),
"tokyo-night" => Box::new(TOKYO_NIGHT),
"rose-pine" => Box::new(ROSE_PINE),
"terminal" => Box::new(TERMINAL_NATIVE),
"no-color" => Box::new(NO_COLOR),
_ => Box::new(CATPPUCCIN_MOCHA),
}
}
#[must_use]
pub fn builtin_themes() -> Vec<Box<dyn Theme>> {
vec![
Box::new(CATPPUCCIN_MOCHA),
Box::new(DRACULA),
Box::new(NORD),
Box::new(GRUVBOX_DARK),
Box::new(ONE_DARK),
Box::new(SOLARIZED_DARK),
Box::new(TAILWIND_DARK),
Box::new(TOKYO_NIGHT),
Box::new(ROSE_PINE),
Box::new(TERMINAL_NATIVE),
]
}
#[must_use]
pub fn available_theme_ids() -> Vec<&'static str> {
vec![
"catppuccin",
"dracula",
"nord",
"gruvbox",
"one-dark",
"solarized",
"tailwind",
"tokyo-night",
"rose-pine",
"terminal",
]
}
#[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 all_builtin_themes_have_unique_ids() {
let themes = builtin_themes();
let ids: Vec<&str> = themes.iter().map(|t| t.id()).collect();
let mut dedup = ids.clone();
dedup.sort_unstable();
dedup.dedup();
assert_eq!(ids.len(), dedup.len());
}
#[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 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(), ratatui::style::Color::Reset);
assert_eq!(t.text(), ratatui::style::Color::Reset);
assert_eq!(t.error(), ratatui::style::Color::Reset);
assert_eq!(t.border(), ratatui::style::Color::Reset);
}
#[test]
fn every_builtin_has_distinct_status_colors() {
for theme in builtin_themes() {
assert_ne!(
theme.success(),
theme.error(),
"theme '{}' has same success and 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 '{}' has same diff_added and diff_removed",
theme.id()
);
}
}
#[test]
fn terminal_native_uses_named_colors() {
let t = TerminalNative;
assert_eq!(t.success(), ratatui::style::Color::Green);
assert_eq!(t.error(), ratatui::style::Color::Red);
assert_eq!(t.accent(), ratatui::style::Color::Blue);
}
#[test]
fn tailwind_dark_uses_palette_constants() {
let t = TailwindDark;
assert_ne!(t.accent(), ratatui::style::Color::Reset);
assert_ne!(t.success(), ratatui::style::Color::Reset);
}
#[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 every_builtin_has_non_empty_name_and_id() {
for theme in builtin_themes() {
assert!(!theme.name().is_empty(), "theme name must not be empty");
assert!(!theme.id().is_empty(), "theme id must not be empty");
assert!(
!theme.id().contains(' '),
"theme id '{}' must not contain spaces",
theme.id()
);
}
}
#[test]
fn every_builtin_surface_differs_from_text() {
for theme in builtin_themes() {
assert_ne!(
theme.surface(),
theme.text(),
"theme '{}' has same surface and text — focus highlight would be invisible",
theme.id()
);
}
}
#[test]
fn resolve_empty_string_falls_back() {
let theme = resolve_theme("");
assert_eq!(theme.id(), "catppuccin");
}
#[test]
fn custom_theme_implements_trait() {
let custom = CustomTheme {
name: "Test".to_owned(),
id: "test".to_owned(),
accent: ratatui::style::Color::Magenta,
accent_dim: ratatui::style::Color::DarkGray,
text: ratatui::style::Color::White,
text_dim: ratatui::style::Color::Gray,
text_bright: ratatui::style::Color::White,
success: ratatui::style::Color::Green,
error: ratatui::style::Color::Red,
warning: ratatui::style::Color::Yellow,
info: ratatui::style::Color::Cyan,
diff_added: ratatui::style::Color::Green,
diff_removed: ratatui::style::Color::Red,
diff_context: ratatui::style::Color::DarkGray,
border: ratatui::style::Color::DarkGray,
surface: ratatui::style::Color::Black,
};
let theme: &dyn Theme = &custom;
assert_eq!(theme.name(), "Test");
assert_eq!(theme.id(), "test");
assert_eq!(theme.accent(), ratatui::style::Color::Magenta);
assert_eq!(theme.block_pass(), theme.success());
}
}