fur_cli/commands/
tree.rs

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    // Load index and thread
25    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    // Load avatars.json once
40    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
60/// Preload all messages into a HashMap
61fn 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                // enqueue children + branches
75                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
100/// Recursive tree renderer
101fn 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        // build tree connector
110        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        // Lifetime-safe empty vec
146        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        // merge both: if branches exist, prefer them
151        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}