1#![allow(
4 clippy::cast_precision_loss,
5 clippy::cast_possible_truncation,
6 clippy::cast_sign_loss
7)]
8
9use crate::common;
10use crate::progress::no_color;
11use crate::types::{Server, TestResult};
12use owo_colors::OwoColorize;
13
14use super::ratings::{
15 bufferbloat_colorized, bufferbloat_grade, colorize_rating, degradation_str, format_duration,
16 format_speed_colored, format_speed_plain, ping_rating, speed_rating_mbps,
17};
18
19fn build_speed_section(label: &str, speed_bps: f64, bytes: bool, nc: bool) -> String {
20 let speed = if nc {
21 format_speed_plain(speed_bps, bytes)
22 } else {
23 format_speed_colored(speed_bps, bytes)
24 };
25 let rating = colorize_rating(speed_rating_mbps(speed_bps / 1_000_000.0), nc);
26 if nc {
27 format!(" {label:>14}: {speed}")
28 } else {
29 format!(" {:>14}: {speed} {rating}", label.dimmed())
30 }
31}
32
33fn build_peak_line(peak_bps: f64, bytes: bool, nc: bool) -> String {
34 let peak = if nc {
35 format_speed_plain(peak_bps, bytes)
36 } else {
37 format_speed_colored(peak_bps, bytes)
38 };
39 if nc {
40 format!(" {:>14}: {peak}", "Peak (1s avg)")
41 } else {
42 format!(" {:>14}: {peak}", "Peak (1s avg)".dimmed())
43 }
44}
45
46fn build_latency_load_line(lat_load: f64, idle_ping: Option<f64>, nc: bool) -> String {
47 let degradation = degradation_str(lat_load, idle_ping, nc);
48 if nc {
49 format!(
50 " {:>14}: {:>8.1} ms{degradation}",
51 "Latency (load)", lat_load
52 )
53 } else {
54 format!(
55 " {:>14}: {}{degradation}",
56 "Latency (load)".dimmed(),
57 format!("{lat_load:.1} ms").yellow(),
58 )
59 }
60}
61
62pub fn build_latency_section(result: &TestResult, nc: bool) -> String {
63 let Some(ping) = result.ping else {
64 return String::new();
65 };
66
67 let mut lines = Vec::new();
68
69 let rating_str = colorize_rating(ping_rating(ping), nc);
70 if nc {
71 lines.push(format!(
72 " {:>14}: {:>8.1} ms ({rating_str})",
73 "Latency", ping
74 ));
75 } else {
76 lines.push(format!(
77 " {:>14}: {} {rating_str}",
78 "Latency".dimmed(),
79 format!("{ping:.1} ms").cyan().bold(),
80 ));
81 }
82
83 if let Some(jitter) = result.jitter {
84 if nc {
85 lines.push(format!(" {:>14}: {:>8.1} ms", "Jitter", jitter));
86 } else {
87 lines.push(format!(
88 " {:>14}: {}",
89 "Jitter".dimmed(),
90 format!("{jitter:.1} ms").cyan()
91 ));
92 }
93 }
94
95 if let Some(loss) = result.packet_loss {
96 let loss_color = if loss == 0.0 {
97 "green"
98 } else if loss < 1.0 {
99 "yellow"
100 } else {
101 "red"
102 };
103 let loss_str = format!("{loss:.1}%");
104 if nc {
105 lines.push(format!(" {:>14}: {:>8}", "Packet Loss", loss_str));
106 } else {
107 let display = if loss == 0.0 {
108 format!("{} {}", loss_str.green(), "✓".green())
109 } else {
110 match loss_color {
111 "green" => loss_str.green().to_string(),
112 "yellow" => loss_str.yellow().to_string(),
113 "red" => loss_str.red().bold().to_string(),
114 _ => loss_str.dimmed().to_string(),
115 }
116 };
117 lines.push(format!(" {:>14}: {display}", "Packet Loss".dimmed()));
118 }
119 }
120
121 if let (Some(lat_dl), Some(lat_ul)) = (result.latency_download, result.latency_upload) {
123 let max_load = lat_dl.max(lat_ul);
124 let (grade, added) = bufferbloat_grade(max_load, result.ping.unwrap_or(0.0));
125 let display = bufferbloat_colorized(grade, added, nc);
126 if nc {
127 lines.push(format!(" {:>14}: {:>12}", "Bufferbloat", display));
128 } else {
129 lines.push(format!(" {:>14}: {display}", "Bufferbloat".dimmed()));
130 }
131 }
132
133 lines.join("\n")
134}
135
136pub fn format_latency_section(result: &TestResult, nc: bool) {
137 let output = build_latency_section(result, nc);
138 if !output.is_empty() {
139 eprintln!("{output}");
140 }
141}
142
143pub fn build_download_section(result: &TestResult, bytes: bool, nc: bool) -> String {
144 let Some(dl) = result.download else {
145 return String::new();
146 };
147
148 let mut lines = Vec::new();
149 lines.push(build_speed_section("Download", dl, bytes, nc));
150
151 if let Some(peak) = result.download_peak {
152 lines.push(build_peak_line(peak, bytes, nc));
153 }
154
155 if let Some(lat_dl) = result.latency_download {
156 lines.push(build_latency_load_line(lat_dl, result.ping, nc));
157 }
158
159 lines.join("\n")
160}
161
162pub fn format_download_section(result: &TestResult, bytes: bool, nc: bool) {
163 let output = build_download_section(result, bytes, nc);
164 if !output.is_empty() {
165 eprintln!("{output}");
166 }
167}
168
169pub fn build_upload_section(result: &TestResult, bytes: bool, nc: bool) -> String {
170 let Some(ul) = result.upload else {
171 return String::new();
172 };
173
174 let mut lines = Vec::new();
175 lines.push(build_speed_section("Upload", ul, bytes, nc));
176
177 if let Some(peak) = result.upload_peak {
178 lines.push(build_peak_line(peak, bytes, nc));
179 }
180
181 if let Some(lat_ul) = result.latency_upload {
182 lines.push(build_latency_load_line(lat_ul, result.ping, nc));
183 }
184
185 if let (Some(dl), Some(ul)) = (result.download, result.upload) {
187 let ratio = if ul > 0.0 { dl / ul } else { f64::INFINITY };
188 let ratio_str = if nc {
189 format!("{ratio:.2}x")
190 } else {
191 let (color, label) = if ratio > 1.5 {
192 ("yellow", "download-heavy")
193 } else if ratio < 0.67 {
194 ("cyan", "upload-favored")
195 } else {
196 ("green", "balanced")
197 };
198 match color {
199 "green" => format!("{ratio:.2}x {label}").green().to_string(),
200 "yellow" => format!("{ratio:.2}x {label}").yellow().to_string(),
201 "cyan" => format!("{ratio:.2}x {label}").cyan().to_string(),
202 _ => format!("{ratio:.2}x {label}"),
203 }
204 };
205 if nc {
206 lines.push(format!(" {:>14}: {ratio_str}", "UL/DL Ratio"));
207 } else {
208 lines.push(format!(" {:>14}: {ratio_str}", "UL/DL Ratio".dimmed()));
209 }
210 }
211
212 lines.join("\n")
213}
214
215pub fn format_upload_section(result: &TestResult, bytes: bool, nc: bool) {
216 let output = build_upload_section(result, bytes, nc);
217 if !output.is_empty() {
218 eprintln!("{output}");
219 }
220}
221
222pub fn build_connection_info(result: &TestResult, nc: bool) -> String {
223 let dist = common::format_distance(result.server.distance);
224 let mut lines = Vec::new();
225
226 if nc {
227 lines.push(String::from("\n CONNECTION INFO"));
228 } else {
229 lines.push(format!("\n {}", "CONNECTION INFO".bold().underline()));
230 }
231
232 if nc {
233 lines.push(format!(
234 " {:>16}: {} ({})",
235 "Server", result.server.sponsor, result.server.name
236 ));
237 } else {
238 lines.push(format!(
239 " {:>16}: {} ({})",
240 "Server".dimmed(),
241 result.server.sponsor.white().bold(),
242 result.server.name
243 ));
244 }
245
246 if nc {
247 lines.push(format!(
248 " {:>16}: {} ({dist})",
249 "Location", result.server.country
250 ));
251 } else {
252 lines.push(format!(
253 " {:>16}: {} ({dist})",
254 "Location".dimmed(),
255 result.server.country,
256 ));
257 }
258
259 if let Some(ip) = &result.client_ip {
260 if nc {
261 lines.push(format!(" {:>16}: {ip}", "Client IP"));
262 } else {
263 lines.push(format!(" {:>16}: {ip}", "Client IP".dimmed()));
264 }
265 }
266
267 lines.join("\n")
268}
269
270pub fn format_connection_info(result: &TestResult, nc: bool) {
271 eprintln!("{}", build_connection_info(result, nc));
272}
273
274pub fn build_test_summary(
275 dl_bytes: u64,
276 ul_bytes: u64,
277 dl_duration: f64,
278 ul_duration: f64,
279 nc: bool,
280) -> String {
281 let mut lines = Vec::new();
282
283 if nc {
284 lines.push(String::from("\n TEST SUMMARY"));
285 } else {
286 lines.push(format!("\n {}", "TEST SUMMARY".bold().underline()));
287 }
288
289 if dl_bytes > 0 {
290 lines.push(format!(
291 " {:>14}: {} in {}",
292 "Download",
293 common::format_data_size(dl_bytes),
294 format_duration(dl_duration)
295 ));
296 }
297 if ul_bytes > 0 {
298 lines.push(format!(
299 " {:>14}: {} in {}",
300 "Upload",
301 common::format_data_size(ul_bytes),
302 format_duration(ul_duration)
303 ));
304 }
305 let total = dl_bytes + ul_bytes;
306 let total_dur = dl_duration + ul_duration;
307 if total > 0 {
308 lines.push(format!(
309 " {:>14}: {} in {}",
310 "Total",
311 common::format_data_size(total),
312 format_duration(total_dur)
313 ));
314 }
315
316 lines.join("\n")
317}
318
319pub fn format_test_summary(
320 dl_bytes: u64,
321 ul_bytes: u64,
322 dl_duration: f64,
323 ul_duration: f64,
324 nc: bool,
325) {
326 eprintln!(
327 "{}",
328 build_test_summary(dl_bytes, ul_bytes, dl_duration, ul_duration, nc)
329 );
330}
331
332pub fn build_footer(timestamp: &str, nc: bool) -> String {
333 if nc {
334 format!("\n Completed at: {timestamp}")
335 } else {
336 format!(
337 "\n {} {}",
338 "Completed at:".dimmed(),
339 timestamp.bright_black()
340 )
341 }
342}
343
344pub fn format_footer(timestamp: &str, nc: bool) {
345 eprintln!("{}", build_footer(timestamp, nc));
346}
347
348pub fn build_list(servers: &[Server]) -> String {
350 let nc = no_color();
351
352 let (max_id_len, max_sponsor_len, max_name_len) =
353 servers
354 .iter()
355 .fold((3, 7, 24), |(max_id, max_sponsor, max_name), s| {
356 let name_len = s.name.len() + s.country.len() + 3;
357 (
358 max_id.max(s.id.len()),
359 max_sponsor.max(s.sponsor.len()),
360 max_name.max(name_len),
361 )
362 });
363
364 let idw = max_id_len.max(3);
365 let sw = max_sponsor_len.max(7);
366 let nw = max_name_len.max(24);
367
368 let mut lines = Vec::new();
369
370 if nc {
371 lines.push(String::from("\n AVAILABLE SERVERS"));
372 } else {
373 lines.push(format!("\n {}", "AVAILABLE SERVERS".bold().underline()));
374 }
375
376 if nc {
377 lines.push(format!(
378 " {:<idw$} {:<sw$} {:<nw$} {:>10}",
379 "ID", "Sponsor", "Name (Country)", "Distance"
380 ));
381 } else {
382 lines.push(format!(
383 " {:<idw$} {:<sw$} {:<nw$} {:>10}",
384 "ID".dimmed(),
385 "Sponsor".dimmed(),
386 "Name (Country)".dimmed(),
387 "Distance".dimmed()
388 ));
389 }
390
391 if nc {
392 lines.push(format!(
393 " {:->idw$} {:->sw$} {:->nw$} {:->10}",
394 "", "", "", ""
395 ));
396 } else {
397 lines.push(format!(
398 " {:->idw$} {:->sw$} {:->nw$} {:->10}",
399 "",
400 "",
401 "",
402 "".dimmed()
403 ));
404 }
405
406 for server in servers {
407 let dist = common::format_distance(server.distance);
408 if nc {
409 lines.push(format!(
410 " {:<idw$} {:<sw$} {:<24} {:>10}",
411 server.id,
412 server.sponsor,
413 format!("{} ({})", server.name, server.country),
414 dist,
415 ));
416 } else {
417 lines.push(format!(
418 " {:<idw$} {:<sw$} {:<24} {:>10}",
419 server.id,
420 server.sponsor.white().bold(),
421 format!("{} ({})", server.name, server.country),
422 dist.bright_black(),
423 ));
424 }
425 }
426
427 lines.join("\n")
428}
429
430pub fn format_list(servers: &[Server]) -> Result<(), std::io::Error> {
437 eprintln!("{}", build_list(servers));
438 Ok(())
439}