diff --git a/CHANGELOG.md b/CHANGELOG.md index b6a8a1b05..1400f0556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ more discoverable. Added a `--color` option which allows explicitly enabling/disabling colour output. +Added a `--background` option which controls whether difftastic uses +bright or dark colours. + ## 0.17 (released 25 January 2022) ### Diffing diff --git a/src/inline.rs b/src/inline.rs index b00a39caf..ed6191eb1 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -4,7 +4,7 @@ use crate::{ context::{calculate_after_context, calculate_before_context, opposite_positions}, hunks::Hunk, lines::{format_line_num, MaxLine}, - style::{self, apply_colors}, + style::{self, apply_colors, BackgroundColor}, syntax::MatchedPos, }; use colored::*; @@ -17,9 +17,10 @@ pub fn display( hunks: &[Hunk], display_path: &str, lang_name: &str, + background: BackgroundColor, ) -> String { - let lhs_colored = apply_colors(lhs_src, true, lhs_positions); - let rhs_colored = apply_colors(rhs_src, false, rhs_positions); + let lhs_colored = apply_colors(lhs_src, true, background, lhs_positions); + let rhs_colored = apply_colors(rhs_src, false, background, rhs_positions); let lhs_lines: Vec<_> = lhs_colored.lines().collect(); let rhs_lines: Vec<_> = rhs_colored.lines().collect(); @@ -30,7 +31,13 @@ pub fn display( let opposite_to_rhs = opposite_positions(rhs_positions); for (i, hunk) in hunks.iter().enumerate() { - res.push_str(&style::header(display_path, i + 1, hunks.len(), lang_name)); + res.push_str(&style::header( + display_path, + i + 1, + hunks.len(), + lang_name, + background, + )); res.push('\n'); let hunk_lines = hunk.lines.clone(); diff --git a/src/main.rs b/src/main.rs index 3f37d759e..08eaa9f3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,7 @@ use atty::Stream; use clap::{crate_authors, crate_description, crate_version, App, AppSettings, Arg}; use sliders::fix_all_sliders; use std::{env, path::Path}; +use style::BackgroundColor; use summary::DiffResult; use syntax::init_next_prev; use typed_arena::Arena; @@ -93,6 +94,7 @@ enum ColorOutput { enum Mode { Diff { + background_color: BackgroundColor, color_output: ColorOutput, display_width: usize, display_path: String, @@ -145,6 +147,12 @@ fn app() -> clap::App<'static> { .value_name("WHEN") .help("When to use color output.") ) + .arg( + Arg::new("background").long("background") + .default_value("dark") + .possible_values(["dark", "light"]) + .help("Set the background color. Difftastic will prefer brighter colours on dark backgrounds.") + ) .arg( Arg::new("paths") .multiple_values(true) @@ -230,7 +238,18 @@ fn parse_args() -> Mode { ColorOutput::Auto }; + let background_color = if let Some(background) = matches.value_of("background") { + if background == "light" { + BackgroundColor::Light + } else { + BackgroundColor::Dark + } + } else { + BackgroundColor::Dark + }; + Mode::Diff { + background_color, color_output, display_width, display_path, @@ -292,6 +311,7 @@ fn main() { } } Mode::Diff { + background_color, color_output, display_width, display_path, @@ -306,11 +326,11 @@ fn main() { if lhs_path.is_dir() && rhs_path.is_dir() { for diff_result in diff_directories(lhs_path, rhs_path) { - print_diff_result(display_width, &diff_result); + print_diff_result(display_width, background_color, &diff_result); } } else { let diff_result = diff_file(&display_path, lhs_path, rhs_path); - print_diff_result(display_width, &diff_result); + print_diff_result(display_width, background_color, &diff_result); } } }; @@ -432,9 +452,12 @@ fn diff_directories(lhs_dir: &Path, rhs_dir: &Path) -> Vec { res } -fn print_diff_result(display_width: usize, summary: &DiffResult) { +fn print_diff_result(display_width: usize, background: BackgroundColor, summary: &DiffResult) { if summary.binary { - println!("{}", style::header(&summary.path, 1, 1, "binary")); + println!( + "{}", + style::header(&summary.path, 1, 1, "binary", background) + ); return; } @@ -452,7 +475,10 @@ fn print_diff_result(display_width: usize, summary: &DiffResult) { let lang_name = summary.language.clone().unwrap_or_else(|| "text".into()); if hunks.is_empty() { - println!("{}", style::header(&summary.path, 1, 1, &lang_name)); + println!( + "{}", + style::header(&summary.path, 1, 1, &lang_name, background) + ); if lang_name == "text" { println!("No changes.\n"); } else { @@ -472,6 +498,7 @@ fn print_diff_result(display_width: usize, summary: &DiffResult) { &hunks, &summary.path, &lang_name, + background ) ); } else { @@ -480,6 +507,7 @@ fn print_diff_result(display_width: usize, summary: &DiffResult) { side_by_side::display_hunks( &hunks, display_width, + background, &summary.path, &lang_name, &summary.lhs_src, diff --git a/src/side_by_side.rs b/src/side_by_side.rs index 2a85ed785..a766c866e 100644 --- a/src/side_by_side.rs +++ b/src/side_by_side.rs @@ -11,7 +11,7 @@ use crate::{ hunks::{matched_lines_for_hunk, Hunk}, lines::{codepoint_len, format_line_num, LineNumber}, positions::SingleLineSpan, - style::{self, apply_colors, color_positions, split_and_apply, Style}, + style::{self, apply_colors, color_positions, split_and_apply, BackgroundColor, Style}, syntax::{zip_pad_shorter, MatchedPos}, }; @@ -45,11 +45,17 @@ fn format_missing_line_num(prev_num: LineNumber, column_width: usize) -> String } /// Display `src` in a single column (e.g. a file removal or addition). -fn display_single_column(display_path: &str, lang_name: &str, src: &str, color: Color) -> String { +fn display_single_column( + display_path: &str, + lang_name: &str, + src: &str, + color: Color, + background: BackgroundColor, +) -> String { let column_width = format_line_num(src.lines().count().into()).len(); let mut result = String::with_capacity(src.len()); - result.push_str(&style::header(display_path, 1, 1, lang_name)); + result.push_str(&style::header(display_path, 1, 1, lang_name, background)); result.push('\n'); for (i, line) in src.lines().enumerate() { @@ -69,6 +75,7 @@ fn display_line_nums( lhs_line_num: Option, rhs_line_num: Option, widths: &Widths, + background: BackgroundColor, lhs_has_novel: bool, rhs_has_novel: bool, prev_lhs_line_num: Option, @@ -78,7 +85,12 @@ fn display_line_nums( Some(line_num) => { let s = format_line_num_padded(line_num, widths.lhs_line_nums); if lhs_has_novel { - s.red().bold().to_string() + // TODO: factor out applying colours to line numbers. + match background { + BackgroundColor::Dark => s.bright_red(), + BackgroundColor::Light => s.red(), + } + .to_string() } else { s } @@ -92,7 +104,11 @@ fn display_line_nums( Some(line_num) => { let s = format_line_num_padded(line_num, widths.rhs_line_nums); if rhs_has_novel { - s.green().bold().to_string() + match background { + BackgroundColor::Dark => s.bright_green(), + BackgroundColor::Light => s.green(), + } + .to_string() } else { s } @@ -175,13 +191,14 @@ fn lines_with_novel( /// Calculate positions of highlights on both sides. This includes /// both syntax highlighting and added/removed content highlighting. fn highlight_positions( + background: BackgroundColor, lhs_mps: &[MatchedPos], rhs_mps: &[MatchedPos], ) -> ( HashMap>, HashMap>, ) { - let lhs_positions = color_positions(true, lhs_mps); + let lhs_positions = color_positions(true, background, lhs_mps); // Preallocate the hashmap assuming the average line will have 2 items on it. let mut lhs_styles: HashMap> = HashMap::with_capacity(lhs_positions.len() / 2); @@ -190,7 +207,7 @@ fn highlight_positions( styles.push((span, style)); } - let rhs_positions = color_positions(false, rhs_mps); + let rhs_positions = color_positions(false, background, rhs_mps); let mut rhs_styles: HashMap> = HashMap::with_capacity(rhs_positions.len() / 2); for (span, style) in rhs_positions { @@ -228,6 +245,7 @@ fn highlight_as_novel( pub fn display_hunks( hunks: &[Hunk], display_width: usize, + background: BackgroundColor, display_path: &str, lang_name: &str, lhs_src: &str, @@ -235,18 +253,32 @@ pub fn display_hunks( lhs_mps: &[MatchedPos], rhs_mps: &[MatchedPos], ) -> String { - let lhs_colored_src = apply_colors(lhs_src, true, lhs_mps); - let rhs_colored_src = apply_colors(rhs_src, false, rhs_mps); + let lhs_colored_src = apply_colors(lhs_src, true, background, lhs_mps); + let rhs_colored_src = apply_colors(rhs_src, false, background, rhs_mps); if lhs_src.is_empty() { - return display_single_column(display_path, lang_name, &rhs_colored_src, Color::Green); + // TODO: use BrightGreen on dark backgrounds. + // TODO: this doesn't need the coloured source as it applies colours. + return display_single_column( + display_path, + lang_name, + &rhs_colored_src, + Color::Green, + background, + ); } if rhs_src.is_empty() { - return display_single_column(display_path, lang_name, &lhs_colored_src, Color::Red); + return display_single_column( + display_path, + lang_name, + &lhs_colored_src, + Color::Red, + background, + ); } // TODO: this is largely duplicating the `apply_colors` logic. - let (lhs_highlights, rhs_highlights) = highlight_positions(lhs_mps, rhs_mps); + let (lhs_highlights, rhs_highlights) = highlight_positions(background, lhs_mps, rhs_mps); let lhs_lines = split_on_newlines(lhs_src); let rhs_lines = split_on_newlines(rhs_src); let lhs_colored_lines = split_on_newlines(&lhs_colored_src); @@ -262,7 +294,13 @@ pub fn display_hunks( let mut out_lines: Vec = vec![]; for (i, hunk) in hunks.iter().enumerate() { - out_lines.push(style::header(display_path, i + 1, hunks.len(), lang_name)); + out_lines.push(style::header( + display_path, + i + 1, + hunks.len(), + lang_name, + background, + )); let aligned_lines = matched_lines_for_hunk(&matched_lines, hunk); let no_lhs_changes = hunk.lines.iter().all(|(l, _)| l.is_none()); @@ -288,6 +326,7 @@ pub fn display_hunks( lhs_line_num, rhs_line_num, &widths, + background, lhs_line_novel, rhs_line_novel, prev_lhs_line_num, @@ -365,7 +404,11 @@ pub fn display_hunks( ); if let Some(line_num) = lhs_line_num { if lhs_lines_with_novel.contains(&line_num) { - s = s.red().bold().to_string() + s = match background { + BackgroundColor::Dark => s.bright_red(), + BackgroundColor::Light => s.red(), + } + .to_string(); } } s @@ -380,7 +423,11 @@ pub fn display_hunks( ); if let Some(line_num) = rhs_line_num { if rhs_lines_with_novel.contains(&line_num) { - s = s.green().bold().to_string(); + s = match background { + BackgroundColor::Dark => s.bright_green(), + BackgroundColor::Light => s.green(), + } + .to_string(); } } s @@ -430,7 +477,13 @@ mod tests { #[test] fn test_display_single_column() { // Basic smoke test. - let res = display_single_column("foo.py", "Python", "print(123)\n", Color::Green); + let res = display_single_column( + "foo.py", + "Python", + "print(123)\n", + Color::Green, + BackgroundColor::Dark, + ); assert!(res.len() > 10); } @@ -493,6 +546,7 @@ mod tests { display_hunks( &hunks, 80, + BackgroundColor::Dark, "foo.el", "Emacs Lisp", "foo", diff --git a/src/style.rs b/src/style.rs index aa2968930..bf216833a 100644 --- a/src/style.rs +++ b/src/style.rs @@ -11,6 +11,12 @@ use std::{ collections::HashMap, }; +#[derive(Clone, Copy, Debug)] +pub enum BackgroundColor { + Dark, + Light, +} + #[derive(Clone, Copy, Debug)] pub struct Style { foreground: Option, @@ -192,15 +198,35 @@ fn apply(s: &str, styles: &[(SingleLineSpan, Style)]) -> String { res } -pub fn color_positions(is_lhs: bool, positions: &[MatchedPos]) -> Vec<(SingleLineSpan, Style)> { +pub fn color_positions( + is_lhs: bool, + background: BackgroundColor, + positions: &[MatchedPos], +) -> Vec<(SingleLineSpan, Style)> { + let red = match background { + BackgroundColor::Dark => Color::BrightRed, + BackgroundColor::Light => Color::Red, + }; + let green = match background { + BackgroundColor::Dark => Color::BrightGreen, + BackgroundColor::Light => Color::Green, + }; + let novel_color = if is_lhs { red } else { green }; + let mut styles = vec![]; for pos in positions { let line_pos = pos.pos; let style = match pos.kind { MatchKind::Unchanged { highlight, .. } => Style { foreground: match highlight { - TokenKind::Atom(AtomKind::String) => Some(Color::Magenta), - TokenKind::Atom(AtomKind::Comment) => Some(Color::Cyan), + TokenKind::Atom(AtomKind::String) => Some(match background { + BackgroundColor::Dark => Color::BrightMagenta, + BackgroundColor::Light => Color::Magenta, + }), + TokenKind::Atom(AtomKind::Comment) => Some(match background { + BackgroundColor::Dark => Color::BrightCyan, + BackgroundColor::Light => Color::Cyan, + }), _ => None, }, background: None, @@ -212,7 +238,7 @@ pub fn color_positions(is_lhs: bool, positions: &[MatchedPos]) -> Vec<(SingleLin dimmed: false, }, MatchKind::Novel { highlight, .. } => Style { - foreground: Some(if is_lhs { Color::Red } else { Color::Green }), + foreground: Some(novel_color), background: None, bold: match highlight { TokenKind::Delimiter => true, @@ -223,13 +249,13 @@ pub fn color_positions(is_lhs: bool, positions: &[MatchedPos]) -> Vec<(SingleLin dimmed: false, }, MatchKind::ChangedCommentPart { .. } => Style { - foreground: Some(if is_lhs { Color::Red } else { Color::Green }), + foreground: Some(novel_color), background: None, bold: true, dimmed: false, }, MatchKind::UnchangedCommentPart { .. } => Style { - foreground: Some(if is_lhs { Color::Red } else { Color::Green }), + foreground: Some(novel_color), background: None, bold: false, dimmed: false, @@ -240,15 +266,30 @@ pub fn color_positions(is_lhs: bool, positions: &[MatchedPos]) -> Vec<(SingleLin styles } -pub fn apply_colors(s: &str, is_lhs: bool, positions: &[MatchedPos]) -> String { - let styles = color_positions(is_lhs, positions); +pub fn apply_colors( + s: &str, + is_lhs: bool, + background: BackgroundColor, + positions: &[MatchedPos], +) -> String { + let styles = color_positions(is_lhs, background, positions); apply(s, &styles) } -pub fn header(file_name: &str, hunk_num: usize, hunk_total: usize, language_name: &str) -> String { +pub fn header( + file_name: &str, + hunk_num: usize, + hunk_total: usize, + language_name: &str, + background: BackgroundColor, +) -> String { format!( "{} --- {}/{} --- {}", - file_name.yellow().bold(), + match background { + BackgroundColor::Dark => file_name.bright_yellow(), + BackgroundColor::Light => file_name.yellow(), + } + .bold(), hunk_num, hunk_total, language_name