1use crate::config::BoxChars;
2use crate::render::table::ReportBuilder;
3use crate::VERSION;
4
5use indicatif::{ProgressBar, ProgressStyle};
6use owo_colors::OwoColorize;
7
8use super::{format_bytes, format_mbps, SpeedTestResult};
9
10pub struct SpeedQXDisplay {
12 use_ascii: bool,
13 use_colors: bool,
14 json_mode: bool,
15}
16
17impl SpeedQXDisplay {
18 pub fn new(use_ascii: bool, use_colors: bool, json_mode: bool) -> Self {
19 Self {
20 use_ascii,
21 use_colors,
22 json_mode,
23 }
24 }
25
26 pub fn print_header(&self) {
28 if self.json_mode {
29 return;
30 }
31 println!();
32 if self.use_colors {
33 println!(
34 " {} - Internet Speed Test",
35 format!("SpeedQX v{}", VERSION).cyan().bold()
36 );
37 println!(" {}", "QubeTX Developer Tools".dimmed());
38 } else {
39 println!(" SpeedQX v{} - Internet Speed Test", VERSION);
40 println!(" QubeTX Developer Tools");
41 }
42 println!();
43 }
44
45 pub fn create_spinner(&self, step: u32, total: u32, msg: &str) -> ProgressBar {
47 if self.json_mode {
48 return ProgressBar::hidden();
49 }
50 let pb = ProgressBar::new_spinner();
51 let template = format!(" [{{spinner}}] [{}/{}] {{msg}}", step, total);
52 pb.set_style(
53 ProgressStyle::default_spinner()
54 .template(&template)
55 .unwrap_or_else(|_| ProgressStyle::default_spinner()),
56 );
57 pb.set_message(msg.to_string());
58 pb.enable_steady_tick(std::time::Duration::from_millis(80));
59 pb
60 }
61
62 pub fn create_progress_bar(&self, step: u32, total: u32, msg: &str) -> ProgressBar {
64 if self.json_mode {
65 return ProgressBar::hidden();
66 }
67 let pb = ProgressBar::new(100);
68 let template = format!(" [{{bar:20.cyan/dim}}] [{}/{}] {{msg}}", step, total);
69 pb.set_style(
70 ProgressStyle::default_bar()
71 .template(&template)
72 .unwrap_or_else(|_| ProgressStyle::default_bar())
73 .progress_chars("##-"),
74 );
75 pb.set_message(msg.to_string());
76 pb
77 }
78
79 pub fn finish_step(&self, step: u32, total: u32, msg: &str) {
81 if self.json_mode {
82 return;
83 }
84 let check = if self.use_ascii { "[OK]" } else { "\u{2713}" };
85 if self.use_colors {
86 println!(" [{}/{}] {} {}", step, total, check.green(), msg,);
87 } else {
88 println!(" [{}/{}] {} {}", step, total, check, msg);
89 }
90 }
91}
92
93pub fn render_results(result: &SpeedTestResult, use_ascii: bool, use_colors: bool) -> String {
95 let chars = if use_ascii {
96 BoxChars::ascii()
97 } else {
98 BoxChars::unicode()
99 };
100
101 let label_width = 14;
102 let data_width = 27;
103
104 let single_provider = result.providers.len() == 1;
105
106 if single_provider {
107 return render_single_provider(result, chars, label_width, data_width, use_colors);
108 }
109
110 render_multi_provider(result, chars, label_width, data_width, use_colors)
111}
112
113fn render_multi_provider(
114 result: &SpeedTestResult,
115 chars: BoxChars,
116 label_width: usize,
117 data_width: usize,
118 _use_colors: bool,
119) -> String {
120 let mut builder = ReportBuilder::new(label_width, data_width, chars);
121
122 builder = builder.full_top_border().span_row(&format!(
124 " {:^width$}",
125 "SPEEDQX RESULTS",
126 width = label_width + data_width + 3
127 ));
128
129 builder = builder.section_header("Averaged Results");
130
131 if let Some(ping) = result.ping_ms {
133 builder = builder.row("Ping", &format!("{:.1} ms", ping));
134 }
135
136 if let Some(jitter) = result.jitter_ms {
138 builder = builder.row("Jitter", &format!("{:.1} ms", jitter));
139 }
140
141 builder = builder.row(
143 "Download",
144 &format!("{} (avg)", format_mbps(result.download_mbps)),
145 );
146 builder = builder.row(
147 "Upload",
148 &format!("{} (avg)", format_mbps(result.upload_mbps)),
149 );
150
151 if let Some(loss) = result.packet_loss_pct {
153 builder = builder.row("Packet Loss", &format!("{}%", loss));
154 }
155
156 builder = builder.row("Duration", &format!("{:.1}s", result.duration_s));
158
159 if let Some(ref stability) = result.stability {
161 let dl_label = if stability.download_stable {
162 "Stable"
163 } else {
164 "Variable"
165 };
166 let ul_label = if stability.upload_stable {
167 "Stable"
168 } else {
169 "Variable"
170 };
171 builder = builder.row(
172 "Stability",
173 &format!(
174 "DL: {} (CV {:.0}%) / UL: {} (CV {:.0}%)",
175 dl_label,
176 stability.download_cv * 100.0,
177 ul_label,
178 stability.upload_cv * 100.0,
179 ),
180 );
181 }
182
183 if let Some(ref div) = result.provider_divergence {
185 if div.significant {
186 builder = builder.row(
187 "Divergence",
188 &format!(
189 "DL {:.0}% / UL {:.0}% (significant)",
190 div.download * 100.0,
191 div.upload * 100.0,
192 ),
193 );
194 }
195 }
196
197 for provider in &result.providers {
199 builder = render_provider_section(builder, provider);
200 }
201
202 let mut output = builder.finish();
203 output.push('\n');
204 output
205}
206
207fn render_single_provider(
208 result: &SpeedTestResult,
209 chars: BoxChars,
210 label_width: usize,
211 data_width: usize,
212 _use_colors: bool,
213) -> String {
214 let mut builder = ReportBuilder::new(label_width, data_width, chars);
215
216 builder = builder.full_top_border().span_row(&format!(
217 " {:^width$}",
218 "SPEEDQX RESULTS",
219 width = label_width + data_width + 3
220 ));
221
222 if let Some(provider) = result.providers.first() {
223 builder = builder.section_header(&provider.provider);
224
225 builder = builder.row("Server", &provider.server);
227
228 if let Some(ref loc) = provider.location {
230 builder = builder.row("Location", loc);
231 }
232
233 if let Some(ping) = provider.ping_ms {
235 builder = builder.row("Ping", &format!("{:.1} ms", ping));
236 }
237
238 if let Some(jitter) = provider.jitter_ms {
240 builder = builder.row("Jitter", &format!("{:.1} ms", jitter));
241 }
242
243 if let Some(dl) = provider.download_mbps {
245 builder = builder.row("Download", &format_mbps(dl));
246 }
247 if let Some(ul) = provider.upload_mbps {
248 builder = builder.row("Upload", &format_mbps(ul));
249 }
250
251 builder = builder.row("DL Data", &format_bytes(provider.download_bytes));
253 builder = builder.row("UL Data", &format_bytes(provider.upload_bytes));
254
255 if let Some(loss) = provider.packet_loss_pct {
257 builder = builder.row("Packet Loss", &format!("{}%", loss));
258 }
259
260 builder = builder.row("Duration", &format!("{:.1}s", result.duration_s));
262 }
263
264 let mut output = builder.finish();
265 output.push('\n');
266 output
267}
268
269fn render_provider_section(
270 builder: ReportBuilder,
271 provider: &super::ProviderResult,
272) -> ReportBuilder {
273 let mut b = builder.section_header(&provider.provider);
274
275 if let Some(ref err) = provider.error {
277 b = b.row("Error", err);
278 return b;
279 }
280
281 b = b.row("Server", &provider.server);
283
284 if let Some(ref loc) = provider.location {
286 b = b.row("Location", loc);
287 }
288
289 if let Some(ping) = provider.ping_ms {
291 b = b.row("Ping", &format!("{:.1} ms", ping));
292 }
293
294 if let Some(jitter) = provider.jitter_ms {
296 b = b.row("Jitter", &format!("{:.1} ms", jitter));
297 }
298
299 if let Some(dl) = provider.download_mbps {
301 b = b.row("Download", &format_mbps(dl));
302 }
303 if let Some(ul) = provider.upload_mbps {
304 b = b.row("Upload", &format_mbps(ul));
305 }
306
307 b = b.row("DL Data", &format_bytes(provider.download_bytes));
309 b = b.row("UL Data", &format_bytes(provider.upload_bytes));
310
311 b
312}