aboutsummaryrefslogtreecommitdiffstats
path: root/src/output.rs
blob: 9cc21b23733ad0346387964b1a98dea5a5be4cff (plain)
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"));
    }
}