aboutsummaryrefslogtreecommitdiffstats
path: root/src/output.rs
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-09-26 18:47:40 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-09-26 18:47:40 -0300
commitd9738b4be2add82ff955f061c79316b0dc91768f (patch)
treee0f31bb9f72dff315fc475f9e9f30322b69602ff /src/output.rs
downloadposta-d9738b4be2add82ff955f061c79316b0dc91768f.tar.gz
posta-d9738b4be2add82ff955f061c79316b0dc91768f.zip
posta: generic IMAP/SMTP mail CLI with JSON outputHEADmain
Diffstat (limited to 'src/output.rs')
-rw-r--r--src/output.rs98
1 files changed, 98 insertions, 0 deletions
diff --git a/src/output.rs b/src/output.rs
new file mode 100644
index 0000000..9cc21b2
--- /dev/null
+++ b/src/output.rs
@@ -0,0 +1,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"));
+ }
+}