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