1use std::{
2 collections::BTreeMap,
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use crate::config_git::{self, FEATURE_KEYS};
8use crate::style::{Color, Modifier, Style};
9use crate::{Bindings, Res, error::Error, key_parser, menu::Menu, ops::Op};
10use crossterm::event::{KeyCode, KeyModifiers};
11use etcetera::{BaseStrategy, choose_base_strategy};
12use figment::{
13 Figment,
14 providers::{Format, Serialized, Toml},
15};
16use serde::Deserialize;
17
18const DEFAULT_CONFIG: &str = include_str!("default_config.toml");
19
20pub struct Config {
21 pub general: GeneralConfig,
22 pub style: StyleConfig,
23 pub bindings: Bindings,
24 pub picker_bindings: PickerBindings,
25}
26
27#[derive(Default, Deserialize)]
28pub(crate) struct PickerBindingsConfig {
29 #[serde(default)]
30 pub next: Vec<String>,
31 #[serde(default)]
32 pub previous: Vec<String>,
33 #[serde(default)]
34 pub done: Vec<String>,
35 #[serde(default)]
36 pub cancel: Vec<String>,
37 #[serde(default)]
38 pub toggle_list: Vec<String>,
39 #[serde(default)]
40 pub quit: Vec<String>,
41}
42
43#[derive(Default, Deserialize)]
44pub(crate) struct BindingsConfig {
45 #[serde(flatten)]
46 pub menus: BTreeMap<Menu, BTreeMap<Op, Vec<String>>>,
47 #[serde(default)]
48 pub picker: PickerBindingsConfig,
49}
50
51#[derive(Default, Deserialize)]
52pub(crate) struct FigmentConfig {
55 pub general: GeneralConfig,
56 pub style: StyleConfig,
57 pub bindings: BindingsConfig,
58}
59
60#[derive(Default, Debug, Deserialize)]
61pub struct GeneralConfig {
62 pub always_show_help: BoolConfigEntry,
63 pub confirm_quit: BoolConfigEntry,
64 pub refresh_on_file_change: BoolConfigEntry,
65 pub confirm_discard: ConfirmDiscardOption,
66 pub collapsed_sections: Vec<String>,
67 pub stash_list_limit: usize,
68 pub recent_commits_limit: usize,
69 pub log_author_width: usize,
70 pub mouse_support: bool,
71 pub mouse_scroll_lines: usize,
72 pub max_input_bytes: u64,
77 pub verbose_branch_status: bool,
80 #[serde(default)]
84 pub hide: Vec<String>,
85 #[serde(default)]
88 pub visit_context_lines: bool,
89 #[serde(default)]
92 pub status_cursor_on_changes: bool,
93 #[serde(default)]
94 pub diff_renderer: DiffRendererConfig,
95 #[serde(default)]
97 pub log_args: Vec<String>,
98 #[serde(default)]
100 pub show_args: Vec<String>,
101 #[serde(default)]
102 pub file_header: FileHeader,
103 #[serde(default)]
107 pub start_folded: bool,
108 #[serde(default)]
110 pub read_only: ReadOnly,
111 #[serde(default)]
116 pub commands: BTreeMap<String, String>,
117 #[serde(default)]
120 pub views: BTreeMap<String, String>,
121}
122
123#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
127pub enum ReadOnly {
128 #[default]
131 Auto,
132 On,
133 Off,
134}
135
136impl ReadOnly {
137 pub fn is_on(self, repo: &git2::Repository) -> bool {
138 match self {
139 ReadOnly::Auto => repo.workdir().is_some_and(|dir| dir.join(".jj").is_dir()),
140 ReadOnly::On => true,
141 ReadOnly::Off => false,
142 }
143 }
144}
145
146impl<'de> Deserialize<'de> for ReadOnly {
149 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
150 #[derive(Deserialize)]
151 #[serde(untagged)]
152 enum Raw {
153 Bool(bool),
154 Str(String),
155 }
156 match Raw::deserialize(deserializer)? {
157 Raw::Bool(true) => Ok(ReadOnly::On),
158 Raw::Bool(false) => Ok(ReadOnly::Off),
159 Raw::Str(value) => match value.to_lowercase().as_str() {
160 "auto" => Ok(ReadOnly::Auto),
161 "true" | "yes" | "on" | "1" | "" => Ok(ReadOnly::On),
162 "false" | "no" | "off" | "0" => Ok(ReadOnly::Off),
163 _ => Err(serde::de::Error::custom(format!(
164 "read_only: '{value}' is neither a boolean nor auto"
165 ))),
166 },
167 }
168 }
169}
170
171#[derive(Default, Debug, Deserialize, PartialEq, Eq, Clone, Copy)]
173#[serde(rename_all = "lowercase")]
174pub enum FileHeader {
175 #[default]
177 Delta,
178 Stat,
180}
181
182#[derive(Default, Debug, Deserialize)]
183pub struct DiffRendererConfig {
184 #[serde(skip)]
188 pub command: Vec<String>,
189 #[serde(default)]
192 pub features: Vec<String>,
193 #[serde(default)]
197 pub feature_keys: BTreeMap<String, char>,
198 #[serde(default)]
201 pub theme: Option<String>,
202 #[serde(default)]
205 pub builtin_themes: Vec<String>,
206 #[serde(skip)]
209 pub color_mode: Option<ColorMode>,
210 #[serde(skip)]
214 pub enabled_features: EnabledFeatures,
215 #[serde(skip)]
219 pub git_config: RendererGitConfig,
220}
221
222#[derive(Clone, Default)]
224pub struct RendererGitConfig(Option<Arc<OpenGitConfig>>);
225
226type OpenGitConfig = dyn Fn(&Path) -> Option<git2::Config> + Send + Sync;
227
228impl RendererGitConfig {
229 pub fn new(open: impl Fn(&Path) -> Option<git2::Config> + Send + Sync + 'static) -> Self {
230 Self(Some(Arc::new(open)))
231 }
232
233 pub fn open(&self, dir: &Path) -> Option<Option<git2::Config>> {
235 self.0.as_ref().map(|open| open(dir))
236 }
237}
238
239impl std::fmt::Debug for RendererGitConfig {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 f.write_str(if self.0.is_some() {
242 "RendererGitConfig(..)"
243 } else {
244 "RendererGitConfig(none)"
245 })
246 }
247}
248
249#[derive(Clone, Default)]
252pub struct EnabledFeatures(Option<Arc<AskEnabledFeatures>>);
253
254type AskEnabledFeatures = dyn Fn(&Path) -> Vec<String> + Send + Sync;
255
256impl EnabledFeatures {
257 pub fn new(ask: impl Fn(&Path) -> Vec<String> + Send + Sync + 'static) -> Self {
258 Self(Some(Arc::new(ask)))
259 }
260
261 pub fn get(&self, dir: &Path) -> Vec<String> {
262 self.0.as_ref().map(|ask| ask(dir)).unwrap_or_default()
263 }
264}
265
266impl std::fmt::Debug for EnabledFeatures {
267 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268 f.write_str(if self.0.is_some() {
269 "EnabledFeatures(..)"
270 } else {
271 "EnabledFeatures(none)"
272 })
273 }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub enum ColorMode {
279 Dark,
280 Light,
281}
282
283#[derive(Default, Debug, Deserialize)]
284pub struct BoolConfigEntry {
285 #[serde(default)]
286 pub enabled: bool,
287}
288
289#[derive(Default, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
290#[serde(rename_all = "lowercase")]
291pub enum ConfirmDiscardOption {
292 #[default]
293 Line,
294 Hunk,
295 File,
296 Never,
297}
298
299#[derive(Default, Debug, Deserialize)]
300pub struct StyleConfig {
301 pub separator: StyleConfigEntry,
302
303 pub info_msg: StyleConfigEntry,
304 pub error_msg: StyleConfigEntry,
305 pub command: StyleConfigEntry,
306
307 #[serde(default)]
308 pub menu: MenuStyleConfig,
309
310 pub prompt: StyleConfigEntry,
311
312 pub section_header: StyleConfigEntry,
313 pub file_header: StyleConfigEntry,
314 pub hunk_header: StyleConfigEntry,
315
316 #[serde(default)]
317 pub picker: PickerStyleConfig,
318
319 pub cursor: SymbolStyleConfigEntry,
320 pub selection_bar: SymbolStyleConfigEntry,
321 pub selection_line: StyleConfigEntry,
322 pub selection_area: StyleConfigEntry,
323
324 #[serde(default)]
325 pub search_match: StyleConfigEntry,
326
327 pub hash: StyleConfigEntry,
328 #[serde(default)]
330 pub rebase_todo: RebaseTodoStyleConfig,
331 pub branch: StyleConfigEntry,
332 pub remote: StyleConfigEntry,
333 pub tag: StyleConfigEntry,
334 pub author: StyleConfigEntry,
335 pub age: StyleConfigEntry,
336
337 #[serde(default)]
338 pub blame: BlameStyleConfig,
339}
340
341#[derive(Default, Debug, Deserialize)]
342pub struct RebaseTodoStyleConfig {
343 #[serde(default)]
344 pub reword: StyleConfigEntry,
345 #[serde(default)]
346 pub edit: StyleConfigEntry,
347 #[serde(default)]
348 pub squash: StyleConfigEntry,
349 #[serde(default)]
350 pub fixup: StyleConfigEntry,
351 #[serde(default)]
352 pub drop: StyleConfigEntry,
353}
354
355#[derive(Default, Debug, Deserialize)]
356pub struct BlameStyleConfig {
357 #[serde(default)]
358 pub line_num: StyleConfigEntry,
359 #[serde(default)]
360 pub code_line: StyleConfigEntry,
361}
362
363#[derive(Default, Debug, Deserialize)]
364pub struct MenuStyleConfig {
365 #[serde(default)]
366 pub heading: StyleConfigEntry,
367 #[serde(default)]
368 pub key: StyleConfigEntry,
369 #[serde(default)]
371 pub active_arg: StyleConfigEntry,
372 #[serde(default)]
374 pub inactive_arg: StyleConfigEntry,
375}
376
377#[derive(Default, Debug, Deserialize)]
378pub struct PickerStyleConfig {
379 #[serde(default)]
380 pub prompt: StyleConfigEntry,
381 #[serde(default)]
382 pub info: StyleConfigEntry,
383 #[serde(default)]
384 pub selection_line: StyleConfigEntry,
385 #[serde(default)]
386 pub matched: StyleConfigEntry,
387}
388
389#[derive(Default, Debug, Deserialize)]
390pub struct StyleConfigEntry {
391 #[serde(default)]
392 fg: Option<Color>,
393 #[serde(default)]
394 bg: Option<Color>,
395 #[serde(default)]
396 mods: Option<Modifier>,
397}
398
399#[derive(Default, Debug, Deserialize)]
400pub struct SymbolStyleConfigEntry {
401 #[serde(default)]
402 pub symbol: char,
403 #[serde(default)]
404 fg: Option<Color>,
405 #[serde(default)]
406 bg: Option<Color>,
407 #[serde(default)]
408 mods: Option<Modifier>,
409}
410
411impl From<&StyleConfigEntry> for Style {
412 fn from(val: &StyleConfigEntry) -> Self {
413 Style {
414 fg: val.fg,
415 bg: val.bg,
416 add_modifier: val.mods.unwrap_or(Modifier::empty()),
417 sub_modifier: Modifier::empty(),
418 }
419 }
420}
421
422impl From<&SymbolStyleConfigEntry> for Style {
423 fn from(val: &SymbolStyleConfigEntry) -> Self {
424 Style {
425 fg: val.fg,
426 bg: val.bg,
427 add_modifier: val.mods.unwrap_or(Modifier::empty()),
428 sub_modifier: Modifier::empty(),
429 }
430 }
431}
432
433pub struct PickerBindings {
434 pub next: Vec<Vec<(KeyModifiers, KeyCode)>>,
435 pub previous: Vec<Vec<(KeyModifiers, KeyCode)>>,
436 pub done: Vec<Vec<(KeyModifiers, KeyCode)>>,
437 pub cancel: Vec<Vec<(KeyModifiers, KeyCode)>>,
438 pub toggle_list: Vec<Vec<(KeyModifiers, KeyCode)>>,
439 pub quit: Vec<Vec<(KeyModifiers, KeyCode)>>,
440}
441
442impl TryFrom<PickerBindingsConfig> for PickerBindings {
443 type Error = crate::error::Error;
444
445 fn try_from(config: PickerBindingsConfig) -> Result<Self, Self::Error> {
446 let mut bad_bindings = Vec::new();
447
448 let next = parse_picker_keys(&config.next, "picker.next", &mut bad_bindings);
449 let previous = parse_picker_keys(&config.previous, "picker.previous", &mut bad_bindings);
450 let done = parse_picker_keys(&config.done, "picker.done", &mut bad_bindings);
451 let cancel = parse_picker_keys(&config.cancel, "picker.cancel", &mut bad_bindings);
452 let toggle_list =
453 parse_picker_keys(&config.toggle_list, "picker.toggle_list", &mut bad_bindings);
454 let quit = parse_picker_keys(&config.quit, "picker.quit", &mut bad_bindings);
455
456 if !bad_bindings.is_empty() {
457 return Err(Error::Bindings {
458 bad_key_bindings: bad_bindings,
459 });
460 }
461
462 Ok(Self {
463 next,
464 previous,
465 done,
466 cancel,
467 toggle_list,
468 quit,
469 })
470 }
471}
472
473fn parse_picker_keys(
474 raw_keys: &[String],
475 action_name: &str,
476 bad_bindings: &mut Vec<String>,
477) -> Vec<Vec<(KeyModifiers, KeyCode)>> {
478 raw_keys
479 .iter()
480 .filter_map(|keys| {
481 if let Ok(("", parsed)) = key_parser::parse_config_keys(keys) {
482 Some(parsed)
483 } else {
484 bad_bindings.push(format!("- {} = {}", action_name, keys));
485 None
486 }
487 })
488 .collect()
489}
490
491pub fn init_config(path: Option<PathBuf>) -> Res<Config> {
492 let git_config = git2::Repository::open_from_env()
493 .and_then(|repo| repo.config())
494 .or_else(|_| git2::Config::open_default())
495 .ok();
496 init_config_from(path.unwrap_or_else(config_path), git_config.as_ref())
497}
498
499pub(crate) fn init_config_from(
502 config_path: PathBuf,
503 git_config: Option<&git2::Config>,
504) -> Res<Config> {
505 if config_path.exists() {
506 log::info!("Loading config file at {config_path:?}");
507 } else {
508 log::info!("No config file at {config_path:?}");
509 }
510
511 let defaults = Figment::from(Toml::string(DEFAULT_CONFIG));
512 let file = Figment::from(Toml::file(&config_path));
513 let git = match git_config {
514 Some(git_config) => Figment::from(Serialized::defaults(config_git::read(
515 git_config, &defaults,
516 )?)),
517 None => Figment::new(),
518 };
519 let FigmentConfig {
520 mut general,
521 style,
522 bindings: bindings_config,
523 } = defaults
524 .merge(file.clone())
525 .merge(git.clone())
526 .extract()
527 .map_err(Box::new)
528 .map_err(Error::Config)?;
529 if let Some(feature_keys) = users_feature_keys(&git)?.or(users_feature_keys(&file)?) {
530 general.diff_renderer.feature_keys = feature_keys;
531 }
532 let bindings = Bindings::try_from(bindings_config.menus)?;
533 let picker_bindings = PickerBindings::try_from(bindings_config.picker)?;
534 check_feature_keys(&general.diff_renderer.feature_keys, &bindings)?;
535
536 Ok(Config {
537 general,
538 style,
539 bindings,
540 picker_bindings,
541 })
542}
543
544fn users_feature_keys(figment: &Figment) -> Res<Option<BTreeMap<String, char>>> {
547 if !figment.contains(FEATURE_KEYS) {
548 return Ok(None);
549 }
550 figment
551 .extract_inner(FEATURE_KEYS)
552 .map(Some)
553 .map_err(Box::new)
554 .map_err(Error::Config)
555}
556
557fn check_feature_keys(feature_keys: &BTreeMap<String, char>, bindings: &Bindings) -> Res<()> {
560 let taken: Vec<String> = feature_keys
561 .iter()
562 .filter(|(_, key)| bindings.binds_at_top_level(**key))
563 .map(|(feature, key)| {
564 format!("- general.diff_renderer.feature_keys.{feature} = {key} (bound already)")
565 })
566 .collect();
567 if !taken.is_empty() {
568 return Err(Error::Bindings {
569 bad_key_bindings: taken,
570 });
571 }
572 Ok(())
573}
574
575pub fn config_path() -> PathBuf {
576 choose_base_strategy()
577 .expect("Unable to find the config directory!")
578 .config_dir()
579 .join("gitu/config.toml")
580}
581
582#[cfg(test)]
583pub(crate) const TEST_SEARCH_HIGHLIGHT_BG: Color = Color::LightYellow;
584
585#[cfg(test)]
586pub(crate) fn init_test_config() -> Res<Config> {
587 let FigmentConfig {
588 mut general,
589 mut style,
590 bindings: bindings_config,
591 } = Figment::new()
592 .merge(Toml::string(DEFAULT_CONFIG))
593 .extract()
594 .map_err(Box::new)
595 .map_err(Error::Config)?;
596
597 general.always_show_help.enabled = false;
598 general.refresh_on_file_change.enabled = false;
599 general.status_cursor_on_changes = false;
600 general.diff_renderer.command = crate::tests::helpers::ui::renderer();
601
602 style.search_match = StyleConfigEntry {
603 fg: None,
604 bg: Some(TEST_SEARCH_HIGHLIGHT_BG),
605 mods: None,
606 };
607
608 Ok(Config {
609 general,
610 style,
611 bindings: Bindings::try_from(bindings_config.menus).unwrap(),
612 picker_bindings: PickerBindings::try_from(bindings_config.picker).unwrap(),
613 })
614}
615
616#[cfg(test)]
617mod tests {
618 use std::fs;
619
620 use crate::style::{Color, Modifier};
621 use figment::{
622 Figment,
623 providers::{Format, Toml},
624 };
625 use temp_dir::TempDir;
626
627 use super::{Config, DEFAULT_CONFIG, FigmentConfig, init_config_from};
628
629 #[test]
630 fn config_merges() {
631 let config: FigmentConfig = Figment::new()
632 .merge(Toml::string(DEFAULT_CONFIG))
633 .merge(Toml::string(
634 r#"
635 [style]
636 hunk_header.bg = "light green"
637 "#,
638 ))
639 .extract()
640 .unwrap();
641
642 assert_eq!(config.style.hunk_header.bg, Some(Color::LightGreen));
643 assert_eq!(config.style.hunk_header.fg, Some(Color::Blue));
644 }
645
646 fn configured(toml: &str, gitconfig: &str) -> Config {
649 let dir = TempDir::new().unwrap();
650 let toml_path = dir.path().join("config.toml");
651 fs::write(&toml_path, toml).unwrap();
652 let git_path = dir.path().join("gitconfig");
653 fs::write(&git_path, gitconfig).unwrap();
654 let git_config = git2::Config::open(&git_path).unwrap();
655 init_config_from(toml_path, Some(&git_config)).unwrap()
656 }
657
658 fn config_error(gitconfig: &str) -> String {
659 let dir = TempDir::new().unwrap();
660 let git_path = dir.path().join("gitconfig");
661 fs::write(&git_path, gitconfig).unwrap();
662 let git_config = git2::Config::open(&git_path).unwrap();
663 match init_config_from(dir.path().join("config.toml"), Some(&git_config)) {
664 Ok(_) => panic!("{gitconfig} was accepted"),
665 Err(e) => e.to_string(),
666 }
667 }
668
669 #[test]
670 fn git_config_sets_general_settings() {
671 let config = configured(
672 "",
673 "[delta-tui]\n\
674 \tstart-folded = true\n\
675 \tfile-header = stat\n\
676 \tstash-list-limit = 3\n",
677 );
678 assert!(config.general.start_folded);
679 assert_eq!(config.general.file_header, super::FileHeader::Stat);
680 assert_eq!(config.general.stash_list_limit, 3);
681 }
682
683 #[test]
685 fn git_config_values_are_read_as_git_reads_them() {
686 let config = configured(
687 "",
688 "[delta-tui]\n\
689 \tstart-folded\n\
690 \tvisit-context-lines = no\n\
691 \tmax-input-bytes = 4m\n",
692 );
693 assert!(config.general.start_folded);
694 assert!(!config.general.visit_context_lines);
695 assert_eq!(config.general.max_input_bytes, 4 * 1024 * 1024);
696 }
697
698 #[test]
700 fn git_config_lists_are_repeated_keys() {
701 let config = configured(
702 "",
703 "[delta-tui]\n\
704 \tlog-args = --stat\n\
705 \tlog-args = --pretty=format:%h %s\n\
706 \tshow-args = --stat\n",
707 );
708 assert_eq!(config.general.log_args, ["--stat", "--pretty=format:%h %s"]);
709 assert_eq!(config.general.show_args, ["--stat"]);
710 }
711
712 #[test]
713 fn git_config_overrides_the_gitu_config_file() {
714 let config = configured(
715 "[general]\nstart_folded = true\nstash_list_limit = 3\n",
716 "[delta-tui]\n\tstart-folded = false\n",
717 );
718 assert!(!config.general.start_folded);
719 assert_eq!(config.general.stash_list_limit, 3);
720 }
721
722 #[test]
723 fn git_config_subsections_reach_nested_settings() {
724 let config = configured(
725 "",
726 "[delta-tui \"general.diff-renderer\"]\n\
727 \tfeatures = side-by-side\n\
728 \tfeatures = my-*\n",
729 );
730 assert_eq!(
731 config.general.diff_renderer.features,
732 ["side-by-side", "my-*"]
733 );
734 }
735
736 #[test]
738 fn git_config_lists_builtin_themes() {
739 let config = configured(
740 "",
741 "[delta-tui \"general.diff-renderer\"]\n\tbuiltin-themes = diff-so-fancy\n",
742 );
743 assert_eq!(
744 config.general.diff_renderer.builtin_themes,
745 ["diff-so-fancy"]
746 );
747 }
748
749 #[test]
752 fn git_config_sets_feature_keys_by_feature_name() {
753 let config = configured(
754 "",
755 "[delta-tui \"general.diff-renderer.feature-keys\"]\n\
756 \tmy-feature = x\n",
757 );
758 assert_eq!(
759 config
760 .general
761 .diff_renderer
762 .feature_keys
763 .into_iter()
764 .collect::<Vec<_>>(),
765 [("my-feature".to_string(), 'x')]
766 );
767 }
768
769 #[test]
771 fn git_config_sets_commands_by_name() {
772 let config = configured(
773 "",
774 "[delta-tui \"general.commands\"]\n\
775 \topen-pr = gh pr view --web {commit}\n",
776 );
777 assert_eq!(
778 config.general.commands.into_iter().collect::<Vec<_>>(),
779 [(
780 "open-pr".to_string(),
781 "gh pr view --web {commit}".to_string()
782 )]
783 );
784 }
785
786 #[test]
788 fn git_config_sets_views_by_name() {
789 let config = configured(
790 "",
791 "[delta-tui \"general.views\"]\n\
792 \tdiff-since = git diff {commit}...\n",
793 );
794 assert_eq!(
795 config.general.views.into_iter().collect::<Vec<_>>(),
796 [("diff-since".to_string(), "git diff {commit}...".to_string())]
797 );
798 }
799
800 #[test]
801 fn git_config_sets_bindings() {
802 let config = configured(
803 "",
804 "[delta-tui \"bindings.root\"]\n\
805 \tquit = x\n\
806 \tquit = esc\n",
807 );
808 assert!(config.bindings.binds_at_top_level('x'));
809 assert!(!config.bindings.binds_at_top_level('q'));
810 }
811
812 #[test]
815 fn git_config_styles_are_delta_styles() {
816 let config = configured(
817 "",
818 "[delta-tui \"style\"]\n\
819 \thunk-header = bold italic \"#ff0000\" 237\n\
820 \tfile-header = ul\n\
821 \tsection-header = normal blue\n",
822 );
823 let style = &config.style;
824 assert_eq!(style.hunk_header.fg, Some(Color::Rgb(255, 0, 0)));
825 assert_eq!(style.hunk_header.bg, Some(Color::Indexed(237)));
826 assert_eq!(
827 style.hunk_header.mods,
828 Some(Modifier::BOLD | Modifier::ITALIC)
829 );
830 assert_eq!(style.file_header.fg, None);
831 assert_eq!(style.file_header.mods, Some(Modifier::UNDERLINED));
832 assert_eq!(style.section_header.fg, None);
833 assert_eq!(style.section_header.bg, Some(Color::Blue));
834 }
835
836 #[test]
837 fn a_git_config_style_keeps_its_symbol() {
838 let config = configured("", "[delta-tui \"style\"]\n\tcursor = red\n");
839 assert_eq!(config.style.cursor.symbol, '▌');
840 assert_eq!(config.style.cursor.fg, Some(Color::Red));
841 }
842
843 #[test]
844 fn a_git_config_style_inside_a_group_of_styles() {
845 let config = configured("", "[delta-tui \"style.menu\"]\n\tkey = green\n");
846 assert_eq!(config.style.menu.key.fg, Some(Color::Green));
847 }
848
849 #[test]
850 fn a_bad_git_config_value_is_an_error_naming_the_key() {
851 assert!(
852 config_error("[delta-tui]\n\tstart-folded = maybe\n")
853 .contains("delta-tui.start-folded")
854 );
855 assert!(
856 config_error("[delta-tui \"style\"]\n\thunk-header = bold sky\n")
857 .contains("delta-tui.style.hunk-header")
858 );
859 }
860}