devclean-cli 0.5.0

Audit and safely remove rebuildable development artifacts
Documentation
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Instant;

use anyhow::{Context, Result};
use devclean::{ScanOptions, human_bytes, load_config, parse_age, parse_bytes, scan};
use notify::{RecursiveMode, Watcher};

use super::{WatchArgs, record_scan_history, scan_options, select_categories};

pub(super) fn run(arguments: &WatchArgs) -> Result<()> {
    let config = load_config(arguments.shared.config.as_deref())?;
    let categories = select_categories(&arguments.shared, true, false);
    let options = scan_options(&arguments.shared, &config, categories, false)?;
    let threshold = parse_bytes(
        arguments
            .threshold
            .as_deref()
            .unwrap_or(&config.watch.threshold),
    )?;
    let interval = parse_age(
        arguments
            .interval
            .as_deref()
            .unwrap_or(&config.watch.interval),
    )?;

    evaluate(&options, threshold, !arguments.no_notify)?;
    if arguments.once {
        return Ok(());
    }

    let (sender, receiver) = mpsc::channel();
    let mut watcher = notify::recommended_watcher(sender)?;
    for root in &options.roots {
        watcher.watch(root, RecursiveMode::Recursive)?;
    }
    println!(
        "watching {} roots; next scan occurs on a filesystem event, at most once per {}",
        options.roots.len(),
        humantime::format_duration(interval)
    );

    let mut last_scan: Option<Instant> = None;
    loop {
        receiver
            .recv()
            .context("filesystem watcher channel closed")?
            .context("filesystem watcher failed")?;
        if last_scan.is_some_and(|last| last.elapsed() < interval) {
            continue;
        }
        evaluate(&options, threshold, !arguments.no_notify)?;
        last_scan = Some(Instant::now());
    }
}

fn evaluate(options: &ScanOptions, threshold: u64, notify: bool) -> Result<()> {
    let report = scan(options)?;
    record_scan_history(&report);
    println!(
        "watch scan: {} candidates, {} reclaimable (threshold {})",
        report.candidates.len(),
        human_bytes(report.total_bytes),
        human_bytes(threshold)
    );
    if report.total_bytes >= threshold {
        println!("watch threshold reached; review with `devclean scan` before cleaning");
        if notify {
            send_desktop_notification(report.total_bytes);
        }
    }
    Ok(())
}

fn send_desktop_notification(bytes: u64) {
    let message = format!(
        "{} of rebuildable artifacts are ready for review. No cleanup was run.",
        human_bytes(bytes)
    );
    #[cfg(target_os = "macos")]
    let command = Command::new("osascript")
        .args([
            "-e",
            &format!("display notification \"{message}\" with title \"DevCleaner\""),
        ])
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status();

    #[cfg(target_os = "linux")]
    let command = Command::new("notify-send")
        .args(["DevCleaner", &message])
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status();

    #[cfg(any(target_os = "macos", target_os = "linux"))]
    let _ = command;
    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
    let _ = message;
}