fur_cli/commands/
sweep.rs

1use std::fs;
2use std::path::PathBuf;
3use chrono::{DateTime, Local};
4use serde_json::{Value, json};
5use clap::Parser;
6use walkdir::WalkDir;
7use dirs;
8use colored::*;
9
10/// Arguments for `fur sweep`
11#[derive(Parser)]
12pub struct SweepArgs {
13    /// Directory to start scan (default: home)
14    #[arg(long)]
15    pub dir: Option<String>,
16
17    /// Maximum recursion depth
18    #[arg(long, default_value_t = 5)]
19    pub depth: usize,
20
21    /// Output as JSON
22    #[arg(long)]
23    pub json: bool,
24
25    /// Suppress warnings
26    #[arg(long)]
27    pub silent: bool,
28}
29
30pub fn run_sweep(args: SweepArgs) {
31    let start_dir = args.dir
32        .clone()
33        .map(PathBuf::from)
34        .unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")));
35
36    let mut results = Vec::new();
37
38    for entry in WalkDir::new(&start_dir)
39        .follow_links(false)
40        .max_depth(args.depth)
41        .into_iter()
42        .filter_map(|e| e.ok())
43    {
44        let path = entry.path();
45        if path.is_dir() && path.ends_with(".fur") {
46            let index_path = path.join("index.json");
47            if index_path.exists() {
48                if let Ok(content) = fs::read_to_string(&index_path) {
49                    if let Ok(json) = serde_json::from_str::<Value>(&content) {
50                        let threads_vec = json["threads"]
51                            .as_array()
52                            .cloned()
53                            .unwrap_or_else(|| Vec::new());
54                        let active = json["active_thread"].as_str().unwrap_or("-");
55                        let msg_count = threads_vec.iter().filter_map(|tid| {
56                            tid.as_str().map(|t| {
57                                let tp = path.join("threads").join(format!("{}.json", t));
58                                if let Ok(tc) = fs::read_to_string(tp) {
59                                    serde_json::from_str::<Value>(&tc)
60                                        .ok()
61                                        .and_then(|v| v["messages"].as_array().map(|a| a.len()))
62                                        .unwrap_or(0)
63                                } else { 0 }
64                            })
65                        }).sum::<usize>();
66
67                        let modified = fs::metadata(&index_path)
68                            .and_then(|m| m.modified())
69                            .ok()
70                            .map(|t| {
71                                let dt: DateTime<Local> = DateTime::from(t);
72                                dt.format("%Y-%m-%d %H:%M").to_string()
73                            })
74                            .unwrap_or("-".into());
75
76                        results.push(json!({
77                            "path": path.parent().unwrap_or(path).display().to_string(),
78                            "active_thread": active,
79                            "threads": threads_vec.len(),
80                            "messages": msg_count,
81                            "modified": modified
82                        }));
83                    }
84                }
85            }
86        }
87    }
88
89    let mut total_projects = 0;
90    let mut total_threads = 0;
91    let mut total_messages = 0;
92
93    if args.json {
94        println!("{}", serde_json::to_string_pretty(&results).unwrap());
95    } else {
96        println!("{}", "=== 🧭 FUR Project Sweep ===\n".bold().bright_cyan());
97        for r in &results {
98            total_projects += 1;
99            total_threads += r["threads"].as_u64().unwrap_or(0);
100            total_messages += r["messages"].as_u64().unwrap_or(0);
101
102            let path = r["path"].as_str().unwrap_or("-").bright_yellow().bold();
103            let active = r["active_thread"].as_str().unwrap_or("-").bright_cyan();
104            let threads = r["threads"].as_u64().unwrap_or(0).to_string().green();
105            let messages = r["messages"].as_u64().unwrap_or(0).to_string().blue();
106            let modified = r["modified"].as_str().unwrap_or("-").bright_black();
107
108            println!(
109                "{}\n  {} {}\n  {} {}\n  {} {}\n  {} {}\n",
110                path,
111                "Active Thread :".dimmed(),
112                active,
113                "Threads       :".dimmed(),
114                threads,
115                "Messages      :".dimmed(),
116                messages,
117                "Last Modified :".dimmed(),
118                modified,
119            );
120        }
121
122        // Summary footer
123        println!("{}", "─".repeat(50).dimmed());
124        println!(
125            "🌍  Found {} {} | {} {} | {} {}\n",
126            total_projects.to_string().bold().bright_yellow(),
127            if total_projects == 1 { "project" } else { "projects" },
128            total_threads.to_string().bold().green(),
129            if total_threads == 1 { "thread" } else { "threads" },
130            total_messages.to_string().bold().blue(),
131            if total_messages == 1 { "message" } else { "messages" },
132        );
133    }
134}
135
136