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