1use std::{collections::BTreeMap, path::PathBuf};
2
3use crate::style::{Color, Modifier, Style};
4use crate::{Bindings, Res, error::Error, key_parser, menu::Menu, ops::Op};
5use crossterm::event::{KeyCode, KeyModifiers};
6use etcetera::{BaseStrategy, choose_base_strategy};
7use figment::{
8 Figment,
9 providers::{Format, Toml},
10};
11use serde::Deserialize;
12
13const DEFAULT_CONFIG: &str = include_str!("default_config.toml");
14
15pub struct Config {
16 pub path: PathBuf,
19 pub general: GeneralConfig,
20 pub style: StyleConfig,
21 pub bindings: Bindings,
22 pub picker_bindings: PickerBindings,
23}
24
25#[derive(Default, Deserialize)]
26pub(crate) struct PickerBindingsConfig {
27 #[serde(default)]
28 pub next: Vec<String>,
29 #[serde(default)]
30 pub previous: Vec<String>,
31 #[serde(default)]
32 pub done: Vec<String>,
33 #[serde(default)]
34 pub cancel: Vec<String>,
35}
36
37#[derive(Default, Deserialize)]
38pub(crate) struct BindingsConfig {
39 #[serde(flatten)]
40 pub menus: BTreeMap<Menu, BTreeMap<Op, Vec<String>>>,
41 #[serde(default)]
42 pub picker: PickerBindingsConfig,
43}
44
45#[derive(Default, Deserialize)]
46pub(crate) struct FigmentConfig {
49 pub general: GeneralConfig,
50 pub style: StyleConfig,
51 pub bindings: BindingsConfig,
52}
53
54#[derive(Default, Debug, Deserialize)]
55pub struct GeneralConfig {
56 pub always_show_help: BoolConfigEntry,
57 pub confirm_quit: BoolConfigEntry,
58 pub refresh_on_file_change: BoolConfigEntry,
59 pub confirm_discard: ConfirmDiscardOption,
60 pub collapsed_sections: Vec<String>,
61 pub stash_list_limit: usize,
62 pub recent_commits_limit: usize,
63 pub log_author_width: usize,
64 pub mouse_support: bool,
65 pub mouse_scroll_lines: usize,
66 pub max_input_bytes: u64,
71 pub verbose_branch_status: bool,
74 #[serde(default)]
78 pub hide: Vec<String>,
79 #[serde(default)]
82 pub visit_context_lines: bool,
83 #[serde(default)]
84 pub diff_renderer: DiffRendererConfig,
85 #[serde(default)]
86 pub log_renderer: LogRendererConfig,
87 #[serde(default)]
88 pub show_renderer: ShowRendererConfig,
89}
90
91#[derive(Default, Debug, Deserialize)]
92pub struct DiffRendererConfig {
93 #[serde(default)]
96 pub command: Vec<String>,
97 #[serde(default)]
100 pub features: Vec<String>,
101 #[serde(default)]
104 pub feature_keys: BTreeMap<String, char>,
105}
106
107#[derive(Default, Debug, Deserialize)]
108pub struct LogRendererConfig {
109 #[serde(default)]
115 pub command: Vec<String>,
116}
117
118#[derive(Default, Debug, Deserialize)]
119pub struct ShowRendererConfig {
120 #[serde(default)]
123 pub command: Vec<String>,
124}
125
126#[derive(Default, Debug, Deserialize)]
127pub struct BoolConfigEntry {
128 #[serde(default)]
129 pub enabled: bool,
130}
131
132#[derive(Default, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
133#[serde(rename_all = "lowercase")]
134pub enum ConfirmDiscardOption {
135 #[default]
136 Line,
137 Hunk,
138 File,
139 Never,
140}
141
142#[derive(Default, Debug, Deserialize)]
143pub struct StyleConfig {
144 pub separator: StyleConfigEntry,
145
146 pub info_msg: StyleConfigEntry,
147 pub error_msg: StyleConfigEntry,
148 pub command: StyleConfigEntry,
149
150 #[serde(default)]
151 pub menu: MenuStyleConfig,
152
153 pub prompt: StyleConfigEntry,
154
155 pub section_header: StyleConfigEntry,
156 pub file_header: StyleConfigEntry,
157 pub hunk_header: StyleConfigEntry,
158
159 #[serde(default)]
160 pub diff_highlight: DiffHighlightConfig,
161
162 #[serde(default)]
163 pub syntax_highlight: SyntaxHighlightConfig,
164
165 #[serde(default)]
166 pub picker: PickerStyleConfig,
167
168 pub cursor: SymbolStyleConfigEntry,
169 pub selection_bar: SymbolStyleConfigEntry,
170 pub selection_line: StyleConfigEntry,
171 pub selection_area: StyleConfigEntry,
172
173 #[serde(default)]
174 pub search_match: StyleConfigEntry,
175
176 pub hash: StyleConfigEntry,
177 #[serde(default)]
179 pub rebase_todo: RebaseTodoStyleConfig,
180 pub branch: StyleConfigEntry,
181 pub remote: StyleConfigEntry,
182 pub tag: StyleConfigEntry,
183 pub author: StyleConfigEntry,
184 pub age: StyleConfigEntry,
185
186 #[serde(default)]
187 pub blame: BlameStyleConfig,
188}
189
190#[derive(Default, Debug, Deserialize)]
191pub struct RebaseTodoStyleConfig {
192 #[serde(default)]
193 pub reword: StyleConfigEntry,
194 #[serde(default)]
195 pub edit: StyleConfigEntry,
196 #[serde(default)]
197 pub squash: StyleConfigEntry,
198 #[serde(default)]
199 pub fixup: StyleConfigEntry,
200 #[serde(default)]
201 pub drop: StyleConfigEntry,
202}
203
204#[derive(Default, Debug, Deserialize)]
205pub struct BlameStyleConfig {
206 #[serde(default)]
207 pub line_num: StyleConfigEntry,
208 #[serde(default)]
209 pub code_line: StyleConfigEntry,
210}
211
212#[derive(Default, Debug, Deserialize)]
213pub struct MenuStyleConfig {
214 #[serde(default)]
215 pub heading: StyleConfigEntry,
216 #[serde(default)]
217 pub key: StyleConfigEntry,
218 #[serde(default)]
220 pub active_arg: StyleConfigEntry,
221 #[serde(default)]
223 pub inactive_arg: StyleConfigEntry,
224}
225
226#[derive(Default, Debug, Deserialize)]
227pub struct DiffHighlightConfig {
228 #[serde(default)]
229 pub tag_old: StyleConfigEntry,
230 #[serde(default)]
231 pub tag_new: StyleConfigEntry,
232 #[serde(default)]
233 pub unchanged_old: StyleConfigEntry,
234 #[serde(default)]
235 pub unchanged_new: StyleConfigEntry,
236 #[serde(default)]
237 pub changed_old: StyleConfigEntry,
238 #[serde(default)]
239 pub changed_new: StyleConfigEntry,
240}
241
242#[derive(Default, Debug, Deserialize)]
243pub struct SyntaxHighlightConfig {
244 #[serde(default)]
245 pub enabled: bool,
246 #[serde(default)]
247 pub attribute: StyleConfigEntry,
248 #[serde(default)]
249 pub comment: StyleConfigEntry,
250 #[serde(default)]
251 pub constant_builtin: StyleConfigEntry,
252 #[serde(default)]
253 pub constant: StyleConfigEntry,
254 #[serde(default)]
255 pub constructor: StyleConfigEntry,
256 #[serde(default)]
257 pub embedded: StyleConfigEntry,
258 #[serde(default)]
259 pub function_builtin: StyleConfigEntry,
260 #[serde(default)]
261 pub function: StyleConfigEntry,
262 #[serde(default)]
263 pub keyword: StyleConfigEntry,
264 #[serde(default)]
265 pub number: StyleConfigEntry,
266 #[serde(default)]
267 pub module: StyleConfigEntry,
268 #[serde(default)]
269 pub property: StyleConfigEntry,
270 #[serde(default)]
271 pub operator: StyleConfigEntry,
272 #[serde(default)]
273 pub punctuation_bracket: StyleConfigEntry,
274 #[serde(default)]
275 pub punctuation_delimiter: StyleConfigEntry,
276 #[serde(default)]
277 pub string_special: StyleConfigEntry,
278 #[serde(default)]
279 pub string: StyleConfigEntry,
280 #[serde(default)]
281 pub tag: StyleConfigEntry,
282 #[serde(default)]
283 #[serde(rename = "type")]
284 pub type_regular: StyleConfigEntry,
285 #[serde(default)]
286 pub type_builtin: StyleConfigEntry,
287 #[serde(default)]
288 pub variable_builtin: StyleConfigEntry,
289 #[serde(default)]
290 pub variable_parameter: StyleConfigEntry,
291}
292
293#[derive(Default, Debug, Deserialize)]
294pub struct PickerStyleConfig {
295 #[serde(default)]
296 pub prompt: StyleConfigEntry,
297 #[serde(default)]
298 pub info: StyleConfigEntry,
299 #[serde(default)]
300 pub selection_line: StyleConfigEntry,
301 #[serde(default)]
302 pub matched: StyleConfigEntry,
303}
304
305#[derive(Default, Debug, Deserialize)]
306pub struct StyleConfigEntry {
307 #[serde(default)]
308 fg: Option<Color>,
309 #[serde(default)]
310 bg: Option<Color>,
311 #[serde(default)]
312 mods: Option<Modifier>,
313}
314
315#[derive(Default, Debug, Deserialize)]
316pub struct SymbolStyleConfigEntry {
317 #[serde(default)]
318 pub symbol: char,
319 #[serde(default)]
320 fg: Option<Color>,
321 #[serde(default)]
322 bg: Option<Color>,
323 #[serde(default)]
324 mods: Option<Modifier>,
325}
326
327impl From<&StyleConfigEntry> for Style {
328 fn from(val: &StyleConfigEntry) -> Self {
329 Style {
330 fg: val.fg,
331 bg: val.bg,
332 add_modifier: val.mods.unwrap_or(Modifier::empty()),
333 sub_modifier: Modifier::empty(),
334 }
335 }
336}
337
338impl From<&SymbolStyleConfigEntry> for Style {
339 fn from(val: &SymbolStyleConfigEntry) -> Self {
340 Style {
341 fg: val.fg,
342 bg: val.bg,
343 add_modifier: val.mods.unwrap_or(Modifier::empty()),
344 sub_modifier: Modifier::empty(),
345 }
346 }
347}
348
349pub struct PickerBindings {
350 pub next: Vec<Vec<(KeyModifiers, KeyCode)>>,
351 pub previous: Vec<Vec<(KeyModifiers, KeyCode)>>,
352 pub done: Vec<Vec<(KeyModifiers, KeyCode)>>,
353 pub cancel: Vec<Vec<(KeyModifiers, KeyCode)>>,
354}
355
356impl TryFrom<PickerBindingsConfig> for PickerBindings {
357 type Error = crate::error::Error;
358
359 fn try_from(config: PickerBindingsConfig) -> Result<Self, Self::Error> {
360 let mut bad_bindings = Vec::new();
361
362 let next = parse_picker_keys(&config.next, "picker.next", &mut bad_bindings);
363 let previous = parse_picker_keys(&config.previous, "picker.previous", &mut bad_bindings);
364 let done = parse_picker_keys(&config.done, "picker.done", &mut bad_bindings);
365 let cancel = parse_picker_keys(&config.cancel, "picker.cancel", &mut bad_bindings);
366
367 if !bad_bindings.is_empty() {
368 return Err(Error::Bindings {
369 bad_key_bindings: bad_bindings,
370 });
371 }
372
373 Ok(Self {
374 next,
375 previous,
376 done,
377 cancel,
378 })
379 }
380}
381
382fn parse_picker_keys(
383 raw_keys: &[String],
384 action_name: &str,
385 bad_bindings: &mut Vec<String>,
386) -> Vec<Vec<(KeyModifiers, KeyCode)>> {
387 raw_keys
388 .iter()
389 .filter_map(|keys| {
390 if let Ok(("", parsed)) = key_parser::parse_config_keys(keys) {
391 Some(parsed)
392 } else {
393 bad_bindings.push(format!("- {} = {}", action_name, keys));
394 None
395 }
396 })
397 .collect()
398}
399
400pub fn init_config(path: Option<PathBuf>) -> Res<Config> {
401 let config_path = path.unwrap_or_else(config_path);
402
403 if config_path.exists() {
404 log::info!("Loading config file at {config_path:?}");
405 } else {
406 log::info!("No config file at {config_path:?}");
407 }
408
409 let FigmentConfig {
410 general,
411 style,
412 bindings: bindings_config,
413 } = Figment::new()
414 .merge(Toml::string(DEFAULT_CONFIG))
415 .merge(Toml::file(&config_path))
416 .extract()
417 .map_err(Box::new)
418 .map_err(Error::Config)?;
419 let bindings = Bindings::try_from(bindings_config.menus)?;
420 let picker_bindings = PickerBindings::try_from(bindings_config.picker)?;
421 check_feature_keys(&general.diff_renderer.feature_keys, &bindings)?;
422
423 Ok(Config {
424 path: config_path,
425 general,
426 style,
427 bindings,
428 picker_bindings,
429 })
430}
431
432fn check_feature_keys(feature_keys: &BTreeMap<String, char>, bindings: &Bindings) -> Res<()> {
435 let taken: Vec<String> = feature_keys
436 .iter()
437 .filter(|(_, key)| bindings.binds_at_top_level(**key))
438 .map(|(feature, key)| {
439 format!("- general.diff_renderer.feature_keys.{feature} = {key} (bound already)")
440 })
441 .collect();
442 if !taken.is_empty() {
443 return Err(Error::Bindings {
444 bad_key_bindings: taken,
445 });
446 }
447 Ok(())
448}
449
450pub fn config_path() -> PathBuf {
451 choose_base_strategy()
452 .expect("Unable to find the config directory!")
453 .config_dir()
454 .join("gitu/config.toml")
455}
456
457#[cfg(test)]
458pub(crate) const TEST_SEARCH_HIGHLIGHT_BG: Color = Color::LightYellow;
459
460#[cfg(test)]
461pub(crate) fn init_test_config() -> Res<Config> {
462 let FigmentConfig {
463 mut general,
464 mut style,
465 bindings: bindings_config,
466 } = Figment::new()
467 .merge(Toml::string(DEFAULT_CONFIG))
468 .extract()
469 .map_err(Box::new)
470 .map_err(Error::Config)?;
471
472 general.always_show_help.enabled = false;
473 general.refresh_on_file_change.enabled = false;
474
475 style.search_match = StyleConfigEntry {
476 fg: None,
477 bg: Some(TEST_SEARCH_HIGHLIGHT_BG),
478 mods: None,
479 };
480
481 Ok(Config {
482 path: PathBuf::from("/nonexistent/gitu/config.toml"),
485 general,
486 style,
487 bindings: Bindings::try_from(bindings_config.menus).unwrap(),
488 picker_bindings: PickerBindings::try_from(bindings_config.picker).unwrap(),
489 })
490}
491
492#[cfg(test)]
493mod tests {
494 use crate::style::Color;
495 use figment::{
496 Figment,
497 providers::{Format, Toml},
498 };
499
500 use super::{DEFAULT_CONFIG, FigmentConfig};
501
502 #[test]
503 fn config_merges() {
504 let config: FigmentConfig = Figment::new()
505 .merge(Toml::string(DEFAULT_CONFIG))
506 .merge(Toml::string(
507 r#"
508 [style]
509 hunk_header.bg = "light green"
510 "#,
511 ))
512 .extract()
513 .unwrap();
514
515 assert_eq!(config.style.hunk_header.bg, Some(Color::LightGreen));
516 assert_eq!(config.style.hunk_header.fg, Some(Color::Blue));
517 }
518}