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