1use ratatui::style::{Color, Modifier, Style};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ThemeKind {
8 DefaultDark,
9 Nord,
10 CatppuccinMocha,
11 Dracula,
12}
13
14impl ThemeKind {
15 pub const ALL: &'static [ThemeKind] = &[
16 ThemeKind::DefaultDark,
17 ThemeKind::Nord,
18 ThemeKind::CatppuccinMocha,
19 ThemeKind::Dracula,
20 ];
21
22 pub fn name(&self) -> &'static str {
23 match self {
24 ThemeKind::DefaultDark => "default_dark",
25 ThemeKind::Nord => "nord",
26 ThemeKind::CatppuccinMocha => "catppuccin_mocha",
27 ThemeKind::Dracula => "dracula",
28 }
29 }
30
31 pub fn display_name(&self) -> &'static str {
32 match self {
33 ThemeKind::DefaultDark => "Default Dark",
34 ThemeKind::Nord => "Nord",
35 ThemeKind::CatppuccinMocha => "Catppuccin Mocha",
36 ThemeKind::Dracula => "Dracula",
37 }
38 }
39
40 pub fn from_name(name: &str) -> Self {
41 let s = name.to_lowercase();
42 let s = s.trim();
43 match s {
44 "nord" => ThemeKind::Nord,
45 "catppuccin_mocha" | "catppuccin" | "mocha" | "catppuccin mocha" => {
46 ThemeKind::CatppuccinMocha
47 }
48 "dracula" => ThemeKind::Dracula,
49 "default_dark" | "default" | "default dark" => ThemeKind::DefaultDark,
50 _ => ThemeKind::DefaultDark,
51 }
52 }
53
54 pub fn next(self) -> Self {
55 let i = Self::ALL.iter().position(|&k| k == self).unwrap_or(0);
56 let next = (i + 1) % Self::ALL.len();
57 Self::ALL[next]
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct Theme {
64 pub name: String,
65 pub accent: Color,
66 pub accent_dim: Color,
67 pub bg: Color,
68 pub bg_highlight: Color,
69 pub bg_panel: Color,
70 pub fg: Color,
71 pub fg_dim: Color,
72 pub fg_muted: Color,
73 pub border: Color,
74 pub border_focused: Color,
75 pub tab_active_bg: Color,
77 pub tab_active_fg: Color,
79 pub error: Color,
80 pub warning: Color,
81 pub success: Color,
82 pub info: Color,
83 pub keyword: Color,
85 pub function: Color,
86 pub type_: Color,
87 pub string: Color,
88 pub number: Color,
89 pub comment: Color,
90}
91
92impl Theme {
93 pub fn default_dark() -> Self {
94 Self {
95 name: "Default Dark".into(),
96 accent: Color::Rgb(78, 191, 113), accent_dim: Color::Rgb(45, 110, 65),
98 bg: Color::Rgb(24, 24, 24),
99 bg_highlight: Color::Rgb(45, 45, 45),
100 bg_panel: Color::Rgb(32, 32, 32),
101 fg: Color::Rgb(230, 230, 230),
102 fg_dim: Color::Rgb(195, 195, 200),
103 fg_muted: Color::Rgb(140, 140, 145),
104 border: Color::Rgb(60, 60, 60),
105 border_focused: Color::Rgb(78, 191, 113),
106 tab_active_bg: Color::Rgb(88, 66, 134), tab_active_fg: Color::Rgb(255, 255, 255), error: Color::Rgb(244, 67, 54),
109 warning: Color::Rgb(255, 152, 0),
110 success: Color::Rgb(76, 175, 80),
111 info: Color::Rgb(33, 150, 243),
112 keyword: Color::Rgb(198, 120, 221), function: Color::Rgb(97, 175, 239), type_: Color::Rgb(229, 192, 123), string: Color::Rgb(152, 195, 121), number: Color::Rgb(209, 154, 102), comment: Color::Rgb(92, 99, 112), }
119 }
120
121 pub fn nord() -> Self {
122 Self {
123 name: "Nord".into(),
124 accent: Color::Rgb(136, 192, 208), accent_dim: Color::Rgb(94, 129, 172), bg: Color::Rgb(46, 52, 64), bg_highlight: Color::Rgb(59, 66, 82), bg_panel: Color::Rgb(67, 76, 94), fg: Color::Rgb(236, 239, 244), fg_dim: Color::Rgb(229, 233, 240), fg_muted: Color::Rgb(136, 142, 156), border: Color::Rgb(76, 86, 106), border_focused: Color::Rgb(136, 192, 208),
134 tab_active_bg: Color::Rgb(65, 89, 122), tab_active_fg: Color::Rgb(255, 255, 255), error: Color::Rgb(191, 97, 106), warning: Color::Rgb(235, 203, 139), success: Color::Rgb(163, 190, 140), info: Color::Rgb(129, 161, 193), keyword: Color::Rgb(180, 142, 173), function: Color::Rgb(136, 192, 208), type_: Color::Rgb(235, 203, 139), string: Color::Rgb(163, 190, 140), number: Color::Rgb(208, 135, 112), comment: Color::Rgb(76, 86, 106), }
147 }
148
149 pub fn catppuccin_mocha() -> Self {
151 Self {
152 name: "Catppuccin Mocha".into(),
153 accent: Color::Rgb(166, 227, 161), accent_dim: Color::Rgb(116, 199, 236), bg: Color::Rgb(30, 30, 46), bg_highlight: Color::Rgb(49, 50, 68), bg_panel: Color::Rgb(36, 39, 58), fg: Color::Rgb(205, 214, 244), fg_dim: Color::Rgb(205, 214, 244), fg_muted: Color::Rgb(147, 153, 178), border: Color::Rgb(69, 71, 90), border_focused: Color::Rgb(166, 227, 161),
163 tab_active_bg: Color::Rgb(126, 87, 194), tab_active_fg: Color::Rgb(255, 255, 255), error: Color::Rgb(243, 139, 168), warning: Color::Rgb(249, 226, 175), success: Color::Rgb(166, 227, 161), info: Color::Rgb(137, 180, 250), keyword: Color::Rgb(203, 166, 247), function: Color::Rgb(137, 180, 250), type_: Color::Rgb(249, 226, 175), string: Color::Rgb(166, 227, 161), number: Color::Rgb(250, 179, 135), comment: Color::Rgb(108, 112, 134), }
176 }
177
178 pub fn dracula() -> Self {
180 Self {
181 name: "Dracula".into(),
182 accent: Color::Rgb(80, 250, 123), accent_dim: Color::Rgb(139, 233, 253), bg: Color::Rgb(40, 42, 54), bg_highlight: Color::Rgb(68, 71, 90), bg_panel: Color::Rgb(33, 34, 44), fg: Color::Rgb(248, 248, 242), fg_dim: Color::Rgb(230, 230, 235),
189 fg_muted: Color::Rgb(139, 153, 195),
190 border: Color::Rgb(68, 71, 90), border_focused: Color::Rgb(80, 250, 123),
192 tab_active_bg: Color::Rgb(118, 92, 168), tab_active_fg: Color::Rgb(255, 255, 255), error: Color::Rgb(255, 85, 85), warning: Color::Rgb(255, 184, 108), success: Color::Rgb(80, 250, 123), info: Color::Rgb(139, 233, 253), keyword: Color::Rgb(255, 121, 198), function: Color::Rgb(80, 250, 123), type_: Color::Rgb(139, 233, 253), string: Color::Rgb(241, 250, 140), number: Color::Rgb(189, 147, 249), comment: Color::Rgb(98, 114, 164), }
205 }
206
207 pub fn from_kind(kind: ThemeKind) -> Self {
208 match kind {
209 ThemeKind::DefaultDark => Self::default_dark(),
210 ThemeKind::Nord => Self::nord(),
211 ThemeKind::CatppuccinMocha => Self::catppuccin_mocha(),
212 ThemeKind::Dracula => Self::dracula(),
213 }
214 }
215
216 pub fn from_name(name: &str) -> Self {
217 Self::from_kind(ThemeKind::from_name(name))
218 }
219
220 pub fn kind(&self) -> ThemeKind {
221 ThemeKind::from_name(&self.name)
222 }
223
224 pub fn style_accent(&self) -> Style {
226 Style::default().fg(self.accent)
227 }
228
229 pub fn style_accent_bold(&self) -> Style {
230 Style::default()
231 .fg(self.accent)
232 .add_modifier(Modifier::BOLD)
233 }
234
235 pub fn style_normal(&self) -> Style {
236 Style::default().fg(self.fg)
237 }
238
239 pub fn style_dim(&self) -> Style {
240 Style::default().fg(self.fg_dim)
241 }
242
243 pub fn style_muted(&self) -> Style {
244 Style::default().fg(self.fg_muted)
245 }
246
247 pub fn style_highlight(&self) -> Style {
248 Style::default().bg(self.bg_highlight)
249 }
250
251 pub fn style_selected(&self) -> Style {
253 Style::default()
254 .fg(self.fg)
255 .bg(self.bg_highlight)
256 .add_modifier(Modifier::BOLD)
257 }
258
259 pub fn style_border(&self) -> Style {
260 Style::default().fg(self.border)
261 }
262
263 pub fn style_tab_active(&self) -> Style {
265 Style::default()
266 .fg(self.tab_active_fg)
267 .bg(self.tab_active_bg)
268 .add_modifier(Modifier::BOLD)
269 }
270
271 pub fn style_border_glow(&self) -> Style {
273 Style::default().fg(self.accent).add_modifier(Modifier::DIM)
274 }
275
276 pub fn style_border_focused(&self) -> Style {
277 Style::default().fg(self.border_focused)
278 }
279
280 pub fn style_error(&self) -> Style {
281 Style::default().fg(self.error)
282 }
283
284 pub fn style_warning(&self) -> Style {
285 Style::default().fg(self.warning)
286 }
287
288 pub fn style_success(&self) -> Style {
289 Style::default().fg(self.success)
290 }
291
292 pub fn style_info(&self) -> Style {
293 Style::default().fg(self.info)
294 }
295
296 pub fn style_keyword(&self) -> Style {
297 Style::default().fg(self.keyword)
298 }
299
300 pub fn style_function(&self) -> Style {
301 Style::default().fg(self.function)
302 }
303
304 pub fn style_type(&self) -> Style {
305 Style::default().fg(self.type_)
306 }
307
308 pub fn style_string(&self) -> Style {
309 Style::default().fg(self.string)
310 }
311
312 pub fn style_number(&self) -> Style {
313 Style::default().fg(self.number)
314 }
315
316 pub fn style_comment(&self) -> Style {
317 Style::default().fg(self.comment)
318 }
319}
320
321impl Default for Theme {
322 fn default() -> Self {
323 Self::default_dark()
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_theme_kind_from_name() {
333 assert_eq!(ThemeKind::from_name("default"), ThemeKind::DefaultDark);
334 assert_eq!(ThemeKind::from_name("default_dark"), ThemeKind::DefaultDark);
335 assert_eq!(ThemeKind::from_name("nord"), ThemeKind::Nord);
336 assert_eq!(ThemeKind::from_name("Nord"), ThemeKind::Nord);
337 assert_eq!(
338 ThemeKind::from_name("catppuccin_mocha"),
339 ThemeKind::CatppuccinMocha
340 );
341 assert_eq!(ThemeKind::from_name("mocha"), ThemeKind::CatppuccinMocha);
342 assert_eq!(ThemeKind::from_name("dracula"), ThemeKind::Dracula);
343 assert_eq!(ThemeKind::from_name("unknown"), ThemeKind::DefaultDark);
344 }
345
346 #[test]
347 fn test_theme_kind_next_cycles() {
348 let first = ThemeKind::DefaultDark;
349 let second = first.next();
350 assert_ne!(first, second);
351 let mut k = ThemeKind::DefaultDark;
352 for _ in 0..ThemeKind::ALL.len() {
353 k = k.next();
354 }
355 assert_eq!(k, ThemeKind::DefaultDark);
356 }
357
358 #[test]
359 fn test_theme_from_kind() {
360 let t = Theme::from_kind(ThemeKind::Nord);
361 assert_eq!(t.name, "Nord");
362 let t2 = Theme::from_name("dracula");
363 assert_eq!(t2.name, "Dracula");
364 }
365}