fur_cli/commands/
sweep.rs1use 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#[derive(Parser)]
12pub struct SweepArgs {
13 #[arg(long)]
15 pub dir: Option<String>,
16
17 #[arg(long, default_value_t = 5)]
19 pub depth: usize,
20
21 #[arg(long)]
23 pub json: bool,
24
25 #[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 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}