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