use std::fs;
use std::io;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use clap::Parser;
use presentar_terminal::direct::CellBuffer;
use presentar_terminal::tools::{compare_tui, generate_report, TuiComparisonConfig};
#[derive(Parser)]
#[command(name = "tui-compare", version, about = "TUI pixel comparison using CIEDE2000 (ΔE00), CLD, and SSIM metrics", long_about = None)]
struct Cli {
#[arg(short, long)]
reference: String,
#[arg(short, long)]
target: String,
#[arg(long, default_value = "120x40")]
size: String,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long, default_value = "0.01")]
threshold_cld: f64,
#[arg(long, default_value = "2.0")]
threshold_delta_e: f64,
#[arg(long, default_value = "0.95")]
threshold_ssim: f64,
#[arg(long)]
diff_output: Option<PathBuf>,
#[arg(short, long)]
quiet: bool,
}
fn main() -> io::Result<()> {
let cli = Cli::parse();
let (width, height) = parse_size(&cli.size)?;
let reference_buffer = load_or_capture(&cli.reference, width, height)?;
let target_buffer = load_or_capture(&cli.target, width, height)?;
let config = TuiComparisonConfig {
cld_threshold: cli.threshold_cld,
delta_e_threshold: cli.threshold_delta_e,
ssim_threshold: cli.threshold_ssim,
..Default::default()
};
let result = compare_tui(&reference_buffer, &target_buffer, &config);
let report = generate_report(&result, &config);
if let Some(output_path) = &cli.output {
fs::write(output_path, &report)?;
}
if !cli.quiet {
print!("{report}");
}
if let Some(diff_path) = &cli.diff_output {
let diff_text = generate_diff_text(&reference_buffer, &target_buffer, &result);
fs::write(diff_path, diff_text)?;
}
if result.passed {
if !cli.quiet {
eprintln!("\n✓ Comparison PASSED");
}
Ok(())
} else {
if !cli.quiet {
eprintln!("\n✗ Comparison FAILED");
}
std::process::exit(1);
}
}
fn parse_size(size: &str) -> io::Result<(u16, u16)> {
let parts: Vec<&str> = size.split('x').collect();
if parts.len() != 2 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Size must be in WIDTHxHEIGHT format",
));
}
let width: u16 = parts[0]
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid width"))?;
let height: u16 = parts[1]
.parse()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid height"))?;
Ok((width, height))
}
fn load_or_capture(source: &str, width: u16, height: u16) -> io::Result<CellBuffer> {
let path = PathBuf::from(source);
if path.exists() {
load_ansi_file(&path, width, height)
} else if source.contains(' ') || source.starts_with("./") {
capture_command(source, width, height)
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("File not found: {source}"),
))
}
}
fn load_ansi_file(path: &PathBuf, width: u16, height: u16) -> io::Result<CellBuffer> {
let content = fs::read_to_string(path)?;
parse_ansi_to_buffer(&content, width, height)
}
fn capture_command(cmd: &str, width: u16, height: u16) -> io::Result<CellBuffer> {
let output = Command::new("sh")
.arg("-c")
.arg(cmd)
.env("COLUMNS", width.to_string())
.env("LINES", height.to_string())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()?;
let content = String::from_utf8_lossy(&output.stdout);
parse_ansi_to_buffer(&content, width, height)
}
fn skip_ansi_escape(chars: &mut std::iter::Peekable<std::str::Chars>) {
if chars.peek() == Some(&'[') {
chars.next(); while let Some(&c) = chars.peek() {
chars.next();
if c.is_ascii_alphabetic() {
break;
}
}
}
}
fn parse_ansi_to_buffer(content: &str, width: u16, height: u16) -> io::Result<CellBuffer> {
let mut buffer = CellBuffer::new(width, height);
let mut x = 0u16;
let mut y = 0u16;
let mut chars = content.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'\x1b' => skip_ansi_escape(&mut chars),
'\n' => {
x = 0;
y = y.saturating_add(1);
}
'\r' => x = 0,
_ => {
if x < width && y < height {
buffer.set_char(x, y, ch);
x = x.saturating_add(1);
}
}
}
}
Ok(buffer)
}
fn generate_diff_text(
reference: &CellBuffer,
target: &CellBuffer,
result: &presentar_terminal::tools::TuiComparisonResult,
) -> String {
let mut output = String::new();
output.push_str("=== DIFF VISUALIZATION ===\n\n");
if result.diff_cells.is_empty() {
output.push_str("No differences found.\n");
return output;
}
output.push_str(&format!(
"Found {} differing cells:\n\n",
result.diff_cells.len()
));
let mut by_row: std::collections::HashMap<u16, Vec<_>> = std::collections::HashMap::new();
for cell in &result.diff_cells {
by_row.entry(cell.y).or_default().push(cell);
}
let mut rows: Vec<_> = by_row.keys().copied().collect();
rows.sort_unstable();
for row in rows.iter().take(20) {
output.push_str(&format!("Row {row}:\n"));
output.push_str(" REF: ");
for x in 0..reference.width().min(80) {
let ch = reference
.get(x, *row)
.and_then(|c| c.symbol.chars().next())
.unwrap_or(' ');
output.push(ch);
}
output.push('\n');
output.push_str(" TGT: ");
for x in 0..target.width().min(80) {
let ch = target
.get(x, *row)
.and_then(|c| c.symbol.chars().next())
.unwrap_or(' ');
output.push(ch);
}
output.push('\n');
output.push_str(" DIF: ");
let row_diffs = by_row.get(row).unwrap_or(&Vec::new());
for x in 0..reference.width().min(80) {
if row_diffs.iter().any(|d| d.x == x) {
output.push('^');
} else {
output.push(' ');
}
}
output.push_str("\n\n");
}
if rows.len() > 20 {
output.push_str(&format!(
"... and {} more rows with differences\n",
rows.len() - 20
));
}
output
}