Skip to main content

fur_cli/commands/status/
render.rs

1use colored::*;
2use serde_json::Value;
3use std::collections::HashMap;
4use crate::frs::avatars::resolve_avatar;
5
6type Map = HashMap<String, Value>;
7
8/// Print active conversation title
9pub fn print_active_conversation(index: &Value, conversation: &Value) {
10    let fallback_title = conversation["title"]
11        .as_str()
12        .unwrap_or("Untitled");
13
14    let title = index["title"]
15        .as_str()
16        .unwrap_or(fallback_title);
17
18    println!(
19        "{} {} {}",
20        "Active conversation:".bright_cyan().bold(),
21        title.bright_green().bold(),
22        format!("({})", index["active_thread"].as_str().unwrap_or("?"))
23            .bright_black()
24    );
25}
26
27/// Print current message line
28pub fn print_current_message(current_msg_id: &str) {
29    println!(
30        "{} {}",
31        "Current message:".bright_cyan().bold(),
32        current_msg_id.bright_black()
33    );
34}
35
36/// Print lineage (ancestors)
37pub fn print_lineage(
38    map: &HashMap<String, Value>,
39    current: &str,
40    avatars: &Value
41) {
42    let mut chain = vec![];
43    let mut cur = current.to_string();
44
45    while let Some(msg) = map.get(&cur) {
46        chain.push(cur.clone());
47        match msg["parent"].as_str() {
48            Some(pid) => cur = pid.to_string(),
49            None => break,
50        }
51    }
52
53    chain.reverse();
54
55    for mid in chain {
56        if let Some(msg) = map.get(&mid) {
57            let avatar_key = msg["avatar"].as_str().unwrap_or("???");
58            let (name, emoji) = resolve_avatar(avatars, avatar_key);
59
60            let text = msg.get("text")
61                .and_then(|v| v.as_str())
62                .or_else(|| msg["markdown"].as_str())
63                .unwrap_or("<no content>");
64
65            let preview = text.lines().next().unwrap_or("")
66                .chars().take(40).collect::<String>();
67
68            let marker = if mid == current {
69                "(current)".cyan().bold()
70            } else {
71                "✅".green()
72            };
73
74            let branch_label = compute_branch_label(&mid, map);
75
76            println!(
77                "{} {} {} {} {} {}",
78                preview.white(),
79                emoji,
80                format!("[{}]", name).bright_yellow().bold(),
81                &mid[..8].bright_black(),
82                branch_label.bright_green(),
83                marker
84            );
85        }
86    }
87}
88
89pub fn print_next_messages(
90    map: &Map,
91    conversation: &Value,
92    current: &str,
93    avatars: &Value,
94) {
95    let curr_msg = match map.get(current) {
96        Some(v) => v,
97        None => return println!("{}", "(No current message found.)".red()),
98    };
99
100    let next = get_children(curr_msg)
101        .or_else(|| get_sibling_branch(map, curr_msg, current))
102        .or_else(|| get_top_level_siblings(conversation, current))
103        .unwrap_or_default();
104
105    if next.is_empty() {
106        println!("{}", "(No further messages in this branch.)".bright_black());
107        return;
108    }
109
110    for cid in next {
111        if let Some(msg) = map.get(&cid) {
112            render_preview(msg, avatars, &cid, map);
113        }
114    }
115}
116
117
118fn get_children(curr_msg: &Value) -> Option<Vec<String>> {
119    let arr = curr_msg["children"].as_array()?;
120    let v = arr
121        .iter()
122        .filter_map(|c| c.as_str().map(|s| s.to_string()))
123        .collect::<Vec<_>>();
124
125    if v.is_empty() { None } else { Some(v) }
126}
127
128fn get_sibling_branch(map: &Map, curr_msg: &Value, current: &str) -> Option<Vec<String>> {
129    let parent_id = curr_msg["parent"].as_str()?;
130    let parent = map.get(parent_id)?;
131
132    let blocks = parent["branches"].as_array()?;
133    for block in blocks {
134        if let Some(arr) = block.as_array() {
135            if let Some(pos) = arr.iter().position(|v| v.as_str() == Some(current)) {
136                let sibs = arr.iter()
137                    .skip(pos + 1)
138                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
139                    .collect::<Vec<_>>();
140                if !sibs.is_empty() {
141                    return Some(sibs);
142                }
143            }
144        }
145    }
146
147    None
148}
149
150fn get_top_level_siblings(conversation: &Value, current: &str) -> Option<Vec<String>> {
151    let arr = conversation["messages"].as_array()?;
152
153    let pos = arr.iter().position(|v| v.as_str() == Some(current))?;
154    let v = arr.iter()
155        .skip(pos + 1)
156        .filter_map(|v| v.as_str().map(|s| s.to_string()))
157        .collect::<Vec<_>>();
158
159    if v.is_empty() { None } else { Some(v) }
160}
161
162
163fn render_preview(msg: &Value, avatars: &Value, cid: &str, map: &Map) {
164    let avatar_key = msg["avatar"].as_str().unwrap_or("???");
165    let (name, emoji) = resolve_avatar(avatars, avatar_key);
166
167    let text = msg.get("text")
168        .and_then(|v| v.as_str())
169        .or_else(|| msg["markdown"].as_str())
170        .unwrap_or("<no content>");
171
172    let preview = text.lines().next().unwrap_or("")
173        .chars().take(40).collect::<String>();
174
175    let branch_label = compute_branch_label(cid, map);
176
177    println!(
178        "🔹 {} {} {} {} {}",
179        preview.white(),
180        emoji,
181        format!("[{}]", name).bright_yellow().bold(),
182        &cid[..8].bright_black(),
183        branch_label.bright_green(),
184    );
185}
186
187
188/// Compute branch label
189pub fn compute_branch_label(
190    msg_id: &str,
191    map: &HashMap<String, Value>
192) -> String {
193    let mut labels = vec![];
194    let mut cur = msg_id;
195
196    while let Some(msg) = map.get(cur) {
197        if let Some(pid) = msg["parent"].as_str() {
198            if let Some(parent) = map.get(pid) {
199                if let Some(blocks) = parent["branches"].as_array() {
200                    for (i, block) in blocks.iter().enumerate() {
201                        if let Some(arr) = block.as_array() {
202                            if arr.iter().any(|v| v.as_str() == Some(cur)) {
203                                labels.push(format!("{}", i + 1));
204                            }
205                        }
206                    }
207                }
208                cur = pid;
209                continue;
210            }
211        }
212        break;
213    }
214
215    labels.reverse();
216    if labels.is_empty() {
217        "[Root]".to_string()
218    } else {
219        format!("[Branch {}]", labels.join("."))
220    }
221}