From 29df988d9ddf9f1d6463cd0c98ed47d64f14f4a3 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Mon, 10 Aug 2026 13:31:14 +0200 Subject: [PATCH] feat(cli): render uptime in human units The UPTIME column printed raw seconds, so a server up for three hours read as 10549s. Format the two largest non-zero units instead, dropping the smaller one when it is zero: 49s, 12m 4s, 2h 55m, 6d 3h. Days is the largest unit and the output never exceeds eight characters, so the ten-wide column still holds and RESTARTS stays put. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01D9BWMXKKNpid7XM6oFbyYq --- crates/xy/src/cli/format.rs | 83 +++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/crates/xy/src/cli/format.rs b/crates/xy/src/cli/format.rs index 392a999..797e0a8 100644 --- a/crates/xy/src/cli/format.rs +++ b/crates/xy/src/cli/format.rs @@ -7,10 +7,7 @@ pub fn list_table(rows: &[ServerSummary]) -> String { for r in rows { let pid = r.pid.map(|p| p.to_string()).unwrap_or_else(|| "-".into()); - let up = r - .uptime_secs - .map(|s| format!("{}s", s)) - .unwrap_or_else(|| "-".into()); + let up = r.uptime_secs.map(uptime).unwrap_or_else(|| "-".into()); out.push_str(&format!( "{:<20}{:<12}{:<8}{:<8}{:<10}{}\n", @@ -26,6 +23,28 @@ pub fn list_table(rows: &[ServerSummary]) -> String { out } +fn uptime(secs: u64) -> String { + const MINUTE: u64 = 60; + const HOUR: u64 = 60 * MINUTE; + const DAY: u64 = 24 * HOUR; + + let (major, minor, major_unit, minor_unit) = if secs >= DAY { + (secs / DAY, secs % DAY / HOUR, "d", "h") + } else if secs >= HOUR { + (secs / HOUR, secs % HOUR / MINUTE, "h", "m") + } else if secs >= MINUTE { + (secs / MINUTE, secs % MINUTE, "m", "s") + } else { + return format!("{secs}s"); + }; + + if minor == 0 { + return format!("{major}{major_unit}"); + } + + format!("{major}{major_unit} {minor}{minor_unit}") +} + pub(crate) fn wait_line(info: &WaitInfo) -> String { format!( " wait-for: {}\n {}s elapsed, timeout {}s\n", @@ -55,6 +74,62 @@ mod tests { assert!(out.contains("waiting"), "got: {out}"); } + #[test] + fn uptime_under_a_minute_is_bare_seconds() { + assert_eq!(uptime(0), "0s"); + assert_eq!(uptime(49), "49s"); + assert_eq!(uptime(59), "59s"); + } + + #[test] + fn uptime_under_an_hour_is_minutes_and_seconds() { + assert_eq!(uptime(60), "1m"); + assert_eq!(uptime(724), "12m 4s"); + assert_eq!(uptime(3_599), "59m 59s"); + } + + #[test] + fn uptime_under_a_day_is_hours_and_minutes() { + assert_eq!(uptime(3_600), "1h"); + assert_eq!(uptime(10_549), "2h 55m"); + assert_eq!(uptime(86_399), "23h 59m"); + } + + #[test] + fn uptime_of_a_day_or_more_is_days_and_hours() { + assert_eq!(uptime(86_400), "1d"); + assert_eq!(uptime(529_200), "6d 3h"); + assert_eq!(uptime(2_707_200), "31d 8h"); + } + + #[test] + fn uptime_drops_a_trailing_zero_unit() { + assert_eq!(uptime(120), "2m"); + assert_eq!(uptime(7_200), "2h"); + assert_eq!(uptime(518_400), "6d"); + } + + #[test] + fn list_renders_uptime_in_human_units() { + let rows = vec![ServerSummary { + name: "vestige".to_string(), + state: ServerState::Running, + pid: Some(4821), + port: 3928, + uptime_secs: Some(10_549), + restart_count: 0, + last_exit: None, + }]; + + let out = list_table(&rows); + + assert!(out.contains("2h 55m"), "got: {out}"); + assert!( + !out.contains("10549"), + "raw seconds must not leak; got: {out}" + ); + } + #[test] fn status_renders_the_wait_condition_and_elapsed() { let info = WaitInfo {