fur_cli/commands/
status.rs1use std::fs;
2use std::path::Path;
3use serde_json::{Value, json};
4use std::collections::HashMap;
5use colored::*;
6use crate::frs::avatars::resolve_avatar;
7use clap::Parser;
8
9#[derive(Parser, Debug)]
10pub struct StatusArgs {
11 #[clap(skip)]
13 pub thread_override: Option<String>,
14}
15
16pub fn run_status(args: StatusArgs) {
17 let fur_dir = Path::new(".fur");
18 let index_path = fur_dir.join("index.json");
19
20 if !index_path.exists() {
21 eprintln!("{}", "🚨 .fur/ not found. Run `fur new` first.".red().bold());
22 return;
23 }
24
25 let avatars: Value = serde_json::from_str(
27 &fs::read_to_string(fur_dir.join("avatars.json")).unwrap_or_else(|_| "{}".to_string())
28 ).unwrap_or(json!({}));
29
30 let (index, mut thread, mut current_msg_id) = load_index_and_thread(&fur_dir);
32
33 if let Some(ref tid) = args.thread_override {
35 let thread_path = fur_dir.join("tmp").join(format!("{}.json", tid));
36 if let Ok(content) = fs::read_to_string(&thread_path) {
37 if let Ok(tmp_thread) = serde_json::from_str::<Value>(&content) {
38 thread = tmp_thread;
39 }
40 }
41 }
42
43 let id_to_message = build_id_to_message(&fur_dir, &thread);
45
46 if current_msg_id.is_empty() {
48 if let Some(first) = thread["messages"].as_array().and_then(|arr| arr.get(0)) {
49 if let Some(fid) = first.as_str() {
50 current_msg_id = fid.to_string();
51 }
52 }
53 }
54
55 println!(
57 "{} {} {}",
58 "Active thread:".bright_cyan().bold(),
59 thread["title"].as_str().unwrap_or("Untitled").bright_green().bold(),
60 format!("({})", index["active_thread"].as_str().unwrap_or("?")).bright_black()
61 );
62
63 println!(
65 "{} {}",
66 "Current message:".bright_cyan().bold(),
67 current_msg_id.bright_black() );
69 println!("{}", "─────────────────────────────".bright_black());
70
71 print_lineage(&id_to_message, ¤t_msg_id, &avatars);
73
74 println!("{}", "─────────────────────────────".bright_black());
75 println!("{}", "Next messages from here:".bright_magenta().bold());
76
77 print_next_messages(&id_to_message, &thread, ¤t_msg_id, &avatars);
79}
80
81fn load_index_and_thread(fur_dir: &Path) -> (Value, Value, String) {
83 let index_path = fur_dir.join("index.json");
84 let index: Value =
85 serde_json::from_str(&fs::read_to_string(&index_path).expect("❌ Cannot read index.json"))
86 .unwrap();
87
88 let thread_id = index["active_thread"].as_str().unwrap_or("");
89 let current_msg_id = index["current_message"].as_str().unwrap_or("").to_string();
90
91 let thread_path = fur_dir.join("threads").join(format!("{}.json", thread_id));
92 let thread: Value =
93 serde_json::from_str(&fs::read_to_string(&thread_path).expect("❌ Cannot read thread"))
94 .unwrap();
95
96 (index, thread, current_msg_id)
97}
98
99fn build_id_to_message(fur_dir: &Path, thread: &Value) -> HashMap<String, Value> {
101 let mut id_to_message = HashMap::new();
102 let mut to_visit: Vec<String> = thread["messages"]
103 .as_array()
104 .unwrap_or(&vec![])
105 .iter()
106 .filter_map(|id| id.as_str().map(|s| s.to_string()))
107 .collect();
108
109 while let Some(mid) = to_visit.pop() {
110 let path = fur_dir.join("messages").join(format!("{}.json", mid));
111 if let Ok(content) = fs::read_to_string(path) {
112 if let Ok(json) = serde_json::from_str::<Value>(&content) {
113 if let Some(children) = json["children"].as_array() {
115 for c in children {
116 if let Some(cid) = c.as_str() {
117 to_visit.push(cid.to_string());
118 }
119 }
120 }
121 if let Some(branches) = json["branches"].as_array() {
122 for block in branches {
123 if let Some(arr) = block.as_array() {
124 for c in arr {
125 if let Some(cid) = c.as_str() {
126 to_visit.push(cid.to_string());
127 }
128 }
129 }
130 }
131 }
132 id_to_message.insert(mid.clone(), json);
133 }
134 }
135 }
136 id_to_message
137}
138
139fn print_lineage(id_to_message: &HashMap<String, Value>, current_msg_id: &str, avatars: &Value) {
141 let mut lineage = vec![];
142 let mut current = current_msg_id.to_string();
143 while let Some(msg) = id_to_message.get(¤t) {
144 lineage.push(current.clone());
145 match msg["parent"].as_str() {
146 Some(parent_id) => current = parent_id.to_string(),
147 None => break,
148 }
149 }
150 lineage.reverse();
151
152 for id in &lineage {
153 if let Some(msg) = id_to_message.get(id) {
154 let avatar_key = msg["avatar"].as_str().unwrap_or("???");
155 let (name, emoji) = resolve_avatar(avatars, avatar_key);
156 let text = msg.get("text").and_then(|v| v.as_str()).unwrap_or_else(|| {
157 msg.get("markdown")
158 .and_then(|v| v.as_str())
159 .unwrap_or("<no content>")
160 });
161
162 let preview = text.lines().next().unwrap_or("").chars().take(40).collect::<String>();
163 let marker = if *id == current_msg_id { "(current)".cyan().bold() } else { "✅".green() };
164 let id_display = &id[..8];
165 let branch_label = compute_branch_label(id, id_to_message);
166
167 if msg.get("markdown").is_some() {
168 println!(
169 "{} {} {} {} {} {}",
170 preview.white(),
171 emoji,
172 format!("[{}]", name).bright_yellow().bold(),
173 id_display.bright_black(),
174 branch_label.bright_green(),
175 marker
176 );
177 } else {
178 println!(
179 "{} {} {} {} {} {}",
180 preview.white(),
181 emoji,
182 format!("[{}]", name).bright_yellow().bold(),
183 id_display.bright_black(),
184 branch_label.bright_green(),
185 marker
186 );
187 }
188 }
189 }
190}
191
192fn print_next_messages(id_to_message: &HashMap<String, Value>, thread: &Value, current_msg_id: &str, avatars: &Value) {
194 if let Some(curr_msg) = id_to_message.get(current_msg_id) {
195 let mut next_ids: Vec<String> = vec![];
196
197 if let Some(children) = curr_msg["children"].as_array() {
199 next_ids.extend(children.iter().filter_map(|c| c.as_str().map(|s| s.to_string())));
200 }
201
202 if next_ids.is_empty() {
204 if let Some(parent_id) = curr_msg["parent"].as_str() {
205 if let Some(parent) = id_to_message.get(parent_id) {
206 if let Some(branches) = parent["branches"].as_array() {
207 for block in branches {
208 if let Some(arr) = block.as_array() {
209 if let Some(pos) = arr.iter().position(|c| c.as_str() == Some(current_msg_id)) {
210 for sib in arr.iter().skip(pos + 1) {
211 if let Some(cid) = sib.as_str() {
212 next_ids.push(cid.to_string());
213 }
214 }
215 }
216 }
217 }
218 }
219 }
220 }
221 }
222
223 if next_ids.is_empty() && curr_msg["parent"].is_null() {
225 if let Some(thread_msgs) = thread["messages"].as_array() {
226 if let Some(pos) = thread_msgs.iter().position(|id| id.as_str() == Some(current_msg_id)) {
227 for sib in thread_msgs.iter().skip(pos + 1) {
228 if let Some(cid) = sib.as_str() {
229 next_ids.push(cid.to_string());
230 }
231 }
232 }
233 }
234 }
235
236 if next_ids.is_empty() {
237 println!("{}", "(No further messages in this branch.)".bright_black());
238 } else {
239 for child_id in next_ids {
240 if let Some(msg) = id_to_message.get(&child_id) {
241 let avatar_key = msg["avatar"].as_str().unwrap_or("???");
242 let (name, emoji) = resolve_avatar(avatars, avatar_key);
243 let text = msg.get("text").and_then(|v| v.as_str()).unwrap_or_else(|| {
244 msg.get("markdown")
245 .and_then(|v| v.as_str())
246 .unwrap_or("<no content>")
247 });
248
249 let preview = text.lines().next().unwrap_or("").chars().take(40).collect::<String>();
250 let id_display = &child_id[..8];
251 let branch_label = compute_branch_label(&child_id, id_to_message);
252
253 println!(
254 "🔹 {} {} {} {} {}",
255 preview.white(),
256 emoji,
257 format!("[{}]", name).bright_yellow().bold(),
258 id_display.bright_black(),
259 branch_label.bright_green()
260 );
261 }
262 }
263 }
264 } else {
265 println!("{}", "(No current message found.)".red());
266 }
267}
268
269fn compute_branch_label(msg_id: &str, id_to_message: &HashMap<String, Value>) -> String {
271 let mut labels = vec![];
272 let mut current_id = msg_id;
273
274 while let Some(msg) = id_to_message.get(current_id) {
275 if let Some(parent_id) = msg["parent"].as_str() {
276 if let Some(parent) = id_to_message.get(parent_id) {
277 if let Some(branches) = parent["branches"].as_array() {
278 for (b_idx, branch) in branches.iter().enumerate() {
279 if let Some(arr) = branch.as_array() {
280 if arr.iter().any(|c| c.as_str() == Some(current_id)) {
281 labels.push(format!("{}", b_idx + 1));
282 }
283 }
284 }
285 }
286 }
287 current_id = parent_id;
288 } else {
289 break;
290 }
291 }
292
293 labels.reverse();
294 if labels.is_empty() {
295 "[Root]".to_string()
296 } else {
297 format!("[Branch {}]", labels.join("."))
298 }
299}