1use ratatui::style::{Color, Modifier, Style};
7use ratatui::widgets::BorderType;
8use std::str::FromStr;
9use std::sync::RwLock;
10
11pub const LIST_HIGHLIGHT_SYMBOL: &str = "ยป ";
13
14static THEME: RwLock<Theme> = RwLock::new(Theme {
16 theme_type: ThemeType::Dark,
17 primary: Color::Cyan,
18 secondary: Color::Magenta,
19 tertiary: Color::Blue,
20 success: Color::Green,
21 warning: Color::Yellow,
22 error: Color::Red,
23 text: Color::White,
24 text_muted: Color::DarkGray,
25 text_dimmed: Color::Cyan,
26 text_emphasis: Color::Yellow,
27 border: Color::DarkGray,
28 border_focused: Color::Cyan,
29 highlight_bg: Color::DarkGray,
30 background: Color::Reset,
31 dim_bg: Color::Black,
32 border_type: BorderType::Plain,
33 border_focused_type: BorderType::Thick,
34 dialog_border_type: BorderType::Double,
35});
36
37pub fn init_theme(theme_type: ThemeType) {
39 let mut theme = THEME
42 .write()
43 .unwrap_or_else(std::sync::PoisonError::into_inner);
44 *theme = Theme::new(theme_type);
45}
46
47pub fn theme() -> Theme {
49 THEME
51 .read()
52 .unwrap_or_else(std::sync::PoisonError::into_inner)
53 .clone()
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum ThemeType {
59 #[default]
60 Dark,
61 Light,
62 NoColor,
64 Midnight,
66 SolarizedDark,
68 SolarizedLight,
70}
71
72impl ThemeType {
73 #[must_use]
75 pub fn name(&self) -> &'static str {
76 match self {
77 ThemeType::Dark => "Dark",
78 ThemeType::Light => "Light",
79 ThemeType::NoColor => "No Color",
80 ThemeType::Midnight => "Midnight",
81 ThemeType::SolarizedDark => "Solarized Dark",
82 ThemeType::SolarizedLight => "Solarized Light",
83 }
84 }
85
86 #[must_use]
88 pub fn to_config_string(&self) -> &'static str {
89 match self {
90 ThemeType::Dark => "dark",
91 ThemeType::Light => "light",
92 ThemeType::NoColor => "nocolor",
93 ThemeType::Midnight => "midnight",
94 ThemeType::SolarizedDark => "solarized-dark",
95 ThemeType::SolarizedLight => "solarized-light",
96 }
97 }
98
99 #[must_use]
101 pub fn all() -> &'static [ThemeType] {
102 &[
103 ThemeType::Dark,
104 ThemeType::Light,
105 ThemeType::Midnight,
106 ThemeType::SolarizedDark,
107 ThemeType::SolarizedLight,
108 ThemeType::NoColor,
109 ]
110 }
111}
112
113impl FromStr for ThemeType {
114 type Err = ();
115
116 fn from_str(s: &str) -> Result<Self, Self::Err> {
117 Ok(match s.to_lowercase().as_str() {
118 "light" => ThemeType::Light,
119 "midnight" => ThemeType::Midnight,
120 "solarized-dark" | "solarized_dark" | "solarized" => ThemeType::SolarizedDark,
121 "solarized-light" | "solarized_light" => ThemeType::SolarizedLight,
122 "nocolor" | "no-color" | "no_color" => ThemeType::NoColor,
123 _ => ThemeType::Dark,
124 })
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct Theme {
131 pub theme_type: ThemeType,
133
134 pub primary: Color,
137 pub secondary: Color,
139 pub tertiary: Color,
141
142 pub success: Color,
145 pub warning: Color,
147 pub error: Color,
149
150 pub text: Color,
153 pub text_muted: Color,
155 pub text_dimmed: Color,
157 pub text_emphasis: Color,
159
160 pub border: Color,
163 pub border_focused: Color,
165 pub highlight_bg: Color,
167 pub background: Color,
169 pub dim_bg: Color,
171
172 pub border_type: BorderType,
175 pub border_focused_type: BorderType,
177 pub dialog_border_type: BorderType,
179}
180
181impl Theme {
182 #[must_use]
183 pub fn new(theme_type: ThemeType) -> Self {
184 match theme_type {
185 ThemeType::Dark => Self::dark(),
186 ThemeType::Light => Self::light(),
187 ThemeType::NoColor => Self::no_color(),
188 ThemeType::Midnight => Self::midnight(),
189 ThemeType::SolarizedDark => Self::solarized_dark(),
190 ThemeType::SolarizedLight => Self::solarized_light(),
191 }
192 }
193
194 #[must_use]
196 pub fn midnight() -> Self {
197 Self {
198 theme_type: ThemeType::Midnight,
199
200 primary: Color::Rgb(0, 150, 200), secondary: Color::Rgb(170, 50, 170), tertiary: Color::Rgb(60, 100, 200), success: Color::Rgb(40, 160, 60), warning: Color::Rgb(200, 130, 0), error: Color::Rgb(200, 40, 40), text: Color::Rgb(200, 200, 200), text_muted: Color::Rgb(128, 128, 128), text_dimmed: Color::Rgb(100, 100, 100),
214 text_emphasis: Color::Rgb(220, 140, 0), border: Color::Rgb(100, 100, 100), border_focused: Color::Rgb(0, 150, 200), highlight_bg: Color::Rgb(60, 60, 60), background: Color::Rgb(20, 20, 20),
221 dim_bg: Color::Rgb(40, 40, 40),
222
223 border_type: BorderType::Rounded,
224 border_focused_type: BorderType::Thick,
225 dialog_border_type: BorderType::Double,
226 }
227 }
228
229 #[must_use]
231 pub fn solarized_dark() -> Self {
232 Self {
233 theme_type: ThemeType::SolarizedDark,
234
235 primary: Color::Rgb(38, 139, 210), secondary: Color::Rgb(108, 113, 196), tertiary: Color::Rgb(42, 161, 152), success: Color::Rgb(133, 153, 0), warning: Color::Rgb(181, 137, 0), error: Color::Rgb(220, 50, 47), text: Color::Rgb(131, 148, 150), text_muted: Color::Rgb(88, 110, 117), text_dimmed: Color::Rgb(7, 54, 66), text_emphasis: Color::Rgb(147, 161, 161), border: Color::Rgb(88, 110, 117), border_focused: Color::Rgb(38, 139, 210), highlight_bg: Color::Rgb(7, 54, 66), background: Color::Rgb(0, 43, 54), dim_bg: Color::Rgb(7, 54, 66), border_type: BorderType::Rounded,
277 border_focused_type: BorderType::Thick,
278 dialog_border_type: BorderType::Double,
279 }
280 }
281
282 #[must_use]
284 pub fn solarized_light() -> Self {
285 Self {
286 theme_type: ThemeType::SolarizedLight,
287
288 primary: Color::Rgb(38, 139, 210), secondary: Color::Rgb(108, 113, 196), tertiary: Color::Rgb(42, 161, 152), success: Color::Rgb(133, 153, 0), warning: Color::Rgb(203, 75, 22), error: Color::Rgb(220, 50, 47), text: Color::Rgb(101, 123, 131), text_muted: Color::Rgb(147, 161, 161), text_dimmed: Color::Rgb(238, 232, 213), text_emphasis: Color::Rgb(88, 110, 117), border: Color::Rgb(147, 161, 161), border_focused: Color::Rgb(38, 139, 210), highlight_bg: Color::Rgb(238, 232, 213), background: Color::Rgb(253, 246, 227), dim_bg: Color::Rgb(238, 232, 213), border_type: BorderType::Rounded,
322 border_focused_type: BorderType::Thick,
323 dialog_border_type: BorderType::Double,
324 }
325 }
326
327 #[must_use]
329 pub fn dark() -> Self {
330 Self {
331 theme_type: ThemeType::Dark,
332
333 primary: Color::Cyan,
335 secondary: Color::Magenta,
336 tertiary: Color::Blue,
337
338 success: Color::Green,
340 warning: Color::Yellow,
341 error: Color::Red,
342
343 text: Color::Reset,
345 text_muted: Color::DarkGray,
346 text_dimmed: Color::Cyan,
347 text_emphasis: Color::Yellow,
348
349 border: Color::Cyan,
351 border_focused: Color::LightBlue,
352 highlight_bg: Color::DarkGray,
353 background: Color::Reset,
354 dim_bg: Color::Reset,
355
356 border_type: BorderType::Plain,
357 border_focused_type: BorderType::Thick,
358 dialog_border_type: BorderType::Double,
359 }
360 }
361
362 #[must_use]
364 pub fn light() -> Self {
365 Self {
366 theme_type: ThemeType::Light,
367
368 primary: Color::Blue,
370 secondary: Color::Magenta,
371 tertiary: Color::Cyan,
372
373 success: Color::Green,
375 warning: Color::Rgb(180, 120, 0), error: Color::Red,
377
378 text: Color::Reset,
380 text_muted: Color::DarkGray,
381 text_dimmed: Color::Cyan,
382 text_emphasis: Color::Blue,
383
384 border: Color::DarkGray,
386 border_focused: Color::Blue,
387 highlight_bg: Color::Gray,
388 background: Color::Reset,
389 dim_bg: Color::Reset,
390
391 border_type: BorderType::Plain,
392 border_focused_type: BorderType::Thick,
393 dialog_border_type: BorderType::Double,
394 }
395 }
396
397 #[must_use]
402 pub fn no_color() -> Self {
403 Self {
404 theme_type: ThemeType::NoColor,
405
406 primary: Color::Reset,
408 secondary: Color::Reset,
409 tertiary: Color::Reset,
410
411 success: Color::Reset,
412 warning: Color::Reset,
413 error: Color::Reset,
414
415 text: Color::Reset,
416 text_muted: Color::Reset,
417 text_dimmed: Color::Reset,
418 text_emphasis: Color::Reset,
419
420 border: Color::Reset,
421 border_focused: Color::Reset,
422 highlight_bg: Color::Reset,
423 background: Color::Reset,
424 dim_bg: Color::Reset,
425
426 border_type: BorderType::Rounded,
427 border_focused_type: BorderType::Thick,
428 dialog_border_type: BorderType::Double,
429 }
430 }
431
432 #[must_use]
436 pub fn title_style(&self) -> Style {
437 if self.theme_type == ThemeType::NoColor {
438 return Style::default().add_modifier(Modifier::BOLD);
439 }
440 Style::default()
441 .fg(self.primary)
442 .add_modifier(Modifier::BOLD)
443 }
444
445 #[must_use]
447 pub fn text_style(&self) -> Style {
448 if self.theme_type == ThemeType::NoColor {
449 return Style::default();
450 }
451 Style::default().fg(self.text)
452 }
453
454 #[must_use]
456 pub fn muted_style(&self) -> Style {
457 if self.theme_type == ThemeType::NoColor {
458 return Style::default().add_modifier(Modifier::DIM);
459 }
460 Style::default().fg(self.text_muted)
461 }
462
463 #[must_use]
465 pub fn emphasis_style(&self) -> Style {
466 if self.theme_type == ThemeType::NoColor {
467 return Style::default().add_modifier(Modifier::BOLD);
468 }
469 Style::default().fg(self.text_emphasis)
470 }
471
472 #[must_use]
474 pub fn success_style(&self) -> Style {
475 if self.theme_type == ThemeType::NoColor {
476 return Style::default().add_modifier(Modifier::BOLD);
477 }
478 Style::default().fg(self.success)
479 }
480
481 #[allow(dead_code)]
483 #[must_use]
484 pub fn warning_style(&self) -> Style {
485 Style::default().fg(self.warning)
486 }
487
488 #[allow(dead_code)]
490 #[must_use]
491 pub fn error_style(&self) -> Style {
492 Style::default().fg(self.error)
493 }
494
495 #[must_use]
497 pub fn highlight_style(&self) -> Style {
498 if self.theme_type == ThemeType::NoColor {
499 return Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED);
500 }
501 Style::default()
502 .fg(self.text_emphasis)
503 .bg(self.highlight_bg)
504 .add_modifier(Modifier::BOLD)
505 }
506
507 #[must_use]
509 pub fn border_style(&self, focused: bool) -> Style {
510 if focused {
511 self.border_focused_style()
512 } else {
513 self.unfocused_border_style()
514 }
515 }
516
517 #[must_use]
519 pub fn border_type(&self, focused: bool) -> BorderType {
520 if focused {
521 self.border_focused_type
522 } else {
523 self.border_type
524 }
525 }
526
527 #[must_use]
529 pub fn border_focused_style(&self) -> Style {
530 if self.theme_type == ThemeType::NoColor {
531 return Style::default().add_modifier(Modifier::BOLD);
532 }
533 Style::default().fg(self.border_focused)
534 }
535
536 #[must_use]
538 pub fn unfocused_border_style(&self) -> Style {
539 if self.theme_type == ThemeType::NoColor {
540 return Style::default();
541 }
542 Style::default().fg(self.border)
543 }
544
545 #[must_use]
547 pub fn disabled_style(&self) -> Style {
548 if self.theme_type == ThemeType::NoColor {
549 return Style::default().add_modifier(Modifier::DIM);
550 }
551 Style::default().fg(self.text_muted)
552 }
553
554 #[must_use]
556 pub fn background_style(&self) -> Style {
557 if self.theme_type == ThemeType::NoColor {
558 return Style::default();
559 }
560 Style::default().bg(self.background)
561 }
562
563 #[must_use]
565 pub fn dim_style(&self) -> Style {
566 if self.theme_type == ThemeType::NoColor {
567 return Style::default();
568 }
569 Style::default().bg(self.dim_bg).fg(self.text_muted)
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn test_theme_type_from_str() {
579 assert_eq!("dark".parse::<ThemeType>().unwrap(), ThemeType::Dark);
580 assert_eq!("light".parse::<ThemeType>().unwrap(), ThemeType::Light);
581 assert_eq!("nocolor".parse::<ThemeType>().unwrap(), ThemeType::NoColor);
582 assert_eq!("no-color".parse::<ThemeType>().unwrap(), ThemeType::NoColor);
583 assert_eq!("no_color".parse::<ThemeType>().unwrap(), ThemeType::NoColor);
584 assert_eq!(
585 "solarized-dark".parse::<ThemeType>().unwrap(),
586 ThemeType::SolarizedDark
587 );
588 assert_eq!(
589 "solarized_dark".parse::<ThemeType>().unwrap(),
590 ThemeType::SolarizedDark
591 );
592 assert_eq!(
593 "solarized".parse::<ThemeType>().unwrap(),
594 ThemeType::SolarizedDark
595 );
596 assert_eq!(
597 "solarized-light".parse::<ThemeType>().unwrap(),
598 ThemeType::SolarizedLight
599 );
600 assert_eq!(
601 "solarized_light".parse::<ThemeType>().unwrap(),
602 ThemeType::SolarizedLight
603 );
604 }
605
606 #[test]
607 fn test_no_color_theme_styles_do_not_set_colors() {
608 let t = Theme::new(ThemeType::NoColor);
609 let s = t.highlight_style();
610 assert!(s.fg.is_none());
612 assert!(s.bg.is_none());
613 }
614}