use std::collections::{HashMap, HashSet}; use crate::cli; use crate::color; use crate::fatal; use crate::git_config::GitConfig; use crate::style::{self, Style}; #[derive(Debug, Clone)] enum StyleReference { Style(Style), Reference(String), } fn is_style_reference(style_string: &str) -> bool { style_string.ends_with("-style") && !style_string.chars().any(|c| c == ' ') } pub fn parse_styles(opt: &cli::Opt) -> HashMap { let mut styles: HashMap<&str, StyleReference> = HashMap::new(); make_hunk_styles(opt, &mut styles); make_commit_file_hunk_header_styles(opt, &mut styles); make_line_number_styles(opt, &mut styles); make_blame_styles(opt, &mut styles); make_grep_styles(opt, &mut styles); make_merge_conflict_styles(opt, &mut styles); make_misc_styles(opt, &mut styles); let mut resolved_styles = resolve_style_references(styles, opt); resolved_styles.get_mut("minus-emph-style").unwrap().is_emph = true; resolved_styles.get_mut("plus-emph-style").unwrap().is_emph = true; resolved_styles } pub fn parse_styles_map(opt: &cli::Opt) -> Option> { if let Some(styles_map_str) = &opt.map_styles { let mut styles_map = HashMap::new(); for pair_str in styles_map_str.split(',') { let mut style_strs = pair_str.split("=>").map(|s| s.trim()); if let (Some(from_str), Some(to_str)) = (style_strs.next(), style_strs.next()) { let from_style = parse_as_style_or_reference_to_git_config(from_str, opt); let to_style = parse_as_style_or_reference_to_git_config(to_str, opt); styles_map.insert( style::ansi_term_style_equality_key(from_style.ansi_term_style), to_style, ); } } Some(styles_map) } else { None } } fn resolve_style_references( edges: HashMap<&str, StyleReference>, opt: &cli::Opt, ) -> HashMap { let mut resolved_styles = HashMap::new(); for starting_node in edges.keys() { if resolved_styles.contains_key(*starting_node) { continue; } let mut visited = HashSet::new(); let mut node = *starting_node; loop { if !visited.insert(node) { #[cfg(not(test))] fatal(format!("Your delta styles form a cycle! {visited:?}")); #[cfg(test)] return [("__cycle__", Style::default())] .iter() .map(|(a, b)| (a.to_string(), *b)) .collect(); } match &edges.get(&node) { Some(StyleReference::Reference(child_node)) => node = child_node, Some(StyleReference::Style(style)) => { resolved_styles.extend(visited.iter().map(|node| (node.to_string(), *style))); break; } None => { let style = parse_as_reference_to_git_config(node, opt); resolved_styles.extend(visited.iter().map(|node| (node.to_string(), style))); } } } } resolved_styles } fn parse_as_style_or_reference_to_git_config(style_string: &str, opt: &cli::Opt) -> Style { match style_from_str(style_string, None, None, true, opt.git_config()) { StyleReference::Reference(style_ref) => parse_as_reference_to_git_config(&style_ref, opt), StyleReference::Style(style) => style, } } fn parse_as_reference_to_git_config(style_string: &str, opt: &cli::Opt) -> Style { if let Some(git_config) = opt.git_config() { let git_config_key = format!("delta.{style_string}"); match git_config.get::(&git_config_key) { Some(s) => Style::from_git_str(&s), _ => fatal(format!( "Style key not found in git config: {git_config_key}", )), } } else { fatal(format!( "Style not found (git config unavailable): {style_string}", )); } } fn make_hunk_styles<'a>(opt: &'a cli::Opt, styles: &'a mut HashMap<&str, StyleReference>) { let is_light_mode = opt.computed.is_light_mode; let true_color = opt.computed.true_color; let minus_style = style_from_str( &opt.minus_style, Some(Style::from_colors( None, Some(color::get_minus_background_color_default( is_light_mode, true_color, )), )), None, true_color, opt.git_config(), ); let minus_emph_style = style_from_str( &opt.minus_emph_style, Some(Style::from_colors( None, Some(color::get_minus_emph_background_color_default( is_light_mode, true_color, )), )), None, true_color, opt.git_config(), ); let minus_non_emph_style = style_from_str( &opt.minus_non_emph_style, None, None, true_color, opt.git_config(), ); // The style used to highlight a removed empty line when otherwise it would be invisible due to // lack of background color in minus-style. let minus_empty_line_marker_style = style_from_str( &opt.minus_empty_line_marker_style, Some(Style::from_colors( None, Some(color::get_minus_background_color_default( is_light_mode, true_color, )), )), None, true_color, opt.git_config(), ); let zero_style = style_from_str(&opt.zero_style, None, None, true_color, opt.git_config()); let plus_style = style_from_str( &opt.plus_style, Some(Style::from_colors( None, Some(color::get_plus_background_color_default( is_light_mode, true_color, )), )), None, true_color, opt.git_config(), ); let plus_emph_style = style_from_str( &opt.plus_emph_style, Some(Style::from_colors( None, Some(color::get_plus_emph_background_color_default( is_light_mode, true_color, )), )), None, true_color, opt.git_config(), ); let plus_non_emph_style = style_from_str( &opt.plus_non_emph_style, None, None, true_color, opt.git_config(), ); // The style used to highlight an added empty line when otherwise it would be invisible due to // lack of background color in plus-style. let plus_empty_line_marker_style = style_from_str( &opt.plus_empty_line_marker_style, Some(Style::from_colors( None, Some(color::get_plus_background_color_default( is_light_mode, true_color, )), )), None, true_color, opt.git_config(), ); let whitespace_error_style = style_from_str( &opt.whitespace_error_style, None, None, true_color, opt.git_config(), ); styles.extend([ ("minus-style", minus_style), ("minus-emph-style", minus_emph_style), ("minus-non-emph-style", minus_non_emph_style), ( "minus-empty-line-marker-style", minus_empty_line_marker_style, ), ("zero-style", zero_style), ("plus-style", plus_style), ("plus-emph-style", plus_emph_style), ("plus-non-emph-style", plus_non_emph_style), ("plus-empty-line-marker-style", plus_empty_line_marker_style), ("whitespace-error-style", whitespace_error_style), ]) } fn make_line_number_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) { let true_color = opt.computed.true_color; let line_numbers_left_style = style_from_str( &opt.line_numbers_left_style, None, None, true_color, opt.git_config(), ); let line_numbers_minus_style = style_from_str( &opt.line_numbers_minus_style, None, None, true_color, opt.git_config(), ); let line_numbers_zero_style = style_from_str( &opt.line_numbers_zero_style, None, None, true_color, opt.git_config(), ); let line_numbers_plus_style = style_from_str( &opt.line_numbers_plus_style, None, None, true_color, opt.git_config(), ); let line_numbers_right_style = style_from_str( &opt.line_numbers_right_style, None, None, true_color, opt.git_config(), ); styles.extend([ ("line-numbers-minus-style", line_numbers_minus_style), ("line-numbers-zero-style", line_numbers_zero_style), ("line-numbers-plus-style", line_numbers_plus_style), ("line-numbers-left-style", line_numbers_left_style), ("line-numbers-right-style", line_numbers_right_style), ]) } fn make_commit_file_hunk_header_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) { let true_color = opt.computed.true_color; styles.extend([ ( "commit-style", style_from_str_with_handling_of_special_decoration_attributes( &opt.commit_style, None, Some(&opt.commit_decoration_style), true_color, opt.git_config(), ), ), ( "file-style", style_from_str_with_handling_of_special_decoration_attributes( &opt.file_style, None, Some(&opt.file_decoration_style), true_color, opt.git_config(), ), ), ( "classic-grep-header-style", style_from_str_with_handling_of_special_decoration_attributes( opt.hunk_header_style.as_str(), None, opt.grep_header_decoration_style .as_deref() .or(Some(opt.hunk_header_decoration_style.as_str())), true_color, opt.git_config(), ), ), ( "ripgrep-header-style", style_from_str_with_handling_of_special_decoration_attributes( "file", None, opt.grep_header_decoration_style.as_deref().or(Some("none")), true_color, opt.git_config(), ), ), ( "hunk-header-style", style_from_str_with_handling_of_special_decoration_attributes( &opt.hunk_header_style, None, Some(&opt.hunk_header_decoration_style), true_color, opt.git_config(), ), ), ( "hunk-header-file-style", style_from_str_with_handling_of_special_decoration_attributes( &opt.hunk_header_file_style, None, None, true_color, opt.git_config(), ), ), ( "classic-grep-header-file-style", style_from_str_with_handling_of_special_decoration_attributes( opt.grep_header_file_style .as_deref() .unwrap_or(opt.hunk_header_file_style.as_str()), None, None, true_color, opt.git_config(), ), ), ( "ripgrep-header-file-style", style_from_str_with_handling_of_special_decoration_attributes( opt.grep_header_file_style.as_deref().unwrap_or("magenta"), None, None, true_color, opt.git_config(), ), ), ( "hunk-header-line-number-style", style_from_str_with_handling_of_special_decoration_attributes( &opt.hunk_header_line_number_style, None, None, true_color, opt.git_config(), ), ), ]); } fn make_blame_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) { if let Some(style_string) = &opt.blame_code_style { styles.insert( "blame-code-style", style_from_str( style_string, None, None, opt.computed.true_color, opt.git_config(), ), ); }; if let Some(style_string) = &opt.blame_separator_style { styles.insert( "blame-separator-style", style_from_str( style_string, None, None, opt.computed.true_color, opt.git_config(), ), ); }; } fn make_grep_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) { styles.extend([ ( "grep-match-line-style", if let Some(s) = &opt.grep_match_line_style { style_from_str(s, None, None, opt.computed.true_color, opt.git_config()) } else { StyleReference::Reference("zero-style".to_owned()) }, ), ( "grep-match-word-style", if let Some(s) = &opt.grep_match_word_style { style_from_str(s, None, None, opt.computed.true_color, opt.git_config()) } else { StyleReference::Reference("plus-emph-style".to_owned()) }, ), ( "grep-context-line-style", if let Some(s) = &opt.grep_context_line_style { style_from_str(s, None, None, opt.computed.true_color, opt.git_config()) } else { StyleReference::Reference("zero-style".to_owned()) }, ), ( "grep-file-style", style_from_str( &opt.grep_file_style, None, None, opt.computed.true_color, opt.git_config(), ), ), ( "grep-line-number-style", style_from_str( &opt.grep_line_number_style, None, None, opt.computed.true_color, opt.git_config(), ), ), ]) } fn make_merge_conflict_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) { styles.insert( "merge-conflict-ours-diff-header-style", style_from_str_with_handling_of_special_decoration_attributes( &opt.merge_conflict_ours_diff_header_style, None, Some(&opt.merge_conflict_ours_diff_header_decoration_style), opt.computed.true_color, opt.git_config(), ), ); styles.insert( "merge-conflict-theirs-diff-header-style", style_from_str_with_handling_of_special_decoration_attributes( &opt.merge_conflict_theirs_diff_header_style, None, Some(&opt.merge_conflict_theirs_diff_header_decoration_style), opt.computed.true_color, opt.git_config(), ), ); } fn make_misc_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) { styles.insert( "inline-hint-style", style_from_str( &opt.inline_hint_style, None, None, opt.computed.true_color, opt.git_config(), ), ); styles.insert( "git-minus-style", StyleReference::Style( match opt .git_config() .and_then(|cfg| cfg.get::("color.diff.old")) { Some(s) => Style::from_git_str(&s), None => *style::GIT_DEFAULT_MINUS_STYLE, }, ), ); styles.insert( "git-plus-style", StyleReference::Style( match opt .git_config() .and_then(|cfg| cfg.get::("color.diff.new")) { Some(s) => Style::from_git_str(&s), None => *style::GIT_DEFAULT_PLUS_STYLE, }, ), ); } fn style_from_str( style_string: &str, default: Option