fmtview 0.1.0

Fast terminal formatter and viewer for JSON, JSONL, XML, and formatted diffs
use std::{
    io::{Read, Write},
    process::{Command, Stdio},
};

use anyhow::{Context, Result, bail};
use tempfile::NamedTempFile;

use crate::{
    format::{self, FormatOptions},
    input::InputSource,
};

pub fn diff_sources(
    left: &InputSource,
    right: &InputSource,
    options: &FormatOptions,
) -> Result<NamedTempFile> {
    let left_formatted = format::format_source_to_temp(left, options)
        .with_context(|| format!("failed to format left input {}", left.label()))?;
    let right_formatted = format::format_source_to_temp(right, options)
        .with_context(|| format!("failed to format right input {}", right.label()))?;

    let mut output = NamedTempFile::new().context("failed to create diff temp file")?;
    match run_external_diff(left, right, &left_formatted, &right_formatted, &mut output) {
        Ok(()) => {}
        Err(error) => {
            writeln!(
                output,
                "external diff failed ({error:#}); falling back to streaming line comparison"
            )
            .context("failed to write diff fallback header")?;
            streaming_diff(left, right, &left_formatted, &right_formatted, &mut output)?;
        }
    }
    output.flush().context("failed to flush diff temp file")?;
    Ok(output)
}

fn run_external_diff(
    left: &InputSource,
    right: &InputSource,
    left_formatted: &NamedTempFile,
    right_formatted: &NamedTempFile,
    output: &mut NamedTempFile,
) -> Result<()> {
    let mut child = Command::new("diff")
        .arg("-u")
        .arg("--label")
        .arg(left.label())
        .arg("--label")
        .arg(right.label())
        .arg(left_formatted.path())
        .arg(right_formatted.path())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .context("failed to start diff")?;

    let mut stdout = child
        .stdout
        .take()
        .context("failed to capture diff stdout")?;
    std::io::copy(&mut stdout, output).context("failed to copy diff output")?;

    let result = child
        .wait_with_output()
        .context("failed to wait for diff")?;
    match result.status.code() {
        Some(0) => {
            writeln!(output, "No differences").context("failed to write empty diff message")?;
            Ok(())
        }
        Some(1) => Ok(()),
        _ => {
            let stderr = String::from_utf8_lossy(&result.stderr);
            bail!("diff exited with {}: {}", result.status, stderr.trim())
        }
    }
}

fn streaming_diff(
    left: &InputSource,
    right: &InputSource,
    left_formatted: &NamedTempFile,
    right_formatted: &NamedTempFile,
    output: &mut NamedTempFile,
) -> Result<()> {
    let mut left_reader = std::io::BufReader::new(std::fs::File::open(left_formatted.path())?);
    let mut right_reader = std::io::BufReader::new(std::fs::File::open(right_formatted.path())?);
    let mut left_line = String::new();
    let mut right_line = String::new();
    let mut line_number = 1_usize;
    let mut differences = 0_usize;

    writeln!(output, "--- {}", left.label()).context("failed to write diff header")?;
    writeln!(output, "+++ {}", right.label()).context("failed to write diff header")?;

    loop {
        left_line.clear();
        right_line.clear();
        let left_read = read_line(&mut left_reader, &mut left_line)?;
        let right_read = read_line(&mut right_reader, &mut right_line)?;
        if left_read == 0 && right_read == 0 {
            break;
        }

        if left_line != right_line {
            differences += 1;
            writeln!(output, "@@ line {} @@", line_number).context("failed to write diff hunk")?;
            if left_read != 0 {
                write!(output, "-{}", left_line).context("failed to write left diff line")?;
            }
            if right_read != 0 {
                write!(output, "+{}", right_line).context("failed to write right diff line")?;
            }
        }
        line_number += 1;
    }

    if differences == 0 {
        writeln!(output, "No differences").context("failed to write empty diff message")?;
    }

    Ok(())
}

fn read_line<R: Read>(reader: &mut std::io::BufReader<R>, line: &mut String) -> Result<usize> {
    use std::io::BufRead;

    reader.read_line(line).context("failed to read diff line")
}