1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
//! JSON and table rendering.
use anyhow::Result;
use serde_json::Value;
use crate::message::MsgSummary;
pub fn print_json(v: &Value) -> Result<()> {
println!("{}", serde_json::to_string_pretty(v)?);
Ok(())
}
/// Truncates to `max` characters, ellipsizing multi-byte text safely.
pub fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let cut: String = s.chars().take(max.saturating_sub(1)).collect();
format!("{cut}…")
}
}
pub fn message_table(items: &[MsgSummary]) -> String {
let mut out = String::new();
out.push_str(&format!(
"{:>6} {:2} {:22} {:32} {}\n",
"UID", "", "DATE", "FROM", "SUBJECT"
));
for m in items {
let unread = if m.flags.iter().any(|f| f == "\\Seen") {
" "
} else {
"●"
};
out.push_str(&format!(
"{:>6} {:2} {:22} {:32} {}\n",
m.uid,
unread,
truncate(m.date.as_deref().unwrap_or("-"), 22),
truncate(m.from.as_deref().unwrap_or("-"), 32),
truncate(m.subject.as_deref().unwrap_or("-"), 60),
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_keeps_short_strings() {
assert_eq!(truncate("hola", 10), "hola");
assert_eq!(truncate("hola", 4), "hola");
}
#[test]
fn truncate_ellipsizes_long_strings() {
assert_eq!(truncate("abcdefgh", 5), "abcd…");
}
#[test]
fn truncate_is_multibyte_safe() {
// Would panic with byte-based slicing.
assert_eq!(truncate("ñandú y cóndor", 6), "ñandú…");
assert_eq!(truncate("🚀🚀🚀🚀", 3), "🚀🚀…");
}
fn sample(flags: &[&str]) -> MsgSummary {
MsgSummary {
uid: 7,
flags: flags.iter().map(|s| s.to_string()).collect(),
size: None,
date: Some("Fri, 17 Jul 2026 10:00:00 -0300".into()),
from: Some("Alice <alice@example.com>".into()),
to: None,
subject: Some("Hello".into()),
}
}
#[test]
fn table_marks_unread_messages() {
let unread = message_table(&[sample(&[])]);
assert!(unread.contains('●'), "unread marker missing:\n{unread}");
let read = message_table(&[sample(&["\\Seen"])]);
assert!(
!read.contains('●'),
"read message should have no marker:\n{read}"
);
}
#[test]
fn table_contains_uid_and_subject() {
let t = message_table(&[sample(&[])]);
assert!(t.contains(" 7"));
assert!(t.contains("Hello"));
}
}
|