1use std::{
2 str::FromStr,
3 sync::{Arc, LazyLock, RwLock},
4};
5
6use config::{Config, ConfigBuilder, Environment, File, builder::DefaultState};
7use ratatui::style::Color;
8
9use crate::highlight::{DEFAULT_COLOR_MAP, HIGHLIGHT_NAMES};
10
11fn config_builder() -> ConfigBuilder<DefaultState> {
12 let config_dir = dirs::home_dir().unwrap();
13 let config_file = config_dir.join(".config").join("mdt").join("config.toml");
14 Config::builder().add_source(File::with_name(config_file.to_str().unwrap()).required(false))
15}
16
17#[derive(Debug, Clone, Copy)]
18pub struct ColorConfig {
19 pub italic_color: Color,
21 pub bold_color: Color,
22 pub striketrough_color: Color,
23 pub bold_italic_color: Color,
24 pub code_fg_color: Color,
25 pub code_bg_color: Color,
26 pub link_color: Color,
27 pub link_selected_fg_color: Color,
28 pub link_selected_bg_color: Color,
29
30 pub code_block_bg_color: Color,
32 pub heading_fg_color: Color,
33 pub heading_bg_color: Color,
34 pub table_header_fg_color: Color,
35 pub table_header_bg_color: Color,
36 pub quote_bg_color: Color,
37
38 pub file_tree_selected_fg_color: Color,
40 pub file_tree_page_count_color: Color,
41 pub file_tree_name_color: Color,
42 pub file_tree_path_color: Color,
43
44 pub quote_important: Color,
46 pub quote_warning: Color,
47 pub quote_tip: Color,
48 pub quote_note: Color,
49 pub quote_caution: Color,
50 pub quote_default: Color,
51
52 pub help_bg_color: Color,
53 pub help_fg_color: Color,
54 pub help_title_color: Color,
55}
56
57#[must_use]
58pub fn read_color_config_from_file() -> ColorConfig {
59 let settings = config_builder()
60 .add_source(Environment::with_prefix("MDT").separator("_"))
61 .build()
62 .unwrap_or_default();
63
64 ColorConfig {
65 heading_bg_color: Color::from_str(
66 &settings.get::<String>("h_bg_color").unwrap_or_default(),
67 )
68 .unwrap_or(Color::Blue),
69 heading_fg_color: Color::from_str(
70 &settings.get::<String>("h_fg_color").unwrap_or_default(),
71 )
72 .unwrap_or(Color::Black),
73 italic_color: Color::from_str(&settings.get::<String>("italic_color").unwrap_or_default())
74 .unwrap_or(Color::Reset),
75 bold_color: Color::from_str(&settings.get::<String>("bold_color").unwrap_or_default())
76 .unwrap_or(Color::Reset),
77 striketrough_color: Color::from_str(
78 &settings
79 .get_string("striketrough_color")
80 .unwrap_or_default(),
81 )
82 .unwrap_or(Color::Reset),
83 quote_bg_color: Color::from_str(
84 &settings.get::<String>("quote_bg_color").unwrap_or_default(),
85 )
86 .unwrap_or(Color::Reset),
87 code_fg_color: Color::from_str(
88 &settings.get::<String>("code_fg_color").unwrap_or_default(),
89 )
90 .unwrap_or(Color::Red),
91 code_bg_color: Color::from_str(
92 &settings.get::<String>("code_bg_color").unwrap_or_default(),
93 )
94 .unwrap_or(Color::Rgb(48, 48, 48)),
95 code_block_bg_color: Color::from_str(
96 &settings
97 .get::<String>("code_block_bg_color")
98 .unwrap_or_default(),
99 )
100 .unwrap_or(Color::Rgb(48, 48, 48)),
101 link_color: Color::from_str(&settings.get::<String>("link_color").unwrap_or_default())
102 .unwrap_or(Color::Blue),
103 link_selected_fg_color: Color::from_str(
104 &settings
105 .get::<String>("link_selected_fg_color")
106 .unwrap_or_default(),
107 )
108 .unwrap_or(Color::Green),
109 link_selected_bg_color: Color::from_str(
110 &settings
111 .get::<String>("link_selected_bg_color")
112 .unwrap_or_default(),
113 )
114 .unwrap_or(Color::DarkGray),
115 table_header_fg_color: Color::from_str(
116 &settings
117 .get::<String>("table_header_fg_color")
118 .unwrap_or_default(),
119 )
120 .unwrap_or(Color::Yellow),
121 table_header_bg_color: Color::from_str(
122 &settings
123 .get::<String>("table_header_bg_color")
124 .unwrap_or_default(),
125 )
126 .unwrap_or(Color::Reset),
127 file_tree_selected_fg_color: Color::from_str(
128 &settings
129 .get::<String>("file_tree_selected_fg_color")
130 .unwrap_or_default(),
131 )
132 .unwrap_or(Color::LightGreen),
133 file_tree_page_count_color: Color::from_str(
134 &settings
135 .get::<String>("file_tree_page_count_color")
136 .unwrap_or_default(),
137 )
138 .unwrap_or(Color::LightGreen),
139 file_tree_name_color: Color::from_str(
140 &settings
141 .get::<String>("file_tree_name_color")
142 .unwrap_or_default(),
143 )
144 .unwrap_or(Color::Blue),
145 file_tree_path_color: Color::from_str(
146 &settings
147 .get::<String>("file_tree_path_color")
148 .unwrap_or_default(),
149 )
150 .unwrap_or(Color::DarkGray),
151 bold_italic_color: Color::from_str(
152 &settings
153 .get::<String>("bold_italic_color")
154 .unwrap_or_default(),
155 )
156 .unwrap_or(Color::Reset),
157 quote_important: Color::from_str(
158 &settings
159 .get::<String>("quote_important")
160 .unwrap_or_default(),
161 )
162 .unwrap_or(Color::LightRed),
163 quote_warning: Color::from_str(
164 &settings.get::<String>("quote_warning").unwrap_or_default(),
165 )
166 .unwrap_or(Color::LightYellow),
167
168 quote_tip: Color::from_str(&settings.get::<String>("quote_tip").unwrap_or_default())
169 .unwrap_or(Color::LightGreen),
170
171 quote_note: Color::from_str(&settings.get::<String>("quote_note").unwrap_or_default())
172 .unwrap_or(Color::LightBlue),
173
174 quote_caution: Color::from_str(
175 &settings.get::<String>("quote_caution").unwrap_or_default(),
176 )
177 .unwrap_or(Color::LightMagenta),
178
179 quote_default: Color::from_str(
180 &settings.get::<String>("quote_default").unwrap_or_default(),
181 )
182 .unwrap_or(Color::White),
183
184 help_bg_color: Color::from_str(
185 &settings.get::<String>("help_bg_color").unwrap_or_default(),
186 )
187 .unwrap_or(Color::Black),
188
189 help_fg_color: Color::from_str(
190 &settings.get::<String>("help_fg_color").unwrap_or_default(),
191 )
192 .unwrap_or(Color::LightGreen),
193
194 help_title_color: Color::from_str(
195 &settings
196 .get::<String>("help_title_color")
197 .unwrap_or_default(),
198 )
199 .unwrap_or(Color::LightGreen),
200 }
201}
202
203static COLOR_CONFIG_INTERNAL: LazyLock<Arc<RwLock<ColorConfig>>> =
204 LazyLock::new(|| Arc::new(RwLock::new(read_color_config_from_file())));
205
206pub fn set_color_config(config: ColorConfig) {
207 let mut color_config_internal = COLOR_CONFIG_INTERNAL.write().unwrap();
208 *color_config_internal = config;
209}
210
211#[must_use]
212pub fn color_config() -> ColorConfig {
213 *COLOR_CONFIG_INTERNAL.read().unwrap()
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct HighlightColors([Color; HIGHLIGHT_NAMES.len()]);
218
219impl std::ops::Index<usize> for HighlightColors {
220 type Output = Color;
221
222 fn index(&self, index: usize) -> &Self::Output {
223 &self.0[index]
224 }
225}
226
227#[must_use]
229pub fn highlight_colors_from_settings(settings: &Config) -> HighlightColors {
230 let mut colors = DEFAULT_COLOR_MAP;
231
232 for (color, name) in colors.iter_mut().zip(HIGHLIGHT_NAMES) {
233 let key = format!("code_hl_{}", name.replace('.', "_"));
234 if let Ok(value) = settings.get::<String>(&key)
235 && let Ok(parsed) = Color::from_str(&value)
236 {
237 *color = parsed;
238 }
239 }
240
241 HighlightColors(colors)
242}
243
244#[must_use]
245pub fn read_highlight_colors_from_file() -> HighlightColors {
246 let settings = config_builder()
247 .add_source(Environment::with_prefix("MDT").separator("_"))
248 .build()
249 .unwrap_or_default();
250
251 highlight_colors_from_settings(&settings)
252}
253
254static HIGHLIGHT_COLORS_INTERNAL: LazyLock<Arc<RwLock<HighlightColors>>> =
255 LazyLock::new(|| Arc::new(RwLock::new(read_highlight_colors_from_file())));
256
257pub fn set_highlight_colors(colors: HighlightColors) {
258 let mut highlight_colors_internal = HIGHLIGHT_COLORS_INTERNAL.write().unwrap();
259 *highlight_colors_internal = colors;
260}
261
262#[must_use]
263pub fn highlight_colors() -> HighlightColors {
264 *HIGHLIGHT_COLORS_INTERNAL.read().unwrap()
265}
266
267#[derive(Clone, Copy)]
268pub struct HeadingColors {
269 pub level_2: Color,
270 pub level_3: Color,
271 pub level_4: Color,
272 pub level_5: Color,
273 pub level_6: Color,
274}
275
276#[must_use]
277pub fn read_heading_colors_from_file() -> HeadingColors {
278 let settings = config_builder().build().unwrap_or_default();
279
280 HeadingColors {
281 level_2: settings
282 .get::<String>("h2_fg_color")
283 .map(|s| Color::from_str(&s).unwrap_or(Color::Green))
284 .unwrap_or(Color::Green),
285 level_3: settings
286 .get_string("h3_fg_color")
287 .map(|s| Color::from_str(&s).unwrap_or(Color::Magenta))
288 .unwrap_or(Color::Magenta),
289 level_4: settings
290 .get_string("h4_fg_color")
291 .map(|s| Color::from_str(&s).unwrap_or(Color::Cyan))
292 .unwrap_or(Color::Cyan),
293 level_5: settings
294 .get_string("h5_fg_color")
295 .map(|s| Color::from_str(&s).unwrap_or(Color::Yellow))
296 .unwrap_or(Color::Yellow),
297 level_6: settings
298 .get_string("h6_fg_color")
299 .map(|s| Color::from_str(&s).unwrap_or(Color::LightRed))
300 .unwrap_or(Color::LightRed),
301 }
302}
303
304static HEADING_COLORS_INTERNAL: LazyLock<Arc<RwLock<HeadingColors>>> =
305 LazyLock::new(|| Arc::new(RwLock::new(read_heading_colors_from_file())));
306
307pub fn set_heading_colors(config: HeadingColors) {
308 let mut heading_colors_internal = HEADING_COLORS_INTERNAL.write().unwrap();
309 *heading_colors_internal = config;
310}
311
312#[must_use]
313pub fn heading_colors() -> HeadingColors {
314 *HEADING_COLORS_INTERNAL.read().unwrap()
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use config::FileFormat;
321
322 fn settings_from(toml: &str) -> Config {
323 Config::builder()
324 .add_source(File::from_str(toml, FileFormat::Toml))
325 .build()
326 .unwrap()
327 }
328
329 fn index_of(name: &str) -> usize {
330 HIGHLIGHT_NAMES
331 .iter()
332 .position(|n| *n == name)
333 .expect("unknown highlight name")
334 }
335
336 #[test]
337 fn defaults_match_the_built_in_palette() {
338 let colors = highlight_colors_from_settings(&settings_from(""));
339
340 for (i, default) in DEFAULT_COLOR_MAP.iter().enumerate() {
341 assert_eq!(colors[i], *default);
342 }
343 }
344
345 #[test]
346 fn a_key_overrides_a_single_capture() {
347 let colors =
348 highlight_colors_from_settings(&settings_from(r##"code_hl_keyword = "#123123""##));
349
350 assert_eq!(colors[index_of("keyword")], Color::Rgb(0x12, 0x31, 0x23));
351 assert_eq!(
352 colors[index_of("string")],
353 DEFAULT_COLOR_MAP[index_of("string")]
354 );
355 }
356
357 #[test]
358 fn dotted_captures_are_keyed_with_underscores() {
359 let colors = highlight_colors_from_settings(&settings_from(
360 r##"
361 code_hl_function_builtin = "green"
362 code_hl_punctuation_bracket = "reset"
363 code_hl_variable_parameter = "#ABCDEF"
364 "##,
365 ));
366
367 assert_eq!(colors[index_of("function.builtin")], Color::Green);
368 assert_eq!(colors[index_of("punctuation.bracket")], Color::Reset);
369 assert_eq!(
370 colors[index_of("variable.parameter")],
371 Color::Rgb(0xAB, 0xCD, 0xEF)
372 );
373 }
374
375 #[test]
376 fn unparseable_values_fall_back_to_the_default() {
377 let colors = highlight_colors_from_settings(&settings_from(
378 r#"
379 code_hl_keyword = "not-a-color"
380 code_hl_string = ""
381 "#,
382 ));
383
384 assert_eq!(
385 colors[index_of("keyword")],
386 DEFAULT_COLOR_MAP[index_of("keyword")]
387 );
388 assert_eq!(
389 colors[index_of("string")],
390 DEFAULT_COLOR_MAP[index_of("string")]
391 );
392 }
393}