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_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
22pub 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
31pub 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
183pub 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}