bashers 0.8.8

Installable cli helpers
Documentation
use anyhow::{Context, Result};
use std::process::Command;
use std::time::Duration;

use crate::utils::colors::Colors;
use diff;

pub fn run(command: &[String], interval_secs: u64, no_diff: bool) -> Result<()> {
    if command.is_empty() {
        anyhow::bail!("command cannot be empty");
    }
    let program = &command[0];
    let args = &command[1..];

    ctrlc::set_handler(move || std::process::exit(0)).context("setting Ctrl+C handler")?;

    let mut colors = Colors::new();
    let mut previous: Option<String> = None;

    loop {
        let output = run_cmd(program, args)?;
        clear_screen();
        let show_diff = !no_diff && previous.is_some();
        print_header(interval_secs, command, &mut colors, show_diff)?;

        if no_diff {
            let _ = colors.reset();
            let _ = colors.println(&output);
        } else if let Some(ref prev) = previous {
            print_diff(prev, &output, &mut colors)?;
        } else {
            let _ = colors.reset();
            let _ = colors.println(&output);
        }
        previous = Some(output);

        let _ = colors.reset();
        let _ = colors.flush();
        std::thread::sleep(Duration::from_secs(interval_secs));
    }
}

fn run_cmd(program: &str, args: &[String]) -> Result<String> {
    let out = Command::new(program)
        .args(args)
        .output()
        .with_context(|| format!("running {} {}", program, args.join(" ")))?;
    let mut s = String::from_utf8_lossy(&out.stdout).into_owned();
    if !out.stderr.is_empty() {
        s.push_str(&String::from_utf8_lossy(&out.stderr));
    }
    if s.ends_with('\n') {
        s.pop();
    }
    Ok(s)
}

fn clear_screen() {
    print!("\x1b[2J\x1b[H");
}

fn print_header(
    interval_secs: u64,
    command: &[String],
    colors: &mut Colors,
    show_diff_hint: bool,
) -> std::io::Result<()> {
    let _ = colors.cyan();
    let _ = colors.bold();
    let _ = colors.print(&format!("Every {}s: ", interval_secs));
    let _ = colors.reset();
    let _ = colors.println(&command.join(" "));
    if show_diff_hint {
        let _ = colors.green();
        let _ = colors.print("green");
        let _ = colors.reset();
        let _ = colors.println(" = changed since last run");
    }
    let _ = colors.println("");
    Ok(())
}

fn print_diff(prev: &str, curr: &str, colors: &mut Colors) -> std::io::Result<()> {
    let results = diff::lines(prev, curr);
    let mut pending_lefts: Vec<&str> = Vec::new();
    for r in results {
        match r {
            diff::Result::Left(line) => {
                pending_lefts.push(line);
            }
            diff::Result::Both(prev_line, curr_line) => {
                print_line_char_diff(prev_line, curr_line, colors)?;
            }
            diff::Result::Right(curr_line) => {
                if let Some(prev_line) = pending_lefts.pop() {
                    print_line_char_diff(prev_line, curr_line, colors)?;
                } else {
                    let _ = colors.green();
                    let _ = colors.println(curr_line);
                }
            }
        }
    }
    Ok(())
}

fn print_line_char_diff(
    prev_line: &str,
    curr_line: &str,
    colors: &mut Colors,
) -> std::io::Result<()> {
    let mut normal = String::new();
    let mut green = String::new();
    for r in diff::chars(prev_line, curr_line) {
        match r {
            diff::Result::Left(_) => {}
            diff::Result::Both(c, _) => {
                if !green.is_empty() {
                    let _ = colors.green();
                    let _ = colors.print(&green);
                    let _ = colors.reset();
                    green.clear();
                }
                normal.push(c);
            }
            diff::Result::Right(c) => {
                if !normal.is_empty() {
                    let _ = colors.reset();
                    let _ = colors.print(&normal);
                    normal.clear();
                }
                green.push(c);
            }
        }
    }
    let _ = colors.reset();
    let _ = colors.print(&normal);
    let _ = colors.green();
    let _ = colors.print(&green);
    let _ = colors.reset();
    let _ = colors.println("");
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_run_empty_command_errors() {
        let err = run(&[], 1, false).unwrap_err();
        assert!(err.to_string().contains("empty"));
    }
}