netsky 0.1.5

netsky CLI: the viable system launcher and subcommand dispatcher
//! `netsky handoffs [which]` — inspect the handoff archive.

use std::fs;
use std::path::PathBuf;

use netsky_core::paths::handoff_archive_dir;

pub fn run(which: &str, limit: usize) -> netsky_core::Result<()> {
    let dir = handoff_archive_dir();
    if !dir.exists() {
        println!(
            "archive not present: {} (no restart since the archive-feature shipped)",
            dir.display()
        );
        return Ok(());
    }

    let files = newest_first_jsons(&dir)?;
    if files.is_empty() {
        println!("(archive empty)");
        return Ok(());
    }

    match which {
        "list" => list(&files, limit),
        "last" => dump(&files[0]),
        s if s.chars().all(|c| c.is_ascii_digit()) => {
            let idx: usize = s.parse()?;
            if idx == 0 || idx > files.len() {
                netsky_core::bail!("index {idx} out of range (have {} entries)", files.len());
            }
            dump(&files[idx - 1])
        }
        other => netsky_core::bail!("unknown selector '{other}' (use: list | last | <N>)"),
    }
}

fn newest_first_jsons(dir: &PathBuf) -> netsky_core::Result<Vec<PathBuf>> {
    let mut entries: Vec<(std::time::SystemTime, PathBuf)> = Vec::new();
    for e in fs::read_dir(dir)? {
        let e = e?;
        let p = e.path();
        if p.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        let mtime = e.metadata()?.modified()?;
        entries.push((mtime, p));
    }
    entries.sort_by(|a, b| b.0.cmp(&a.0));
    Ok(entries.into_iter().map(|(_, p)| p).collect())
}

fn list(files: &[PathBuf], limit: usize) -> netsky_core::Result<()> {
    for f in files.iter().take(limit) {
        let md = fs::metadata(f)?;
        let ts: chrono::DateTime<chrono::Utc> = md.modified()?.into();
        let size = md.len();
        let preview = preview_of(f).unwrap_or_else(|| "(preview unavailable)".to_string());
        println!(
            "  {}  {:>5}B  {preview}",
            ts.format("%Y-%m-%dT%H:%M:%SZ"),
            size
        );
    }
    Ok(())
}

fn dump(path: &PathBuf) -> netsky_core::Result<()> {
    let content = fs::read_to_string(path)?;
    match serde_json::from_str::<serde_json::Value>(&content) {
        Ok(v) => println!("{}", serde_json::to_string_pretty(&v)?),
        Err(_) => println!("{content}"),
    }
    Ok(())
}

fn preview_of(path: &PathBuf) -> Option<String> {
    let content = fs::read_to_string(path).ok()?;
    let v: serde_json::Value = serde_json::from_str(&content).ok()?;
    let text = v.get("text")?.as_str()?;
    let line: String = text.chars().take(70).collect::<String>().replace('\n', " ");
    Some(line)
}