Skip to main content

fur_cli/commands/
sweep.rs

1use chrono::{DateTime, Local};
2use clap::Parser;
3use colored::*;
4use dirs;
5use serde_json::{json, Value};
6use std::fs;
7use std::path::PathBuf;
8use walkdir::WalkDir;
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
32        .dir
33        .clone()
34        .map(PathBuf::from)
35        .unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")));
36
37    let mut results = Vec::new();
38
39    for entry in WalkDir::new(&start_dir)
40        .follow_links(false)
41        .max_depth(args.depth)
42        .into_iter()
43        .filter_map(|e| e.ok())
44    {
45        let path = entry.path();
46        if path.is_dir() && path.ends_with(".fur") {
47            let index_path = path.join("index.json");
48            if index_path.exists() {
49                if let Ok(content) = fs::read_to_string(&index_path) {
50                    if let Ok(json) = serde_json::from_str::<Value>(&content) {
51                        let threads_vec = json["threads"]
52                            .as_array()
53                            .cloned()
54                            .unwrap_or_else(|| Vec::new());
55                        let active = json["active_thread"].as_str().unwrap_or("-");
56                        let msg_count = threads_vec
57                            .iter()
58                            .filter_map(|tid| {
59                                tid.as_str().map(|t| {
60                                    let tp = path.join("threads").join(format!("{}.json", t));
61                                    if let Ok(tc) = fs::read_to_string(tp) {
62                                        serde_json::from_str::<Value>(&tc)
63                                            .ok()
64                                            .and_then(|v| v["messages"].as_array().map(|a| a.len()))
65                                            .unwrap_or(0)
66                                    } else {
67                                        0
68                                    }
69                                })
70                            })
71                            .sum::<usize>();
72
73                        let modified = fs::metadata(&index_path)
74                            .and_then(|m| m.modified())
75                            .ok()
76                            .map(|t| {
77                                let dt: DateTime<Local> = DateTime::from(t);
78                                dt.format("%Y-%m-%d %H:%M").to_string()
79                            })
80                            .unwrap_or("-".into());
81
82                        results.push(json!({
83                            "path": path.parent().unwrap_or(path).display().to_string(),
84                            "active_thread": active,
85                            "threads": threads_vec.len(),
86                            "messages": msg_count,
87                            "modified": modified
88                        }));
89                    }
90                }
91            }
92        }
93    }
94
95    let mut total_projects = 0;
96    let mut total_threads = 0;
97    let mut total_messages = 0;
98
99    if args.json {
100        println!("{}", serde_json::to_string_pretty(&results).unwrap());
101    } else {
102        println!("{}", "=== 🧭 FUR Project Sweep ===\n".bold().bright_cyan());
103        for r in &results {
104            total_projects += 1;
105            total_threads += r["threads"].as_u64().unwrap_or(0);
106            total_messages += r["messages"].as_u64().unwrap_or(0);
107
108            let path = r["path"].as_str().unwrap_or("-").bright_yellow().bold();
109            let active = r["active_thread"].as_str().unwrap_or("-").bright_cyan();
110            let threads = r["threads"].as_u64().unwrap_or(0).to_string().green();
111            let messages = r["messages"].as_u64().unwrap_or(0).to_string().blue();
112            let modified = r["modified"].as_str().unwrap_or("-").bright_black();
113
114            println!(
115                "{}\n  {} {}\n  {} {}\n  {} {}\n  {} {}\n",
116                path,
117                "Active Thread :".dimmed(),
118                active,
119                "Threads       :".dimmed(),
120                threads,
121                "Messages      :".dimmed(),
122                messages,
123                "Last Modified :".dimmed(),
124                modified,
125            );
126        }
127
128        // Summary footer
129        println!("{}", "─".repeat(50).dimmed());
130        println!(
131            "🌍  Found {} {} | {} {} | {} {}\n",
132            total_projects.to_string().bold().bright_yellow(),
133            if total_projects == 1 {
134                "project"
135            } else {
136                "projects"
137            },
138            total_threads.to_string().bold().green(),
139            if total_threads == 1 {
140                "conversation"
141            } else {
142                "threads"
143            },
144            total_messages.to_string().bold().blue(),
145            if total_messages == 1 {
146                "message"
147            } else {
148                "messages"
149            },
150        );
151    }
152}