Skip to main content

fur_cli/commands/status/
render.rs

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