use anstyle::{AnsiColor, Style};
use similar::{ChangeTag, TextDiff};
use std::io::{self, IsTerminal, Write};
use std::path::Path;
const CONTEXT: usize = 3;
pub struct Palette {
header: Style,
delete: Style,
insert: Style,
}
impl Palette {
pub fn plain() -> Palette {
Palette {
header: Style::new(),
delete: Style::new(),
insert: Style::new(),
}
}
pub fn coloured() -> Palette {
Palette {
header: Style::new().bold(),
delete: AnsiColor::Red.on_default(),
insert: AnsiColor::Green.on_default(),
}
}
pub fn for_stream(stream: &impl IsTerminal) -> Palette {
let set = |name: &str| std::env::var_os(name).filter(|value| !value.is_empty());
let forced = set("CLICOLOR_FORCE").is_some_and(|value| value != "0");
if set("NO_COLOR").is_none() && (forced || stream.is_terminal()) {
Palette::coloured()
} else {
Palette::plain()
}
}
}
pub fn write(
out: &mut impl Write,
path: &Path,
before: &str,
after: &str,
palette: &Palette,
) -> io::Result<()> {
let diff = TextDiff::from_lines(before, after);
for group in diff.grouped_ops(CONTEXT) {
let Some(first) = group.first() else { continue };
let line = first.old_range().start + 1;
let header = palette.header;
writeln!(
out,
"{header}Diff in {} at line {line}:{header:#}",
path.display()
)?;
for op in &group {
for change in diff.iter_changes(op) {
let (sign, style) = match change.tag() {
ChangeTag::Delete => ('-', palette.delete),
ChangeTag::Insert => ('+', palette.insert),
ChangeTag::Equal => (' ', Style::new()),
};
let text = change.value().trim_end_matches(['\n', '\r']);
writeln!(out, "{style}{sign}{text}{style:#}")?;
}
}
}
Ok(())
}