1use crate::output::colors::*;
2use crossterm::style::{Color, ResetColor, SetBackgroundColor, SetForegroundColor};
3#[cfg(feature = "theme-config")]
4use serde::Deserialize;
5use std::collections::HashMap;
6#[cfg(feature = "theme-config")]
7use std::fs;
8use std::io::{stdout, Write};
9#[cfg(feature = "theme-config")]
10use std::path::Path;
11use std::sync::OnceLock;
12
13#[derive(Clone)]
14pub struct Theme {
15 pub name: String,
16 pub fg: Color,
17 pub bg: Color,
18 pub log_styles: HashMap<&'static str, Color>,
19}
20
21impl Theme {
22 pub fn apply(&self) {
23 let _ = write!(
24 stdout(),
25 "{}{}",
26 SetForegroundColor(self.fg),
27 SetBackgroundColor(self.bg)
28 );
29 let _ = stdout().flush();
30 }
31
32 pub fn reset() {
33 let _ = write!(stdout(), "{ResetColor}");
34 let _ = stdout().flush();
35 }
36
37 pub fn get_log_color(&self, key: &str) -> Color {
38 self.log_styles.get(key).copied().unwrap_or(self.fg)
39 }
40}
41
42static THEME: OnceLock<Theme> = OnceLock::new();
43
44fn log_defaults(base: Color) -> HashMap<&'static str, Color> {
45 let mut map = HashMap::new();
46 map.insert("error", COLOR_ERROR);
47 map.insert("warn", COLOR_WARNING);
48 map.insert("success", COLOR_SUCCESS);
49 map.insert("debug", COLOR_DEBUG);
50 map.insert("info", COLOR_INFO);
51 map.insert("trace", COLOR_TRACE);
52 map.insert("notice", COLOR_NOTICE);
53 map.insert("status", COLOR_STATUS);
54 map.insert("default", base);
55 map.insert("menu_selected_bg", COLOR_STATUS);
59 map.insert("menu_selected_fg", BLACK);
60 map.insert("menu_stripe_fg", DARK_GREY);
61 map
62}
63
64pub fn apply_theme(name: &str) {
65 let theme = match name.to_lowercase().as_str() {
66 "monochrome" => Theme {
67 name: "monochrome".into(),
68 fg: GREY,
69 bg: BLACK,
70 log_styles: log_defaults(GREY),
71 },
72 "inverted" => Theme {
73 name: "inverted".into(),
74 fg: BLACK,
75 bg: WHITE,
76 log_styles: log_defaults(BLACK),
77 },
78 "blue" => Theme {
79 name: "blue".into(),
80 fg: WHITE,
81 bg: BLUE,
82 log_styles: log_defaults(WHITE),
83 },
84 "green" => Theme {
85 name: "green".into(),
86 fg: BLACK,
87 bg: GREEN,
88 log_styles: log_defaults(BLACK),
89 },
90 _ => Theme {
91 name: "default".into(),
92 fg: WHITE,
93 bg: BLACK,
94 log_styles: log_defaults(WHITE),
95 },
96 };
97
98 let _ = THEME.set(theme.clone()); theme.apply();
100}
101
102pub fn current_theme() -> Theme {
103 THEME.get().cloned().unwrap_or_else(|| Theme {
104 name: "default".into(),
105 fg: WHITE,
106 bg: BLACK,
107 log_styles: log_defaults(WHITE),
108 })
109}
110
111pub struct ThemeGuard {
114 reset: bool,
115}
116
117impl ThemeGuard {
118 pub fn apply(name: &str) -> Self {
119 apply_theme(name);
120 Self { reset: true }
121 }
122
123 pub fn disable_reset(mut self) -> Self {
124 self.reset = false;
125 self
126 }
127}
128
129impl Drop for ThemeGuard {
130 fn drop(&mut self) {
131 if self.reset {
132 Theme::reset();
133 }
134 }
135}
136
137#[cfg(feature = "theme-config")]
138#[derive(Deserialize)]
139struct ThemeFile {
140 name: Option<String>,
141 fg: Option<String>,
142 bg: Option<String>,
143 log_styles: Option<HashMap<String, String>>,
144}
145
146#[cfg(feature = "theme-config")]
148pub fn load_theme_from_json<P: AsRef<Path>>(path: P) -> Result<Theme, String> {
149 let data = fs::read_to_string(&path).map_err(|e| format!("read failed: {e}"))?;
150 let tf: ThemeFile =
151 serde_json::from_str(&data).map_err(|e| format!("json parse failed: {e}"))?;
152
153 let fg = tf.fg.as_deref().map(get).unwrap_or(WHITE);
154 let bg = tf.bg.as_deref().map(get).unwrap_or(BLACK);
155 let mut log = log_defaults(fg);
156 if let Some(map) = tf.log_styles {
157 for (k, v) in map.into_iter() {
158 log.insert(Box::leak(k.into_boxed_str()), get(&v));
159 }
160 }
161 Ok(Theme {
162 name: tf.name.unwrap_or_else(|| "loaded".into()),
163 fg,
164 bg,
165 log_styles: log,
166 })
167}