fur_cli/renderer/
utils.rs1use chrono::{DateTime, FixedOffset, Local};
2use serde_json::Value;
3use std::fs;
4use std::path::Path;
5
6use crate::avatars::resolve_avatar;
7
8#[allow(dead_code)]
9pub struct MessageInfo {
10 pub date_str: String,
11 pub time_str: String,
12 pub name: String,
13 pub emoji: String,
14 pub text: String,
15 pub markdown: Option<String>,
16 pub attachment: Option<String>,
17 #[allow(dead_code)]
18 pub children: Vec<String>,
19 pub branches: Vec<Vec<String>>,
20}
21
22pub fn preview_of(msg: &Value, width: usize) -> String {
28 if let Some(text) = msg.get("text").and_then(|v| v.as_str()) {
29 if !text.trim().is_empty() {
30 return clip(text, width);
31 }
32 }
33
34 if let Some(path) = msg.get("markdown").and_then(|v| v.as_str()) {
35 if let Some(line) = first_meaningful_line(path) {
36 return clip(&line, width);
37 }
38
39 let name = Path::new(path)
41 .file_name()
42 .and_then(|f| f.to_str())
43 .unwrap_or(path);
44
45 return format!("📄 {}", clip(name, width.saturating_sub(2)));
46 }
47
48 "<no content>".to_string()
49}
50
51fn first_meaningful_line(path: &str) -> Option<String> {
54 let content = fs::read_to_string(path).ok()?;
55
56 let mut lines = content.lines().peekable();
57
58 if lines.peek().map(|l| l.trim_end()) == Some("---") {
59 lines.next();
60 for line in lines.by_ref() {
61 if line.trim_end() == "---" {
62 break;
63 }
64 }
65 }
66
67 for line in lines {
68 let trimmed = line.trim();
69
70 if trimmed.is_empty() || trimmed.starts_with("<!--") {
71 continue;
72 }
73
74 let cleaned = trimmed.trim_start_matches('#').trim();
75
76 if !cleaned.is_empty() {
77 return Some(cleaned.to_string());
78 }
79 }
80
81 None
82}
83
84fn clip(text: &str, width: usize) -> String {
85 let line = text.lines().next().unwrap_or("").trim();
86
87 if line.chars().count() <= width {
88 return line.to_string();
89 }
90
91 let kept: String = line.chars().take(width.saturating_sub(1)).collect();
92
93 format!("{}…", kept.trim_end())
94}
95
96pub fn load_message(fur_dir: &Path, msg_id: &str, avatars: &Value) -> Option<MessageInfo> {
97 let msg_path = fur_dir.join("messages").join(format!("{}.json", msg_id));
98 let msg_content = fs::read_to_string(&msg_path).ok()?;
99 let msg_json: Value = serde_json::from_str(&msg_content).ok()?;
100
101 let raw_time = msg_json["timestamp"].as_str().unwrap_or("???");
103 let (date_str, time_str) = if let Ok(dt) = raw_time.parse::<DateTime<FixedOffset>>() {
104 let local_dt = dt.with_timezone(&Local);
105 (
106 local_dt.format("%Y-%m-%d").to_string(),
107 local_dt.format("%H:%M:%S").to_string(),
108 )
109 } else {
110 (raw_time.to_string(), "".to_string())
111 };
112
113 let avatar_key = msg_json["avatar"].as_str().unwrap_or("???");
115 let (name, emoji) = resolve_avatar(avatars, avatar_key);
116
117 let text = msg_json["text"]
119 .as_str()
120 .unwrap_or("<no content>")
121 .to_string();
122 let markdown = msg_json["markdown"].as_str().map(|s| s.to_string());
123
124 let children = msg_json["children"]
126 .as_array()
127 .map(|arr| {
128 arr.iter()
129 .filter_map(|v| v.as_str().map(|s| s.to_string()))
130 .collect()
131 })
132 .unwrap_or_else(Vec::new);
133
134 let branches = msg_json["branches"]
136 .as_array()
137 .map(|outer| {
138 outer
139 .iter()
140 .filter_map(|block| {
141 block.as_array().map(|arr| {
142 arr.iter()
143 .filter_map(|v| v.as_str().map(|s| s.to_string()))
144 .collect::<Vec<_>>()
145 })
146 })
147 .collect::<Vec<_>>()
148 })
149 .unwrap_or_else(Vec::new);
150
151 let attachment = msg_json["attachment"].as_str().map(|s| s.to_string());
152
153 Some(MessageInfo {
154 date_str,
155 time_str,
156 name,
157 emoji,
158 text,
159 markdown,
160 attachment,
161 children,
162 branches,
163 })
164}