Skip to main content

ratatui_themekit/
lib.rs

1//! # ratatui-themekit
2//!
3//! Semantic theme system for [ratatui](https://ratatui.rs).
4//!
5//! ```rust
6//! use ratatui::text::Line;
7//! use ratatui_themekit::{Theme, ThemeExt, CatppuccinMocha};
8//!
9//! let t = CatppuccinMocha;
10//!
11//! let header = Line::from(vec![
12//!     t.fg_accent("App v1.0").bold().build(),
13//!     t.fg_border(" | ").build(),
14//!     t.fg_success("Ready").build(),
15//! ]);
16//! ```
17
18pub mod builders;
19mod resolve;
20mod theme_trait;
21mod themes;
22
23// ── Core exports ──���────────────────────────────────────────────
24
25pub use builders::ThemeExt;
26pub use builders::{
27    GaugeStyles, InputStyles, ListStyles, NotificationStyles, ScrollbarStyles, StateStyles,
28    TabStyles, TableStyles, ThemedBar, ThemedBlock, ThemedLine, ThemedSpan, ThemedStatusLine,
29    zebra_rows,
30};
31pub use theme_trait::Theme;
32pub use themes::CustomTheme;
33pub use themes::ThemeData;
34
35// ── Resolution ─────────────────────────────────────────────────
36
37pub use resolve::{
38    available_theme_ids, builtin_themes, default_theme, no_color_active, resolve_theme,
39};
40
41// ── Theme constants (SCREAMING_SNAKE) ──────────────────────────
42
43pub use themes::{
44    BUILTIN_THEMES, CATPPUCCIN_MOCHA, DRACULA, GRUVBOX_DARK, NO_COLOR, NORD, ONE_DARK, ROSE_PINE,
45    SOLARIZED_DARK, TAILWIND_DARK, TERMINAL_NATIVE, TOKYO_NIGHT,
46};
47
48// ── Theme aliases (PascalCase for ergonomic usage) ─────────────
49
50#[allow(non_upper_case_globals, missing_docs, clippy::wildcard_imports)]
51mod aliases {
52    use super::themes::*;
53    pub const CatppuccinMocha: ThemeData = CATPPUCCIN_MOCHA;
54    pub const Dracula: ThemeData = DRACULA;
55    pub const Nord: ThemeData = NORD;
56    pub const GruvboxDark: ThemeData = GRUVBOX_DARK;
57    pub const OneDark: ThemeData = ONE_DARK;
58    pub const SolarizedDark: ThemeData = SOLARIZED_DARK;
59    pub const TailwindDark: ThemeData = TAILWIND_DARK;
60    pub const TokyoNight: ThemeData = TOKYO_NIGHT;
61    pub const RosePine: ThemeData = ROSE_PINE;
62    pub const TerminalNative: ThemeData = TERMINAL_NATIVE;
63    pub const NoColor: ThemeData = NO_COLOR;
64}
65pub use aliases::*;
66
67// ── Integration tests ─��────────────────────────────────────────
68
69#[cfg(test)]
70mod tests {
71    use ratatui::style::Color;
72
73    use super::*;
74
75    #[test]
76    fn all_builtin_themes_have_unique_ids() {
77        let ids: Vec<&str> = BUILTIN_THEMES.iter().map(|t| t.id).collect();
78        let mut dedup = ids.clone();
79        dedup.sort_unstable();
80        dedup.dedup();
81        assert_eq!(ids.len(), dedup.len());
82    }
83
84    #[test]
85    fn every_builtin_has_non_empty_name_and_id() {
86        for theme in BUILTIN_THEMES {
87            assert!(!theme.name.is_empty());
88            assert!(!theme.id.is_empty());
89            assert!(!theme.id.contains(' '));
90        }
91    }
92
93    #[test]
94    fn every_builtin_has_distinct_status_colors() {
95        for theme in BUILTIN_THEMES {
96            assert_ne!(theme.success, theme.error, "{}: success == error", theme.id);
97        }
98    }
99
100    #[test]
101    fn every_builtin_has_distinct_diff_colors() {
102        for theme in BUILTIN_THEMES {
103            assert_ne!(
104                theme.diff_added, theme.diff_removed,
105                "{}: added == removed",
106                theme.id
107            );
108        }
109    }
110
111    #[test]
112    fn every_builtin_surface_differs_from_text() {
113        for theme in BUILTIN_THEMES {
114            assert_ne!(theme.surface, theme.text, "{}: surface == text", theme.id);
115        }
116    }
117
118    #[test]
119    fn derived_methods_use_core_slots() {
120        let t = CatppuccinMocha;
121        assert_eq!(t.block_pass(), t.success());
122        assert_eq!(t.block_fail(), t.error());
123        assert_eq!(t.indicator_passed(), t.success());
124    }
125
126    #[test]
127    fn no_color_is_all_reset() {
128        assert_eq!(NoColor.accent, Color::Reset);
129        assert_eq!(NoColor.text, Color::Reset);
130        assert_eq!(NoColor.error, Color::Reset);
131    }
132
133    #[test]
134    fn tokyo_night_has_blue_accent() {
135        assert_eq!(TokyoNight.accent, Color::Rgb(122, 162, 247));
136    }
137
138    #[test]
139    fn rose_pine_has_rose_accent() {
140        assert_eq!(RosePine.accent, Color::Rgb(235, 188, 186));
141    }
142
143    #[test]
144    fn theme_data_display() {
145        assert_eq!(CatppuccinMocha.to_string(), "Catppuccin Mocha (catppuccin)");
146    }
147
148    #[test]
149    fn theme_data_equality() {
150        assert_eq!(CatppuccinMocha, CATPPUCCIN_MOCHA);
151        assert_ne!(CatppuccinMocha, Dracula);
152    }
153
154    #[test]
155    fn theme_data_copy() {
156        let a = CatppuccinMocha;
157        let b = a; // Copy
158        assert_eq!(a, b);
159    }
160
161    #[test]
162    fn custom_theme_with_trait() {
163        let t = CustomTheme {
164            name: "Test".to_owned(),
165            id: "test".to_owned(),
166            accent: Color::Magenta,
167            accent_dim: Color::DarkGray,
168            text: Color::White,
169            text_dim: Color::Gray,
170            text_bright: Color::White,
171            success: Color::Green,
172            error: Color::Red,
173            warning: Color::Yellow,
174            info: Color::Cyan,
175            diff_added: Color::Green,
176            diff_removed: Color::Red,
177            diff_context: Color::DarkGray,
178            border: Color::DarkGray,
179            surface: Color::Black,
180            background: Color::Reset,
181        };
182        let theme: &dyn Theme = &t;
183        assert_eq!(theme.accent(), Color::Magenta);
184        assert_eq!(theme.block_pass(), theme.success());
185    }
186
187    #[test]
188    fn background_has_real_color() {
189        assert_eq!(CatppuccinMocha.background(), Color::Rgb(30, 30, 46));
190        assert_eq!(Dracula.background(), Color::Rgb(40, 42, 54));
191        // TerminalNative and NoColor keep Reset (respect terminal default)
192        assert_eq!(TerminalNative.background(), Color::Reset);
193        assert_eq!(NoColor.background(), Color::Reset);
194    }
195
196    #[test]
197    fn style_base_combines_bg_and_fg() {
198        let t = CatppuccinMocha;
199        let base = t.style_base();
200        assert_eq!(base.bg, Some(Color::Rgb(30, 30, 46)));
201        assert_eq!(base.fg, Some(t.text()));
202    }
203
204    #[test]
205    fn stripe_blends_background_and_surface() {
206        // Catppuccin: bg=(30,30,46), surface=(49,50,68) → 70% blend → (43,44,61)
207        let t = CatppuccinMocha;
208        let stripe = t.stripe();
209        assert_ne!(stripe, t.background(), "stripe must differ from background");
210        assert_ne!(stripe, t.surface(), "stripe must differ from surface");
211        assert_eq!(stripe, Color::Rgb(43, 44, 61));
212    }
213
214    #[test]
215    fn stripe_falls_back_to_surface_for_reset_bg() {
216        // NoColor: bg=Reset → stripe=surface (Reset)
217        assert_eq!(NoColor.stripe(), NoColor.surface());
218        // TerminalNative: bg=Reset → stripe=surface (Black)
219        assert_eq!(TerminalNative.stripe(), TerminalNative.surface());
220    }
221}