1use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
4
5pub fn bytes(value: u64) -> String {
9 const UNITS: &[&str] = &["B", "K", "M", "G", "T", "P"];
10 let mut v = value as f64;
11 let mut unit = 0;
12 while v >= 1024.0 && unit < UNITS.len() - 1 {
13 v /= 1024.0;
14 unit += 1;
15 }
16 if unit == 0 {
17 format!("{value}B")
18 } else if v < 10.0 {
19 format!("{v:.1}{}", UNITS[unit])
20 } else {
21 format!("{v:.0}{}", UNITS[unit])
22 }
23}
24
25pub fn rate(bytes_per_sec: f64) -> String {
27 format!("{}/s", bytes(bytes_per_sec.max(0.0) as u64))
28}
29
30pub fn hours_minutes(secs: u64) -> String {
32 let minutes = secs / 60;
33 if minutes >= 60 {
34 format!("{}h{:02}m", minutes / 60, minutes % 60)
35 } else {
36 format!("{minutes}m")
37 }
38}
39
40pub fn latency(ms: f32) -> String {
42 if ms >= 1000.0 {
43 format!("{:.1}s", ms / 1000.0)
44 } else if ms >= 10.0 {
45 format!("{ms:.0}ms")
46 } else {
47 format!("{ms:.1}ms")
48 }
49}
50
51pub fn duration(secs: u64) -> String {
53 let days = secs / 86_400;
54 let hours = (secs % 86_400) / 3_600;
55 let minutes = (secs % 3_600) / 60;
56 if days > 0 {
57 format!("{days}d{hours:02}h")
58 } else if hours > 0 {
59 format!("{hours}h{minutes:02}m")
60 } else {
61 format!("{minutes:02}:{:02}", secs % 60)
62 }
63}
64
65pub fn age(secs: u64) -> String {
67 match secs {
68 0..10 => "just now".to_owned(),
69 10..120 => format!("{secs}s ago"),
70 120..7_200 => format!("{}m ago", secs / 60),
71 7_200..172_800 => format!("{}h ago", secs / 3_600),
72 _ => format!("{}d ago", secs / 86_400),
73 }
74}
75
76pub fn link_speed(mbps: f64) -> String {
78 if mbps >= 1000.0 {
79 let gbps = mbps / 1000.0;
80 if gbps.fract().abs() < 0.05 {
81 format!("{gbps:.0}G")
82 } else {
83 format!("{gbps:.1}G")
84 }
85 } else {
86 format!("{mbps:.0}M")
87 }
88}
89
90pub fn truncate(text: &str, width: usize) -> String {
95 if text.width() <= width {
96 return text.to_owned();
97 }
98 if width == 0 {
99 return String::new();
100 }
101 let mut out = String::new();
103 let mut used = 0;
104 for ch in text.chars() {
105 let w = ch.width().unwrap_or(0);
106 if used + w > width - 1 {
107 break;
108 }
109 out.push(ch);
110 used += w;
111 }
112 out.push('…');
113 out
114}
115
116pub fn pad(text: &str, width: usize) -> String {
119 let mut out = truncate(text, width);
120 let used = out.width();
121 out.extend(std::iter::repeat_n(' ', width.saturating_sub(used)));
122 out
123}
124
125pub fn percent(value: Option<f32>) -> String {
128 value.map_or_else(|| " --%".to_owned(), |v| format!("{v:>3.0}%"))
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn percent_or_unknown() {
137 assert_eq!(percent(Some(7.4)), " 7%");
138 assert_eq!(percent(Some(100.0)), "100%");
139 assert_eq!(percent(None), " --%");
140 }
141 use unicode_width::UnicodeWidthStr;
142
143 #[test]
144 fn bytes_uses_binary_units_and_short_output() {
145 assert_eq!(bytes(0), "0B");
146 assert_eq!(bytes(1023), "1023B");
147 assert_eq!(bytes(1536), "1.5K");
148 assert_eq!(bytes(20 * 1024 * 1024), "20M");
149 assert_eq!(bytes(6_000_067_395_584), "5.5T");
150 }
151
152 #[test]
153 fn rate_appends_per_second() {
154 assert_eq!(rate(3_560_916.6), "3.4M/s");
155 assert_eq!(rate(-1.0), "0B/s");
156 }
157
158 #[test]
159 fn hours_and_minutes() {
160 assert_eq!(hours_minutes(59), "0m");
161 assert_eq!(hours_minutes(45 * 60), "45m");
162 assert_eq!(hours_minutes(3 * 3600 + 10 * 60 + 30), "3h10m");
163 }
164
165 #[test]
166 fn latency_keeps_three_digits() {
167 assert_eq!(latency(0.04), "0.0ms");
168 assert_eq!(latency(2.345), "2.3ms");
169 assert_eq!(latency(12.6), "13ms");
170 assert_eq!(latency(1420.0), "1.4s");
171 }
172
173 #[test]
174 fn duration_picks_the_largest_units() {
175 assert_eq!(duration(75), "01:15");
176 assert_eq!(duration(3 * 3600 + 5 * 60), "3h05m");
177 assert_eq!(duration(2 * 86_400 + 4 * 3600), "2d04h");
178 }
179
180 #[test]
181 fn age_picks_a_readable_unit() {
182 assert_eq!(age(3), "just now");
183 assert_eq!(age(45), "45s ago");
184 assert_eq!(age(8 * 60), "8m ago");
185 assert_eq!(age(5 * 3600), "5h ago");
186 assert_eq!(age(3 * 86_400), "3d ago");
187 }
188
189 #[test]
190 fn link_speed_uses_g_and_m() {
191 assert_eq!(link_speed(10_000.0), "10G");
192 assert_eq!(link_speed(2_500.0), "2.5G");
193 assert_eq!(link_speed(1_000.0), "1G");
194 assert_eq!(link_speed(576.4), "576M");
195 }
196
197 #[test]
198 fn wide_characters_count_as_two_columns() {
199 assert_eq!(truncate("イーサネット 3", 14), "イーサネット 3");
201 assert_eq!(truncate("イーサネット 3", 8), "イーサ…");
202 assert_eq!(pad("イーサネット 3", 16), "イーサネット 3 ");
203 assert_eq!(pad("enp9s0", 8), "enp9s0 ");
204 assert_eq!(pad("イーサネット", 5).width(), 5);
205 }
206
207 #[test]
208 fn truncate_marks_the_cut() {
209 assert_eq!(truncate("rigtop", 10), "rigtop");
210 assert_eq!(truncate("rigtop-gui", 6), "rigto…");
211 assert_eq!(truncate("abc", 0), "");
212 }
213}