cockup 0.2.3

Yet another backup tool for various configurations
use crate::report;
use serde_json::Value;
use std::{
    collections::BTreeMap,
    process::Command,
    sync::{
        Mutex,
        atomic::{AtomicUsize, Ordering},
    },
};
fn brew(args: &[&str]) -> Result<String, String> {
    let output = Command::new("brew")
        .args(args)
        .env("HOMEBREW_NO_AUTO_UPDATE", "1")
        .output()
        .map_err(|e| format!("Homebrew is not accessible: {e}"))?;
    if !output.status.success() {
        return Err(format!(
            "Homebrew {} failed: {}",
            args.join(" "),
            String::from_utf8_lossy(&output.stderr).trim()
        ));
    }
    String::from_utf8(output.stdout).map_err(|e| e.to_string())
}
fn cask(name: &str) -> Result<Vec<String>, String> {
    let data: Value = serde_json::from_str(&brew(&["info", "--json=v2", "--cask", name])?)
        .map_err(|e| format!("Invalid Homebrew JSON: {e}"))?;
    let record = data["casks"]
        .as_array()
        .and_then(|c| c.first())
        .ok_or("Cask not found")?;
    let artifacts = record["artifacts"]
        .as_array()
        .ok_or("Cask artifacts missing")?;
    let mut paths = Vec::new();
    for artifact in artifacts {
        if let Some(zap) = artifact.get("zap").and_then(Value::as_array) {
            for item in zap {
                for key in ["rmdir", "trash"] {
                    match item.get(key) {
                        Some(Value::String(path)) => paths.push(path.clone()),
                        Some(Value::Array(items)) => {
                            for path in items {
                                if let Some(path) = path.as_str() {
                                    paths.push(path.to_string());
                                }
                            }
                        }
                        _ => {}
                    }
                }
            }
        }
    }
    Ok(paths)
}
pub fn detect(mut names: Vec<String>) -> Result<(), String> {
    brew(&["--version"])?;
    if names.is_empty() {
        names = brew(&["list", "--casks"])?
            .lines()
            .filter(|s| !s.is_empty())
            .map(str::to_string)
            .collect();
    }
    let count = std::thread::available_parallelism()
        .map_or(1, usize::from)
        .min(32)
        .min(names.len());
    let next = AtomicUsize::new(0);
    let results = Mutex::new(BTreeMap::new());
    std::thread::scope(|scope| {
        for _ in 0..count {
            scope.spawn(|| {
                loop {
                    let i = next.fetch_add(1, Ordering::Relaxed);
                    let Some(name) = names.get(i) else {
                        break;
                    };
                    let result = cask(name);
                    results.lock().unwrap().insert(name.clone(), result);
                }
            });
        }
    });
    let mut failed = 0;
    let mut found = false;
    for (name, result) in results.into_inner().unwrap() {
        match result {
            Ok(paths) if !paths.is_empty() => {
                found = true;
                report::app_name(&name);
                for path in paths {
                    println!("  {path}");
                }
            }
            Err(e) => {
                report::error(&format!("Cask `{name}`: {e}"));
                failed += 1;
            }
            _ => {}
        }
    }
    if failed > 0 {
        return Err(format!(
            "Homebrew discovery completed with {failed} failures."
        ));
    }
    if !found {
        println!("No potential configs found.");
    }
    Ok(())
}