1use std::fs;
2use std::path::Path;
3use serde_json::Value;
4use clap::Parser;
5use std::collections::HashMap;
6use crate::frs::avatars::resolve_avatar;
7use colored::*;
8
9#[derive(Parser, Clone)]
10pub struct TreeArgs {
11 #[clap(skip)]
12 pub thread_override: Option<String>,
13}
14
15pub fn run_tree(args: TreeArgs) {
16 let fur_dir = Path::new(".fur");
17 let index_path = fur_dir.join("index.json");
18
19 if !index_path.exists() {
20 eprintln!("{}", "🚨 .fur/ not found. Run `fur new` first.".red().bold());
21 return;
22 }
23
24 let index_data: Value =
26 serde_json::from_str(&fs::read_to_string(&index_path).expect("❌ Cannot read index.json"))
27 .unwrap();
28
29 let thread_id = if let Some(ref override_id) = args.thread_override {
30 override_id
31 } else {
32 index_data["active_thread"].as_str().unwrap_or("")
33 };
34 let thread_path = fur_dir.join("threads").join(format!("{}.json", thread_id));
35 let thread_data: Value =
36 serde_json::from_str(&fs::read_to_string(&thread_path).expect("❌ Cannot read thread"))
37 .unwrap();
38
39 let avatars: Value = serde_json::from_str(
41 &fs::read_to_string(fur_dir.join("avatars.json")).unwrap_or_else(|_| "{}".to_string())
42 ).unwrap_or(serde_json::json!({}));
43
44 println!(
45 "{} {}",
46 "🌳 Thread Tree:".bold().cyan(),
47 thread_data["title"].as_str().unwrap_or("Untitled").green().bold()
48 );
49
50 if let Some(messages) = thread_data["messages"].as_array() {
51 let id_to_message = build_id_to_message(&fur_dir, &thread_data);
52 for (idx, msg_id) in messages.iter().enumerate() {
53 if let Some(mid) = msg_id.as_str() {
54 render_message(&id_to_message, mid, "", idx == messages.len() - 1, &avatars);
55 }
56 }
57 }
58}
59
60fn build_id_to_message(fur_dir: &Path, thread: &Value) -> HashMap<String, Value> {
62 let mut id_to_message = HashMap::new();
63 let mut to_visit: Vec<String> = thread["messages"]
64 .as_array()
65 .unwrap_or(&vec![])
66 .iter()
67 .filter_map(|id| id.as_str().map(|s| s.to_string()))
68 .collect();
69
70 while let Some(mid) = to_visit.pop() {
71 let path = fur_dir.join("messages").join(format!("{}.json", mid));
72 if let Ok(content) = fs::read_to_string(path) {
73 if let Ok(json) = serde_json::from_str::<Value>(&content) {
74 if let Some(children) = json["children"].as_array() {
76 for c in children {
77 if let Some(cid) = c.as_str() {
78 to_visit.push(cid.to_string());
79 }
80 }
81 }
82 if let Some(branches) = json["branches"].as_array() {
83 for block in branches {
84 if let Some(arr) = block.as_array() {
85 for c in arr {
86 if let Some(cid) = c.as_str() {
87 to_visit.push(cid.to_string());
88 }
89 }
90 }
91 }
92 }
93 id_to_message.insert(mid.clone(), json);
94 }
95 }
96 }
97 id_to_message
98}
99
100fn render_message(
102 id_to_message: &HashMap<String, Value>,
103 msg_id: &str,
104 prefix: &str,
105 is_last: bool,
106 avatars: &Value,
107) {
108 if let Some(msg) = id_to_message.get(msg_id) {
109 let branch_symbol = if is_last { "└──" } else { "├──" };
111 let tree_prefix = format!("{}{}", prefix, branch_symbol.bright_green());
112
113 let avatar_key = msg["avatar"].as_str().unwrap_or("???");
114 let (name, emoji) = resolve_avatar(avatars, avatar_key);
115
116 let text = msg.get("text").and_then(|v| v.as_str()).unwrap_or_else(|| {
117 msg.get("markdown")
118 .and_then(|v| v.as_str())
119 .unwrap_or("<no content>")
120 });
121
122 let id_display = msg_id[..8].to_string();
123
124 if msg.get("markdown").is_some() {
125 println!(
126 "{} {} {} {} {} {}",
127 tree_prefix,
128 "[Root]".cyan(),
129 emoji.yellow(),
130 format!("[{}]", name).bright_yellow(),
131 text.white(),
132 format!("📄 {}", id_display).magenta()
133 );
134 } else {
135 println!(
136 "{} {} {} {} {}",
137 tree_prefix,
138 "[Root]".cyan(),
139 emoji.yellow(),
140 format!("[{}]", name).bright_yellow(),
141 format!("{} {}", text.white(), id_display.bright_black())
142 );
143 }
144
145 let empty: Vec<Value> = Vec::new();
147 let children = msg["children"].as_array().unwrap_or(&empty);
148 let branches = msg["branches"].as_array().unwrap_or(&empty);
149
150 if !branches.is_empty() {
152 for (_b_idx, branch) in branches.iter().enumerate() {
153 if let Some(arr) = branch.as_array() {
154 for (i, child_id) in arr.iter().enumerate() {
155 if let Some(cid) = child_id.as_str() {
156 let new_prefix = format!(
157 "{}{} ",
158 prefix,
159 if is_last { " " } else { "│ " }.bright_green()
160 );
161 render_message(id_to_message, cid, &new_prefix, i == arr.len() - 1, avatars);
162 }
163 }
164 }
165 }
166 } else {
167 for (i, child_id) in children.iter().enumerate() {
168 if let Some(cid) = child_id.as_str() {
169 let new_prefix = format!(
170 "{}{} ",
171 prefix,
172 if is_last { " " } else { "│ " }.bright_green()
173 );
174 render_message(id_to_message, cid, &new_prefix, i == children.len() - 1, avatars);
175 }
176 }
177 }
178 }
179}