Skip to main content

cli_speedtest/
theme.rs

1// src/theme.rs
2
3use crate::models::AppConfig;
4use owo_colors::OwoColorize;
5
6/// Returns the value formatted and ANSI-colored for speed (Mbps).
7pub fn color_speed(mbps: f64, config: &AppConfig) -> String {
8    let s = format!("{:.2} Mbps", mbps);
9    if !config.color {
10        return s;
11    }
12
13    if mbps >= 100.0 {
14        s.green().to_string()
15    } else if mbps >= 25.0 {
16        s.yellow().to_string()
17    } else {
18        s.red().to_string()
19    }
20}
21
22/// Returns the value formatted and ANSI-colored for ping (ms).
23pub fn color_ping(ms: f64, config: &AppConfig) -> String {
24    let s = format!("{:.1} ms", ms);
25    if !config.color {
26        return s;
27    }
28
29    if ms <= 20.0 {
30        s.green().to_string()
31    } else if ms <= 80.0 {
32        s.yellow().to_string()
33    } else {
34        s.red().to_string()
35    }
36}
37
38/// Returns the value formatted and ANSI-colored for jitter (ms).
39pub fn color_jitter(ms: f64, config: &AppConfig) -> String {
40    let s = format!("{:.2} ms", ms);
41    if !config.color {
42        return s;
43    }
44
45    if ms <= 5.0 {
46        s.green().to_string()
47    } else if ms <= 20.0 {
48        s.yellow().to_string()
49    } else {
50        s.red().to_string()
51    }
52}
53
54/// Returns the value formatted and ANSI-colored for packet loss (%).
55pub fn color_loss(pct: f64, config: &AppConfig) -> String {
56    let s = format!("{:.1}%", pct);
57    if !config.color {
58        return s;
59    }
60
61    if pct == 0.0 {
62        s.green().to_string()
63    } else {
64        s.red().to_string()
65    }
66}
67
68/// Returns a short rating label for a given Mbps value.
69pub fn speed_rating(mbps: f64, config: &AppConfig) -> String {
70    let label = if mbps >= 500.0 {
71        "Excellent"
72    } else if mbps >= 100.0 {
73        "Great"
74    } else if mbps >= 25.0 {
75        "Good"
76    } else if mbps >= 5.0 {
77        "Fair"
78    } else {
79        "Poor"
80    };
81
82    if !config.color {
83        return label.to_string();
84    }
85
86    if mbps >= 100.0 {
87        label.green().to_string()
88    } else if mbps >= 25.0 {
89        label.yellow().to_string()
90    } else {
91        label.red().to_string()
92    }
93}
94
95/// Returns the visible (printed) length of a string by stripping ANSI codes first.
96/// Uses console::measure_text_width which correctly handles multi-column Unicode characters.
97pub fn visible_len(s: &str) -> usize {
98    console::measure_text_width(s)
99}
100
101/// Right-pads `s` with spaces so its *visible* width equals `width`.
102/// If the visible length already meets or exceeds `width`, returns `s` unchanged.
103pub fn pad_to(s: &str, width: usize) -> String {
104    let vlen = visible_len(s);
105    if vlen >= width {
106        s.to_string()
107    } else {
108        format!("{}{}", s, " ".repeat(width - vlen))
109    }
110}
111
112/// Truncates a string to a max length, appending an ellipsis if truncated.
113pub fn truncate_to(s: &str, max_len: usize) -> String {
114    if s.len() <= max_len {
115        s.to_string()
116    } else {
117        format!("{}...", &s[..max_len - 1])
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn mock_config(color: bool) -> AppConfig {
126        AppConfig {
127            quiet: false,
128            color,
129        }
130    }
131
132    #[test]
133    fn color_speed_green() {
134        let res = color_speed(150.0, &mock_config(true));
135        assert!(res.contains("\x1b[32m")); // ANSI green
136        assert!(res.contains("150.00 Mbps"));
137    }
138
139    #[test]
140    fn color_speed_plain_when_no_color() {
141        let res = color_speed(150.0, &mock_config(false));
142        assert!(!res.contains("\x1b"));
143        assert_eq!(res, "150.00 Mbps");
144    }
145
146    #[test]
147    fn speed_rating_boundaries() {
148        let c = mock_config(false);
149        assert_eq!(speed_rating(500.0, &c), "Excellent");
150        assert_eq!(speed_rating(100.0, &c), "Great");
151        assert_eq!(speed_rating(25.0, &c), "Good");
152        assert_eq!(speed_rating(5.0, &c), "Fair");
153        assert_eq!(speed_rating(4.9, &c), "Poor");
154    }
155
156    #[test]
157    fn truncate_to_short_string() {
158        assert_eq!(truncate_to("short", 10), "short");
159        assert_eq!(truncate_to("exact", 5), "exact");
160    }
161
162    #[test]
163    fn truncate_to_long_string() {
164        // "long string" (11 chars) truncated to 5 -> "long..."
165        assert_eq!(truncate_to("long string", 5), "long...");
166    }
167
168    #[test]
169    fn visible_len_plain_string() {
170        assert_eq!(visible_len("hello"), 5);
171    }
172
173    #[test]
174    fn visible_len_colored_string() {
175        assert_eq!(visible_len("\x1b[32m401.74\x1b[0m"), 6);
176    }
177
178    #[test]
179    fn pad_to_short_string_pads_correctly() {
180        assert_eq!(pad_to("hi", 5), "hi   ");
181    }
182
183    #[test]
184    fn pad_to_colored_string_pads_to_visible_width() {
185        let colored = "\x1b[32m401.74\x1b[0m";
186        let padded = pad_to(colored, 10);
187        assert_eq!(visible_len(&padded), 10);
188        assert!(padded.starts_with(colored));
189    }
190
191    #[test]
192    fn pad_to_already_at_width_unchanged() {
193        assert_eq!(pad_to("hello", 5), "hello");
194    }
195
196    #[test]
197    fn pad_to_over_width_unchanged() {
198        assert_eq!(pad_to("toolong", 4), "toolong");
199    }
200}