fur_cli/commands/status/
render.rs1use colored::*;
2use serde_json::Value;
3use std::collections::HashMap;
4use crate::frs::avatars::resolve_avatar;
5
6type Map = HashMap<String, Value>;
7
8pub 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
27pub 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
36pub 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
188pub 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}