fur_cli/commands/
thread.rs1use std::fs;
2use std::path::Path;
3use serde_json::{Value, json};
4use clap::Parser;
5use chrono::{DateTime, Local, Utc};
6use crate::renderer::list::render_list;
7
8#[derive(Parser)]
10pub struct ThreadArgs {
11 pub id: Option<String>,
13
14 #[arg(long)]
16 pub view: bool,
17}
18
19pub fn run_thread(args: ThreadArgs) {
21 let fur_dir = Path::new(".fur");
22 let index_path = fur_dir.join("index.json");
23
24 if !index_path.exists() {
25 eprintln!("🚨 .fur/ not found. Run `fur new` first.");
26 return;
27 }
28
29 let mut index: Value =
30 serde_json::from_str(&fs::read_to_string(&index_path).unwrap()).unwrap();
31
32 if args.view || args.id.is_none() {
36 let empty_vec: Vec<Value> = Vec::new();
37 let threads = index["threads"].as_array().unwrap_or(&empty_vec);
38 let active = index["active_thread"].as_str().unwrap_or("");
39
40 let mut rows = Vec::new();
41 let mut active_idx = None;
42
43 let mut thread_info = Vec::new();
45 for tid in threads {
46 if let Some(tid_str) = tid.as_str() {
47 let thread_path = fur_dir.join("threads").join(format!("{}.json", tid_str));
48 if let Ok(content) = fs::read_to_string(thread_path) {
49 if let Ok(thread_json) = serde_json::from_str::<Value>(&content) {
50 let title = thread_json["title"].as_str().unwrap_or("Untitled").to_string();
51 let created_raw = thread_json["created_at"].as_str().unwrap_or("");
52 let msg_count = thread_json["messages"]
53 .as_array()
54 .map(|a| a.len())
55 .unwrap_or(0);
56
57 let parsed_time = DateTime::parse_from_rfc3339(created_raw)
59 .map(|dt| dt.with_timezone(&Utc))
60 .unwrap_or_else(|_| Utc::now());
61 let local_time: DateTime<Local> = DateTime::from(parsed_time);
62 let date_str = local_time.format("%Y-%m-%d").to_string();
63 let time_str = local_time.format("%H:%M").to_string();
64
65 thread_info.push((
66 tid_str.to_string(),
67 title,
68 date_str,
69 time_str,
70 msg_count,
71 parsed_time,
72 ));
73 }
74 }
75 }
76 }
77
78 thread_info.sort_by(|a, b| b.5.cmp(&a.5));
80
81 for (i, (tid, title, date, time, msg_count, _)) in thread_info.iter().enumerate() {
83 let short_id = &tid[..8];
84 rows.push(vec![
85 short_id.to_string(),
86 title.to_string(),
87 format!("{} | {}", date, time),
88 msg_count.to_string(),
89 ]);
90 if tid == active {
91 active_idx = Some(i);
92 }
93 }
94
95 render_list("Threads", &["ID", "Title", "Created", "#Msgs"], rows, active_idx);
96 return;
97 }
98
99 if let Some(tid) = args.id {
103 let empty_vec: Vec<Value> = Vec::new();
104 let threads: Vec<String> = index["threads"]
105 .as_array()
106 .unwrap_or(&empty_vec)
107 .iter()
108 .filter_map(|t| t.as_str().map(|s| s.to_string()))
109 .collect();
110
111 let mut found = threads.iter().find(|&s| s == &tid);
112 if found.is_none() {
113 let matches: Vec<&String> = threads.iter().filter(|s| s.starts_with(&tid)).collect();
114 if matches.len() == 1 {
115 found = Some(matches[0]);
116 } else if matches.len() > 1 {
117 eprintln!("❌ Ambiguous prefix '{}'. Matches: {:?}", tid, matches);
118 return;
119 }
120 }
121
122 let tid_full = match found {
123 Some(s) => s,
124 None => {
125 eprintln!("❌ Thread not found: {}", tid);
126 return;
127 }
128 };
129
130 index["active_thread"] = json!(tid_full);
131 index["current_message"] = serde_json::Value::Null;
132 fs::write(&index_path, serde_json::to_string_pretty(&index).unwrap()).unwrap();
133
134 let thread_path = fur_dir.join("threads").join(format!("{}.json", tid_full));
135 let content = fs::read_to_string(thread_path).unwrap();
136 let thread_json: Value = serde_json::from_str(&content).unwrap();
137 let title = thread_json["title"].as_str().unwrap_or("Untitled");
138
139 println!("✔️ Switched active thread to {} \"{}\"", &tid_full[..8], title);
140 }
141}