use amberfork_align::{DiffParams, LexicalCost, diff};
use amberfork_ingest::{IngestError, Ingested};
use amberfork_model::Warning;
use clap::{Args, Parser, Subcommand};
use render::{RenderOpts, resolve_color_mode};
use std::io::IsTerminal;
use std::path::PathBuf;
use std::process::ExitCode;
mod render;
const EXIT_CONVERGED: u8 = 0;
const EXIT_FORKED: u8 = 1;
const EXIT_TROUBLE: u8 = 2;
const DEMO_GOOD: &str = include_str!("../assets/demo/good.json");
const DEMO_BAD: &str = include_str!("../assets/demo/bad.json");
const DEMO_HINT: &str =
" bundled sample pair · try your own runs: amberfork diff <bad> --against <good>";
#[derive(Parser)]
#[command(name = "amberfork", version, about)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Diff(DiffArgs),
Demo(DemoArgs),
}
#[derive(Args)]
struct DiffArgs {
#[arg(value_name = "BAD")]
bad: PathBuf,
#[arg(long, value_name = "GOOD")]
against: PathBuf,
#[command(flatten)]
output: OutputArgs,
}
#[derive(Args)]
struct DemoArgs {
#[command(flatten)]
output: OutputArgs,
}
#[derive(Args)]
struct OutputArgs {
#[arg(long)]
json: bool,
#[arg(long)]
no_color: bool,
}
fn main() -> ExitCode {
let cli = Cli::parse();
match cli.command {
Command::Diff(args) => run_diff(&args).unwrap_or_else(|err| {
eprintln!("amberfork: {err}");
ExitCode::from(EXIT_TROUBLE)
}),
Command::Demo(args) => run_demo(&args),
}
}
fn run_diff(args: &DiffArgs) -> Result<ExitCode, IngestError> {
let good = amberfork_ingest::load_file(&args.against)?;
let bad = amberfork_ingest::load_file(&args.bad)?;
Ok(diff_and_report(good, bad, &args.output, None))
}
fn run_demo(args: &DemoArgs) -> ExitCode {
let good = amberfork_ingest::from_json_str(DEMO_GOOD)
.expect("embedded demo trace good.json parses (locked by demo_cli tests)");
let bad = amberfork_ingest::from_json_str(DEMO_BAD)
.expect("embedded demo trace bad.json parses (locked by demo_cli tests)");
diff_and_report(good, bad, &args.output, Some(DEMO_HINT))
}
fn diff_and_report(
good: Ingested,
bad: Ingested,
output: &OutputArgs,
footer: Option<&str>,
) -> ExitCode {
let params = DiffParams::default()
.validated()
.expect("dev-calibrated defaults satisfy their own invariants");
let mut result = diff(&good.run, &bad.run, &LexicalCost, ¶ms);
result.warnings = merged_warnings(good.warnings, bad.warnings);
if output.json {
let json = serde_json::to_string_pretty(&result)
.expect("DiffResult serialization is infallible (no non-string map keys)");
println!("{json}");
} else {
let color = resolve_color_mode(
output.no_color,
std::io::stdout().is_terminal(),
std::env::var("NO_COLOR").ok().as_deref(),
std::env::var("TERM").ok().as_deref(),
std::env::var("COLORTERM").ok().as_deref(),
);
let width = terminal_size::terminal_size().map_or(100, |(w, _)| usize::from(w.0));
let opts = RenderOpts {
color,
width: width.max(60),
};
print!("{}", render::render(&result, &good.run, &bad.run, &opts));
if let Some(footer) = footer {
println!("{}", color.dim(footer));
}
for warning in &result.warnings {
eprintln!("amberfork: warning: {}", warning.msg);
}
}
let code = if result.fork.is_some() {
EXIT_FORKED
} else {
EXIT_CONVERGED
};
ExitCode::from(code)
}
fn merged_warnings(reference: Vec<Warning>, observed: Vec<Warning>) -> Vec<Warning> {
fn tag(side: char, warnings: Vec<Warning>) -> impl Iterator<Item = Warning> {
warnings.into_iter().map(move |w| Warning {
code: w.code,
msg: format!("run {side}: {}", w.msg),
})
}
tag('a', reference).chain(tag('b', observed)).collect()
}