Skip to main content

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 conversation_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 conversation
25    let index_data: Value =
26        serde_json::from_str(
27            &crate::security::io::read_text_file(&index_path)
28                .expect("❌ Project locked. Run `fur unlock`.")
29        ).unwrap();
30
31    let conversation_id = if let Some(ref override_id) = args.conversation_override {
32        override_id
33    } else {
34        index_data["active_thread"].as_str().unwrap_or("")
35    };
36    let convo_path = fur_dir.join("threads").join(format!("{}.json", conversation_id));
37    let conversation_data: Value =
38        serde_json::from_str(
39            &crate::security::io::read_text_file(&convo_path)
40                .expect("❌ Project locked. Run `fur unlock`.")
41        ).unwrap();
42
43    // Load avatars.json once
44    let avatars: Value = serde_json::from_str(
45        &fs::read_to_string(fur_dir.join("avatars.json")).unwrap_or_else(|_| "{}".to_string())
46    ).unwrap_or(serde_json::json!({}));
47
48    println!(
49        "{} {}",
50        "🌳 Conversation Tree:".bold().cyan(),
51        conversation_data["title"].as_str().unwrap_or("Untitled").green().bold()
52    );
53
54    if let Some(messages) = conversation_data["messages"].as_array() {
55        let id_to_message = load_conversation_messages(&fur_dir, &conversation_data);
56        for (idx, msg_id) in messages.iter().enumerate() {
57            if let Some(mid) = msg_id.as_str() {
58                render_message(&id_to_message, mid, "", idx == messages.len() - 1, &avatars);
59            }
60        }
61    }
62}
63
64fn load_conversation_messages(fur_dir: &Path, conversation: &Value) -> HashMap<String, Value> {
65    let mut id_to_message = HashMap::new();
66    let mut to_visit: Vec<String> = conversation["messages"]
67        .as_array()
68        .unwrap_or(&vec![])
69        .iter()
70        .filter_map(|id| id.as_str().map(|s| s.to_string()))
71        .collect();
72
73    while let Some(mid) = to_visit.pop() {
74        let path = fur_dir.join("messages").join(format!("{}.json", mid));
75        if let Some(content) = crate::security::io::read_text_file(&path) {
76            if let Ok(json) = serde_json::from_str::<Value>(&content) {
77                // enqueue children + branches
78                if let Some(children) = json["children"].as_array() {
79                    for c in children {
80                        if let Some(cid) = c.as_str() {
81                            to_visit.push(cid.to_string());
82                        }
83                    }
84                }
85                if let Some(branches) = json["branches"].as_array() {
86                    for block in branches {
87                        if let Some(arr) = block.as_array() {
88                            for c in arr {
89                                if let Some(cid) = c.as_str() {
90                                    to_visit.push(cid.to_string());
91                                }
92                            }
93                        }
94                    }
95                }
96                id_to_message.insert(mid.clone(), json);
97            }
98        }
99    }
100    id_to_message
101}
102
103
104fn render_message(
105    id_to_message: &HashMap<String, Value>,
106    msg_id: &str,
107    prefix: &str,
108    is_last: bool,
109    avatars: &Value,
110) {
111    if let Some(msg) = id_to_message.get(msg_id) {
112        // build tree connector
113        let branch_symbol = if is_last { "└──" } else { "├──" };
114        let tree_prefix = format!("{}{}", prefix, branch_symbol.bright_green());
115
116        let avatar_key = msg["avatar"].as_str().unwrap_or("???");
117        let (name, emoji) = resolve_avatar(avatars, avatar_key);
118
119        let text = msg.get("text").and_then(|v| v.as_str()).unwrap_or_else(|| {
120            msg.get("markdown")
121                .and_then(|v| v.as_str())
122                .unwrap_or("<no content>")
123        });
124
125        let id_display = msg_id[..8].to_string();
126
127        if msg.get("markdown").is_some() {
128            println!(
129                "{} {} {} {} {} {}",
130                tree_prefix,
131                "[Root]".cyan(),
132                emoji.yellow(),
133                format!("[{}]", name).bright_yellow(),
134                text.white(),
135                format!("📄 {}", id_display).magenta()
136            );
137        } else {
138            println!(
139                "{} {} {} {} {}",
140                tree_prefix,
141                "[Root]".cyan(),
142                emoji.yellow(),
143                format!("[{}]", name).bright_yellow(),
144                format!("{} {}", text.white(), id_display.bright_black())
145            );
146        }
147
148        // Lifetime-safe empty vec
149        let empty: Vec<Value> = Vec::new();
150        let children = msg["children"].as_array().unwrap_or(&empty);
151        let branches = msg["branches"].as_array().unwrap_or(&empty);
152
153        // merge both: if branches exist, prefer them
154        if !branches.is_empty() {
155            for (_b_idx, branch) in branches.iter().enumerate() {
156                if let Some(arr) = branch.as_array() {
157                    for (i, child_id) in arr.iter().enumerate() {
158                        if let Some(cid) = child_id.as_str() {
159                            let new_prefix = format!(
160                                "{}{}   ",
161                                prefix,
162                                if is_last { "    " } else { "│  " }.bright_green()
163                            );
164                            render_message(id_to_message, cid, &new_prefix, i == arr.len() - 1, avatars);
165                        }
166                    }
167                }
168            }
169        } else {
170            for (i, child_id) in children.iter().enumerate() {
171                if let Some(cid) = child_id.as_str() {
172                    let new_prefix = format!(
173                        "{}{}   ",
174                        prefix,
175                        if is_last { "    " } else { "│  " }.bright_green()
176                    );
177                    render_message(id_to_message, cid, &new_prefix, i == children.len() - 1, avatars);
178                }
179            }
180        }
181    }
182}