/// Terminal display helpers — table-style command rows with live spinner. /// /// Layout (no borders, aligned columns): /// {2 spaces}{action_emoji (2 display cols)}{2 spaces}{name: String { let w = UnicodeWidthStr::width(s); if w < 2 { format!("{s} ") } else { s.to_string() } } // Fixed column widths (in terminal display columns, ASCII-only content assumed for name/payload/result) const NAME_W: usize = 14; const MIN_PAYLOAD_W: usize = 10; const MIN_RESULT_W: usize = 10; // Overhead: 2 (indent) + 2 (action emoji) + 2 (gap) + NAME_W + 2 (gap) + 2 (gap) + 2 (status emoji) + 2 (gap) const FIXED_OVERHEAD: usize = 2 + 2 + 2 + NAME_W + 2 + 2 + 2 + 2; pub fn terminal_width() -> usize { terminal_size::terminal_size() .map(|(w, _)| w.0 as usize) .unwrap_or(120) .max(60) } /// Split remaining space between payload and result columns. /// payload gets ~55%, result gets the rest; both have minimums. fn col_widths() -> (usize, usize) { let tw = terminal_width(); let remaining = tw.saturating_sub(FIXED_OVERHEAD).max(MIN_PAYLOAD_W + MIN_RESULT_W); let payload_w = (remaining * 55 / 100).max(MIN_PAYLOAD_W); let result_w = remaining.saturating_sub(payload_w).max(MIN_RESULT_W); (payload_w, result_w) } /// Truncate a string to at most `max` Unicode chars, appending `…` if cut. /// Must be called on PLAIN (uncolored) text — ANSI codes confuse char counting. pub fn trunc(s: &str, max: usize) -> String { if max == 0 { return String::new(); } let mut chars = s.chars(); let truncated: String = chars.by_ref().take(max).collect(); if chars.next().is_some() { let mut t: String = truncated.chars().take(max.saturating_sub(1)).collect(); t.push('…'); t } else { truncated } } /// Format one table row into a String (no trailing newline). /// Truncation happens on plain `result` BEFORE colorizing, so ANSI reset codes are never cut off. fn format_row(action: &str, name: &str, payload: &str, status: &str, result: &str, err: bool) -> String { let (payload_w, result_w) = col_widths(); let p = trunc(payload, payload_w); // Truncate BEFORE colorizing — avoids dangling ANSI escape sequences let r_plain = trunc(result, result_w); let r = if err { r_plain.red().to_string() } else { r_plain }; format!( " {} {: