Add a --background option

Improves #55
html_output
Wilfred Hughes 2022-01-29 15:58:51 +07:00
parent 0abc839481
commit c38b072fd2
5 changed files with 168 additions and 35 deletions

@ -23,6 +23,9 @@ more discoverable.
Added a `--color` option which allows explicitly enabling/disabling Added a `--color` option which allows explicitly enabling/disabling
colour output. colour output.
Added a `--background` option which controls whether difftastic uses
bright or dark colours.
## 0.17 (released 25 January 2022) ## 0.17 (released 25 January 2022)
### Diffing ### Diffing

@ -4,7 +4,7 @@ use crate::{
context::{calculate_after_context, calculate_before_context, opposite_positions}, context::{calculate_after_context, calculate_before_context, opposite_positions},
hunks::Hunk, hunks::Hunk,
lines::{format_line_num, MaxLine}, lines::{format_line_num, MaxLine},
style::{self, apply_colors}, style::{self, apply_colors, BackgroundColor},
syntax::MatchedPos, syntax::MatchedPos,
}; };
use colored::*; use colored::*;
@ -17,9 +17,10 @@ pub fn display(
hunks: &[Hunk], hunks: &[Hunk],
display_path: &str, display_path: &str,
lang_name: &str, lang_name: &str,
background: BackgroundColor,
) -> String { ) -> String {
let lhs_colored = apply_colors(lhs_src, true, lhs_positions); let lhs_colored = apply_colors(lhs_src, true, background, lhs_positions);
let rhs_colored = apply_colors(rhs_src, false, rhs_positions); let rhs_colored = apply_colors(rhs_src, false, background, rhs_positions);
let lhs_lines: Vec<_> = lhs_colored.lines().collect(); let lhs_lines: Vec<_> = lhs_colored.lines().collect();
let rhs_lines: Vec<_> = rhs_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); let opposite_to_rhs = opposite_positions(rhs_positions);
for (i, hunk) in hunks.iter().enumerate() { 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'); res.push('\n');
let hunk_lines = hunk.lines.clone(); let hunk_lines = hunk.lines.clone();

@ -47,6 +47,7 @@ use atty::Stream;
use clap::{crate_authors, crate_description, crate_version, App, AppSettings, Arg}; use clap::{crate_authors, crate_description, crate_version, App, AppSettings, Arg};
use sliders::fix_all_sliders; use sliders::fix_all_sliders;
use std::{env, path::Path}; use std::{env, path::Path};
use style::BackgroundColor;
use summary::DiffResult; use summary::DiffResult;
use syntax::init_next_prev; use syntax::init_next_prev;
use typed_arena::Arena; use typed_arena::Arena;
@ -93,6 +94,7 @@ enum ColorOutput {
enum Mode { enum Mode {
Diff { Diff {
background_color: BackgroundColor,
color_output: ColorOutput, color_output: ColorOutput,
display_width: usize, display_width: usize,
display_path: String, display_path: String,
@ -145,6 +147,12 @@ fn app() -> clap::App<'static> {
.value_name("WHEN") .value_name("WHEN")
.help("When to use color output.") .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(
Arg::new("paths") Arg::new("paths")
.multiple_values(true) .multiple_values(true)
@ -230,7 +238,18 @@ fn parse_args() -> Mode {
ColorOutput::Auto 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 { Mode::Diff {
background_color,
color_output, color_output,
display_width, display_width,
display_path, display_path,
@ -292,6 +311,7 @@ fn main() {
} }
} }
Mode::Diff { Mode::Diff {
background_color,
color_output, color_output,
display_width, display_width,
display_path, display_path,
@ -306,11 +326,11 @@ fn main() {
if lhs_path.is_dir() && rhs_path.is_dir() { if lhs_path.is_dir() && rhs_path.is_dir() {
for diff_result in diff_directories(lhs_path, rhs_path) { 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 { } else {
let diff_result = diff_file(&display_path, lhs_path, rhs_path); 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<DiffResult> {
res res
} }
fn print_diff_result(display_width: usize, summary: &DiffResult) { fn print_diff_result(display_width: usize, background: BackgroundColor, summary: &DiffResult) {
if summary.binary { if summary.binary {
println!("{}", style::header(&summary.path, 1, 1, "binary")); println!(
"{}",
style::header(&summary.path, 1, 1, "binary", background)
);
return; 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()); let lang_name = summary.language.clone().unwrap_or_else(|| "text".into());
if hunks.is_empty() { 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" { if lang_name == "text" {
println!("No changes.\n"); println!("No changes.\n");
} else { } else {
@ -472,6 +498,7 @@ fn print_diff_result(display_width: usize, summary: &DiffResult) {
&hunks, &hunks,
&summary.path, &summary.path,
&lang_name, &lang_name,
background
) )
); );
} else { } else {
@ -480,6 +507,7 @@ fn print_diff_result(display_width: usize, summary: &DiffResult) {
side_by_side::display_hunks( side_by_side::display_hunks(
&hunks, &hunks,
display_width, display_width,
background,
&summary.path, &summary.path,
&lang_name, &lang_name,
&summary.lhs_src, &summary.lhs_src,

@ -11,7 +11,7 @@ use crate::{
hunks::{matched_lines_for_hunk, Hunk}, hunks::{matched_lines_for_hunk, Hunk},
lines::{codepoint_len, format_line_num, LineNumber}, lines::{codepoint_len, format_line_num, LineNumber},
positions::SingleLineSpan, 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}, 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). /// 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 column_width = format_line_num(src.lines().count().into()).len();
let mut result = String::with_capacity(src.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'); result.push('\n');
for (i, line) in src.lines().enumerate() { for (i, line) in src.lines().enumerate() {
@ -69,6 +75,7 @@ fn display_line_nums(
lhs_line_num: Option<LineNumber>, lhs_line_num: Option<LineNumber>,
rhs_line_num: Option<LineNumber>, rhs_line_num: Option<LineNumber>,
widths: &Widths, widths: &Widths,
background: BackgroundColor,
lhs_has_novel: bool, lhs_has_novel: bool,
rhs_has_novel: bool, rhs_has_novel: bool,
prev_lhs_line_num: Option<LineNumber>, prev_lhs_line_num: Option<LineNumber>,
@ -78,7 +85,12 @@ fn display_line_nums(
Some(line_num) => { Some(line_num) => {
let s = format_line_num_padded(line_num, widths.lhs_line_nums); let s = format_line_num_padded(line_num, widths.lhs_line_nums);
if lhs_has_novel { 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 { } else {
s s
} }
@ -92,7 +104,11 @@ fn display_line_nums(
Some(line_num) => { Some(line_num) => {
let s = format_line_num_padded(line_num, widths.rhs_line_nums); let s = format_line_num_padded(line_num, widths.rhs_line_nums);
if rhs_has_novel { if rhs_has_novel {
s.green().bold().to_string() match background {
BackgroundColor::Dark => s.bright_green(),
BackgroundColor::Light => s.green(),
}
.to_string()
} else { } else {
s s
} }
@ -175,13 +191,14 @@ fn lines_with_novel(
/// Calculate positions of highlights on both sides. This includes /// Calculate positions of highlights on both sides. This includes
/// both syntax highlighting and added/removed content highlighting. /// both syntax highlighting and added/removed content highlighting.
fn highlight_positions( fn highlight_positions(
background: BackgroundColor,
lhs_mps: &[MatchedPos], lhs_mps: &[MatchedPos],
rhs_mps: &[MatchedPos], rhs_mps: &[MatchedPos],
) -> ( ) -> (
HashMap<LineNumber, Vec<(SingleLineSpan, Style)>>, HashMap<LineNumber, Vec<(SingleLineSpan, Style)>>,
HashMap<LineNumber, Vec<(SingleLineSpan, Style)>>, HashMap<LineNumber, Vec<(SingleLineSpan, Style)>>,
) { ) {
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. // Preallocate the hashmap assuming the average line will have 2 items on it.
let mut lhs_styles: HashMap<LineNumber, Vec<(SingleLineSpan, Style)>> = let mut lhs_styles: HashMap<LineNumber, Vec<(SingleLineSpan, Style)>> =
HashMap::with_capacity(lhs_positions.len() / 2); HashMap::with_capacity(lhs_positions.len() / 2);
@ -190,7 +207,7 @@ fn highlight_positions(
styles.push((span, style)); 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<LineNumber, Vec<(SingleLineSpan, Style)>> = let mut rhs_styles: HashMap<LineNumber, Vec<(SingleLineSpan, Style)>> =
HashMap::with_capacity(rhs_positions.len() / 2); HashMap::with_capacity(rhs_positions.len() / 2);
for (span, style) in rhs_positions { for (span, style) in rhs_positions {
@ -228,6 +245,7 @@ fn highlight_as_novel(
pub fn display_hunks( pub fn display_hunks(
hunks: &[Hunk], hunks: &[Hunk],
display_width: usize, display_width: usize,
background: BackgroundColor,
display_path: &str, display_path: &str,
lang_name: &str, lang_name: &str,
lhs_src: &str, lhs_src: &str,
@ -235,18 +253,32 @@ pub fn display_hunks(
lhs_mps: &[MatchedPos], lhs_mps: &[MatchedPos],
rhs_mps: &[MatchedPos], rhs_mps: &[MatchedPos],
) -> String { ) -> String {
let lhs_colored_src = apply_colors(lhs_src, true, lhs_mps); let lhs_colored_src = apply_colors(lhs_src, true, background, lhs_mps);
let rhs_colored_src = apply_colors(rhs_src, false, rhs_mps); let rhs_colored_src = apply_colors(rhs_src, false, background, rhs_mps);
if lhs_src.is_empty() { 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() { 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. // 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 lhs_lines = split_on_newlines(lhs_src);
let rhs_lines = split_on_newlines(rhs_src); let rhs_lines = split_on_newlines(rhs_src);
let lhs_colored_lines = split_on_newlines(&lhs_colored_src); let lhs_colored_lines = split_on_newlines(&lhs_colored_src);
@ -262,7 +294,13 @@ pub fn display_hunks(
let mut out_lines: Vec<String> = vec![]; let mut out_lines: Vec<String> = vec![];
for (i, hunk) in hunks.iter().enumerate() { 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 aligned_lines = matched_lines_for_hunk(&matched_lines, hunk);
let no_lhs_changes = hunk.lines.iter().all(|(l, _)| l.is_none()); let no_lhs_changes = hunk.lines.iter().all(|(l, _)| l.is_none());
@ -288,6 +326,7 @@ pub fn display_hunks(
lhs_line_num, lhs_line_num,
rhs_line_num, rhs_line_num,
&widths, &widths,
background,
lhs_line_novel, lhs_line_novel,
rhs_line_novel, rhs_line_novel,
prev_lhs_line_num, prev_lhs_line_num,
@ -365,7 +404,11 @@ pub fn display_hunks(
); );
if let Some(line_num) = lhs_line_num { if let Some(line_num) = lhs_line_num {
if lhs_lines_with_novel.contains(&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 s
@ -380,7 +423,11 @@ pub fn display_hunks(
); );
if let Some(line_num) = rhs_line_num { if let Some(line_num) = rhs_line_num {
if rhs_lines_with_novel.contains(&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 s
@ -430,7 +477,13 @@ mod tests {
#[test] #[test]
fn test_display_single_column() { fn test_display_single_column() {
// Basic smoke test. // 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); assert!(res.len() > 10);
} }
@ -493,6 +546,7 @@ mod tests {
display_hunks( display_hunks(
&hunks, &hunks,
80, 80,
BackgroundColor::Dark,
"foo.el", "foo.el",
"Emacs Lisp", "Emacs Lisp",
"foo", "foo",

@ -11,6 +11,12 @@ use std::{
collections::HashMap, collections::HashMap,
}; };
#[derive(Clone, Copy, Debug)]
pub enum BackgroundColor {
Dark,
Light,
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct Style { pub struct Style {
foreground: Option<Color>, foreground: Option<Color>,
@ -192,15 +198,35 @@ fn apply(s: &str, styles: &[(SingleLineSpan, Style)]) -> String {
res 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![]; let mut styles = vec![];
for pos in positions { for pos in positions {
let line_pos = pos.pos; let line_pos = pos.pos;
let style = match pos.kind { let style = match pos.kind {
MatchKind::Unchanged { highlight, .. } => Style { MatchKind::Unchanged { highlight, .. } => Style {
foreground: match highlight { foreground: match highlight {
TokenKind::Atom(AtomKind::String) => Some(Color::Magenta), TokenKind::Atom(AtomKind::String) => Some(match background {
TokenKind::Atom(AtomKind::Comment) => Some(Color::Cyan), BackgroundColor::Dark => Color::BrightMagenta,
BackgroundColor::Light => Color::Magenta,
}),
TokenKind::Atom(AtomKind::Comment) => Some(match background {
BackgroundColor::Dark => Color::BrightCyan,
BackgroundColor::Light => Color::Cyan,
}),
_ => None, _ => None,
}, },
background: None, background: None,
@ -212,7 +238,7 @@ pub fn color_positions(is_lhs: bool, positions: &[MatchedPos]) -> Vec<(SingleLin
dimmed: false, dimmed: false,
}, },
MatchKind::Novel { highlight, .. } => Style { MatchKind::Novel { highlight, .. } => Style {
foreground: Some(if is_lhs { Color::Red } else { Color::Green }), foreground: Some(novel_color),
background: None, background: None,
bold: match highlight { bold: match highlight {
TokenKind::Delimiter => true, TokenKind::Delimiter => true,
@ -223,13 +249,13 @@ pub fn color_positions(is_lhs: bool, positions: &[MatchedPos]) -> Vec<(SingleLin
dimmed: false, dimmed: false,
}, },
MatchKind::ChangedCommentPart { .. } => Style { MatchKind::ChangedCommentPart { .. } => Style {
foreground: Some(if is_lhs { Color::Red } else { Color::Green }), foreground: Some(novel_color),
background: None, background: None,
bold: true, bold: true,
dimmed: false, dimmed: false,
}, },
MatchKind::UnchangedCommentPart { .. } => Style { MatchKind::UnchangedCommentPart { .. } => Style {
foreground: Some(if is_lhs { Color::Red } else { Color::Green }), foreground: Some(novel_color),
background: None, background: None,
bold: false, bold: false,
dimmed: false, dimmed: false,
@ -240,15 +266,30 @@ pub fn color_positions(is_lhs: bool, positions: &[MatchedPos]) -> Vec<(SingleLin
styles styles
} }
pub fn apply_colors(s: &str, is_lhs: bool, positions: &[MatchedPos]) -> String { pub fn apply_colors(
let styles = color_positions(is_lhs, positions); s: &str,
is_lhs: bool,
background: BackgroundColor,
positions: &[MatchedPos],
) -> String {
let styles = color_positions(is_lhs, background, positions);
apply(s, &styles) 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!( format!(
"{} --- {}/{} --- {}", "{} --- {}/{} --- {}",
file_name.yellow().bold(), match background {
BackgroundColor::Dark => file_name.bright_yellow(),
BackgroundColor::Light => file_name.yellow(),
}
.bold(),
hunk_num, hunk_num,
hunk_total, hunk_total,
language_name language_name