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(skip)]
121 pub command: Vec<String>,
122 #[serde(default)]
125 pub features: Vec<String>,
126 #[serde(default)]
129 pub feature_keys: BTreeMap<String, char>,
130}
131
132#[derive(Default, Debug, Deserialize)]
133pub struct BoolConfigEntry {
134 #[serde(default)]
135 pub enabled: bool,
136}
137
138#[derive(Default, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
139#[serde(rename_all = "lowercase")]
140pub enum ConfirmDiscardOption {
141 #[default]
142 Line,
143 Hunk,
144 File,
145 Never,
146}
147
148#[derive(Default, Debug, Deserialize)]
149pub struct StyleConfig {
150 pub separator: StyleConfigEntry,
151
152 pub info_msg: StyleConfigEntry,
153 pub error_msg: StyleConfigEntry,
154 pub command: StyleConfigEntry,
155
156 #[serde(default)]
157 pub menu: MenuStyleConfig,
158
159 pub prompt: StyleConfigEntry,
160
161 pub section_header: StyleConfigEntry,
162 pub file_header: StyleConfigEntry,
163 pub hunk_header: StyleConfigEntry,
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 PickerStyleConfig {
228 #[serde(default)]
229 pub prompt: StyleConfigEntry,
230 #[serde(default)]
231 pub info: StyleConfigEntry,
232 #[serde(default)]
233 pub selection_line: StyleConfigEntry,
234 #[serde(default)]
235 pub matched: StyleConfigEntry,
236}
237
238#[derive(Default, Debug, Deserialize)]
239pub struct StyleConfigEntry {
240 #[serde(default)]
241 fg: Option<Color>,
242 #[serde(default)]
243 bg: Option<Color>,
244 #[serde(default)]
245 mods: Option<Modifier>,
246}
247
248#[derive(Default, Debug, Deserialize)]
249pub struct SymbolStyleConfigEntry {
250 #[serde(default)]
251 pub symbol: char,
252 #[serde(default)]
253 fg: Option<Color>,
254 #[serde(default)]
255 bg: Option<Color>,
256 #[serde(default)]
257 mods: Option<Modifier>,
258}
259
260impl From<&StyleConfigEntry> for Style {
261 fn from(val: &StyleConfigEntry) -> Self {
262 Style {
263 fg: val.fg,
264 bg: val.bg,
265 add_modifier: val.mods.unwrap_or(Modifier::empty()),
266 sub_modifier: Modifier::empty(),
267 }
268 }
269}
270
271impl From<&SymbolStyleConfigEntry> for Style {
272 fn from(val: &SymbolStyleConfigEntry) -> Self {
273 Style {
274 fg: val.fg,
275 bg: val.bg,
276 add_modifier: val.mods.unwrap_or(Modifier::empty()),
277 sub_modifier: Modifier::empty(),
278 }
279 }
280}
281
282pub struct PickerBindings {
283 pub next: Vec<Vec<(KeyModifiers, KeyCode)>>,
284 pub previous: Vec<Vec<(KeyModifiers, KeyCode)>>,
285 pub done: Vec<Vec<(KeyModifiers, KeyCode)>>,
286 pub cancel: Vec<Vec<(KeyModifiers, KeyCode)>>,
287}
288
289impl TryFrom<PickerBindingsConfig> for PickerBindings {
290 type Error = crate::error::Error;
291
292 fn try_from(config: PickerBindingsConfig) -> Result<Self, Self::Error> {
293 let mut bad_bindings = Vec::new();
294
295 let next = parse_picker_keys(&config.next, "picker.next", &mut bad_bindings);
296 let previous = parse_picker_keys(&config.previous, "picker.previous", &mut bad_bindings);
297 let done = parse_picker_keys(&config.done, "picker.done", &mut bad_bindings);
298 let cancel = parse_picker_keys(&config.cancel, "picker.cancel", &mut bad_bindings);
299
300 if !bad_bindings.is_empty() {
301 return Err(Error::Bindings {
302 bad_key_bindings: bad_bindings,
303 });
304 }
305
306 Ok(Self {
307 next,
308 previous,
309 done,
310 cancel,
311 })
312 }
313}
314
315fn parse_picker_keys(
316 raw_keys: &[String],
317 action_name: &str,
318 bad_bindings: &mut Vec<String>,
319) -> Vec<Vec<(KeyModifiers, KeyCode)>> {
320 raw_keys
321 .iter()
322 .filter_map(|keys| {
323 if let Ok(("", parsed)) = key_parser::parse_config_keys(keys) {
324 Some(parsed)
325 } else {
326 bad_bindings.push(format!("- {} = {}", action_name, keys));
327 None
328 }
329 })
330 .collect()
331}
332
333pub fn init_config(path: Option<PathBuf>) -> Res<Config> {
334 let config_path = path.unwrap_or_else(config_path);
335
336 if config_path.exists() {
337 log::info!("Loading config file at {config_path:?}");
338 } else {
339 log::info!("No config file at {config_path:?}");
340 }
341
342 let FigmentConfig {
343 general,
344 style,
345 bindings: bindings_config,
346 } = Figment::new()
347 .merge(Toml::string(DEFAULT_CONFIG))
348 .merge(Toml::file(&config_path))
349 .extract()
350 .map_err(Box::new)
351 .map_err(Error::Config)?;
352 let bindings = Bindings::try_from(bindings_config.menus)?;
353 let picker_bindings = PickerBindings::try_from(bindings_config.picker)?;
354 check_feature_keys(&general.diff_renderer.feature_keys, &bindings)?;
355
356 Ok(Config {
357 path: config_path,
358 general,
359 style,
360 bindings,
361 picker_bindings,
362 })
363}
364
365fn check_feature_keys(feature_keys: &BTreeMap<String, char>, bindings: &Bindings) -> Res<()> {
368 let taken: Vec<String> = feature_keys
369 .iter()
370 .filter(|(_, key)| bindings.binds_at_top_level(**key))
371 .map(|(feature, key)| {
372 format!("- general.diff_renderer.feature_keys.{feature} = {key} (bound already)")
373 })
374 .collect();
375 if !taken.is_empty() {
376 return Err(Error::Bindings {
377 bad_key_bindings: taken,
378 });
379 }
380 Ok(())
381}
382
383pub fn config_path() -> PathBuf {
384 choose_base_strategy()
385 .expect("Unable to find the config directory!")
386 .config_dir()
387 .join("gitu/config.toml")
388}
389
390#[cfg(test)]
391pub(crate) const TEST_SEARCH_HIGHLIGHT_BG: Color = Color::LightYellow;
392
393#[cfg(test)]
394pub(crate) fn init_test_config() -> Res<Config> {
395 let FigmentConfig {
396 mut general,
397 mut style,
398 bindings: bindings_config,
399 } = Figment::new()
400 .merge(Toml::string(DEFAULT_CONFIG))
401 .extract()
402 .map_err(Box::new)
403 .map_err(Error::Config)?;
404
405 general.always_show_help.enabled = false;
406 general.refresh_on_file_change.enabled = false;
407 general.status_cursor_on_changes = false;
408 general.diff_renderer.command = crate::tests::helpers::ui::renderer();
409
410 style.search_match = StyleConfigEntry {
411 fg: None,
412 bg: Some(TEST_SEARCH_HIGHLIGHT_BG),
413 mods: None,
414 };
415
416 Ok(Config {
417 path: PathBuf::from("/nonexistent/gitu/config.toml"),
420 general,
421 style,
422 bindings: Bindings::try_from(bindings_config.menus).unwrap(),
423 picker_bindings: PickerBindings::try_from(bindings_config.picker).unwrap(),
424 })
425}
426
427#[cfg(test)]
428mod tests {
429 use crate::style::Color;
430 use figment::{
431 Figment,
432 providers::{Format, Toml},
433 };
434
435 use super::{DEFAULT_CONFIG, FigmentConfig};
436
437 #[test]
438 fn config_merges() {
439 let config: FigmentConfig = Figment::new()
440 .merge(Toml::string(DEFAULT_CONFIG))
441 .merge(Toml::string(
442 r#"
443 [style]
444 hunk_header.bg = "light green"
445 "#,
446 ))
447 .extract()
448 .unwrap();
449
450 assert_eq!(config.style.hunk_header.bg, Some(Color::LightGreen));
451 assert_eq!(config.style.hunk_header.fg, Some(Color::Blue));
452 }
453}