1use crate::models::{AppConfig, MenuSettings, RunArgs};
4use crate::theme::{pad_to, speed_rating};
5use dialoguer::Select;
6use dialoguer::theme::ColorfulTheme;
7use reqwest::Client;
8use std::sync::Arc;
9
10const DEFAULT_SERVER_URL: &str = "https://speed.cloudflare.com";
11
12const ASCII_ART: &str = r#"
13 ██████╗██╗ ██╗ ███████╗██████╗ ███████╗███████╗██████╗ ████████╗███████╗███████╗████████╗
14██╔════╝██║ ██║ ██╔════╝██╔══██╗██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝
15██║ ██║ ██║ ███████╗██████╔╝█████╗ █████╗ ██║ ██║ ██║ █████╗ ███████╗ ██║
16██║ ██║ ██║ ╚════██║██╔═══╝ ██╔══╝ ██╔══╝ ██║ ██║ ██║ ██╔══╝ ╚════██║ ██║
17╚██████╗███████╗██║ ███████║██║ ███████╗███████╗██████╔╝ ██║ ███████╗███████║ ██║
18 ╚═════╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚══════╝╚══════╝╚═════╝ ╚═╝ ╚══════╝╚══════╝ ╚═╝
19"#;
20
21const ASCII_ART_COMPACT: &str = " CLI SPEEDTEST • v0.1.0";
22
23pub async fn run_menu(config: Arc<AppConfig>, client: Client) -> anyhow::Result<()> {
24 let mut settings = MenuSettings::default();
25
26 loop {
27 print_welcome(&config);
28
29 let options = &[
30 "🚀 Start Full Speed Test",
31 "📡 Quick Ping Only",
32 "⚙️ Settings",
33 "📋 View Commands",
34 "❓ Help",
35 "🚪 Exit",
36 ];
37
38 let selection = Select::with_theme(&ColorfulTheme::default())
39 .with_prompt("Main Menu")
40 .items(options)
41 .default(0)
42 .interact_opt()?;
43
44 match selection {
45 Some(0) => run_full_test(&settings, &config, &client).await?,
46 Some(1) => run_quick_ping(&settings, &config, &client).await?,
47 Some(2) => show_settings(&mut settings, &config)?,
48 Some(3) => show_commands(&config),
49 Some(4) => show_help(&config),
50 Some(5) | None => {
51 clear_screen();
52 break;
53 }
54 _ => unreachable!(),
55 }
56 }
57
58 Ok(())
59}
60
61fn print_welcome(_config: &AppConfig) {
62 clear_screen();
63 let term_width = console::Term::stdout().size().1 as usize;
64
65 if term_width >= 95 {
66 println!("{}", ASCII_ART);
67 } else {
68 println!("\n{}\n", ASCII_ART_COMPACT);
69 }
70
71 println!(" A blazing fast network speed tester — written in Rust");
72 println!(
73 " v{} • Cloudflare backend • github.com/nazakun021/cli-speedtest\n",
74 env!("CARGO_PKG_VERSION")
75 );
76}
77
78async fn run_full_test(
79 settings: &MenuSettings,
80 config: &AppConfig,
81 client: &Client,
82) -> anyhow::Result<()> {
83 clear_screen();
84 let run_args = RunArgs::from(settings);
85 let app_config = Arc::new(AppConfig {
86 quiet: config.quiet,
87 color: settings.color,
88 });
89
90 crate::run(run_args, app_config, client.clone()).await?;
91
92 println!("\n Press Enter to return to menu…");
93 wait_for_enter();
94 Ok(())
95}
96
97async fn run_quick_ping(
98 settings: &MenuSettings,
99 config: &AppConfig,
100 client: &Client,
101) -> anyhow::Result<()> {
102 clear_screen();
103 println!("📡 Running Quick Ping...\n");
104
105 let app_config = Arc::new(AppConfig {
106 quiet: config.quiet,
107 color: settings.color,
108 });
109
110 crate::client::test_ping_stats(client, DEFAULT_SERVER_URL, settings.ping_count, app_config)
111 .await?;
112
113 println!("\n Press Enter to return to menu…");
114 wait_for_enter();
115 Ok(())
116}
117
118fn show_settings(settings: &mut MenuSettings, _config: &AppConfig) -> anyhow::Result<()> {
119 loop {
120 clear_screen();
121 println!(" ⚙️ Settings\n");
122 println!(" ───────────────────────────────");
123
124 let options = &[
125 format!("Test Duration : {}s", settings.duration_secs),
126 format!("Parallel Connections : {}", settings.connections),
127 format!("Ping Probe Count : {}", settings.ping_count),
128 format!(
129 "Color Output : {}",
130 if settings.color { "On" } else { "Off" }
131 ),
132 "↩ Back to Main Menu".to_string(),
133 ];
134
135 let selection = Select::with_theme(&ColorfulTheme::default())
136 .items(options)
137 .default(4)
138 .interact_opt()?;
139
140 match selection {
141 Some(0) => {
142 let durations = &[5, 10, 15, 20, 30];
143 let idx = Select::with_theme(&ColorfulTheme::default())
144 .with_prompt("Select Duration")
145 .items(durations)
146 .default(1)
147 .interact()?;
148 settings.duration_secs = durations[idx] as u64;
149 }
150 Some(1) => {
151 let connections = &[2, 4, 6, 8, 12, 16];
152 let idx = Select::with_theme(&ColorfulTheme::default())
153 .with_prompt("Select Parallel Connections")
154 .items(connections)
155 .default(3)
156 .interact()?;
157 settings.connections = connections[idx];
158 }
159 Some(2) => {
160 let counts = &[5, 10, 20, 30, 50];
161 let idx = Select::with_theme(&ColorfulTheme::default())
162 .with_prompt("Select Ping Count")
163 .items(counts)
164 .default(2)
165 .interact()?;
166 settings.ping_count = counts[idx];
167 }
168 Some(3) => {
169 let idx = Select::with_theme(&ColorfulTheme::default())
170 .with_prompt("Color Output")
171 .items(&["On", "Off"])
172 .default(if settings.color { 0 } else { 1 })
173 .interact()?;
174 settings.color = idx == 0;
175 }
176 Some(4) | None => break,
177 _ => unreachable!(),
178 }
179 }
180 Ok(())
181}
182
183fn show_commands(_config: &AppConfig) {
184 clear_screen();
185 let w = 58;
186 let inner_w = w - 2;
187 println!(" ┌{}┐", "─".repeat(w));
188 println!(" │ {} │", pad_to("📋 Available Commands", inner_w));
189 println!(" ├{}┤", "─".repeat(w));
190 println!(
191 " │ {} │",
192 pad_to(
193 "-d, --duration <SECS> Test duration (default: 10)",
194 inner_w
195 )
196 );
197 println!(
198 " │ {} │",
199 pad_to("-c, --connections <N> Parallel connections", inner_w)
200 );
201 println!(
202 " │ {} │",
203 pad_to(
204 " --server <URL> Custom server base URL",
205 inner_w
206 )
207 );
208 println!(
209 " │ {} │",
210 pad_to(" --no-download Skip download test", inner_w)
211 );
212 println!(
213 " │ {} │",
214 pad_to(" --no-upload Skip upload test", inner_w)
215 );
216 println!(
217 " │ {} │",
218 pad_to(
219 " --ping-count <N> Ping probes (default: 20)",
220 inner_w
221 )
222 );
223 println!(
224 " │ {} │",
225 pad_to(
226 " --json Output results as JSON",
227 inner_w
228 )
229 );
230 println!(
231 " │ {} │",
232 pad_to(" --no-color Disable color output", inner_w)
233 );
234 println!(
235 " │ {} │",
236 pad_to(" --debug Enable debug logging", inner_w)
237 );
238 println!(" ├{}┤", "─".repeat(w));
239 println!(
240 " │ {} │",
241 pad_to(
242 "Example: cli-speedtest --duration 20 --connections 12",
243 inner_w
244 )
245 );
246 println!(
247 " │ {} │",
248 pad_to(
249 "Example: cli-speedtest --json | jq .download_mbps",
250 inner_w
251 )
252 );
253 println!(" └{}┘", "─".repeat(w));
254 println!("\n Press Enter to return…");
255 wait_for_enter();
256}
257
258fn show_help(config: &AppConfig) {
259 clear_screen();
260 let mock_conf = AppConfig {
261 quiet: false,
262 color: config.color,
263 };
264
265 let w = 58;
266 let inner_w = w - 2;
267 println!(" ┌{}┐", "─".repeat(w));
268 println!(" │ {} │", pad_to("❓ Interpreting Your Results", inner_w));
269 println!(" ├{}┤", "─".repeat(w));
270 println!(" │ {} │", pad_to("SPEED", inner_w));
271 println!(
272 " │ {} │",
273 pad_to(
274 &format!(
275 " ≥ 500 Mbps {} — fiber / high-end cable",
276 speed_rating(500.0, &mock_conf)
277 ),
278 inner_w
279 )
280 );
281 println!(
282 " │ {} │",
283 pad_to(
284 &format!(
285 " 100–499 {} — HD streaming, fast downloads",
286 speed_rating(100.0, &mock_conf)
287 ),
288 inner_w
289 )
290 );
291 println!(
292 " │ {} │",
293 pad_to(
294 &format!(
295 " 25–99 {} — video calls, light streaming",
296 speed_rating(25.0, &mock_conf)
297 ),
298 inner_w
299 )
300 );
301 println!(
302 " │ {} │",
303 pad_to(
304 &format!(
305 " 5–24 {} — basic browsing, email",
306 speed_rating(5.0, &mock_conf)
307 ),
308 inner_w
309 )
310 );
311 println!(
312 " │ {} │",
313 pad_to(
314 &format!(
315 " < 5 {} — may struggle with modern web",
316 speed_rating(0.0, &mock_conf)
317 ),
318 inner_w
319 )
320 );
321 println!(" ├{}┤", "─".repeat(w));
322 println!(" │ {} │", pad_to("PING", inner_w));
323 println!(
324 " │ {} │",
325 pad_to(" ≤ 20 ms Excellent — real-time gaming, VoIP", inner_w)
326 );
327 println!(
328 " │ {} │",
329 pad_to(" 21–80 ms Good — video calls, general use", inner_w)
330 );
331 println!(
332 " │ {} │",
333 pad_to(
334 " > 80 ms High — noticeable in latency-sensitive",
335 inner_w
336 )
337 );
338 println!(" │ {} │", pad_to(" applications", inner_w));
339 println!(" ├{}┤", "─".repeat(w));
340 println!(" │ {} │", pad_to("JITTER (variation in ping)", inner_w));
341 println!(
342 " │ {} │",
343 pad_to(" ≤ 5 ms Stable — voice/video calls unaffected", inner_w)
344 );
345 println!(
346 " │ {} │",
347 pad_to(
348 " 6–20 ms Moderate — occasional stutter possible",
349 inner_w
350 )
351 );
352 println!(
353 " │ {} │",
354 pad_to(
355 " > 20 ms Unstable — real-time apps will be impacted",
356 inner_w
357 )
358 );
359 println!(" ├{}┤", "─".repeat(w));
360 println!(" │ {} │", pad_to("PACKET LOSS", inner_w));
361 println!(
362 " │ {} │",
363 pad_to(" 0.0% Ideal — no retransmission overhead", inner_w)
364 );
365 println!(
366 " │ {} │",
367 pad_to(
368 " > 0.0% Lossy — investigate ISP or local network",
369 inner_w
370 )
371 );
372 println!(" └{}┘", "─".repeat(w));
373
374 println!("\n Press Enter to return…");
375 wait_for_enter();
376}
377
378fn clear_screen() {
379 print!("\x1b[2J\x1b[H");
380}
381
382fn wait_for_enter() {
383 use std::io::{self, BufRead};
384 let mut _line = String::new();
385 let _ = io::stdin().lock().read_line(&mut _line);
386}
387
388impl From<&MenuSettings> for RunArgs {
389 fn from(s: &MenuSettings) -> Self {
390 RunArgs {
391 server_url: DEFAULT_SERVER_URL.to_string(),
392 duration_secs: s.duration_secs,
393 connections: Some(s.connections),
394 ping_count: s.ping_count,
395 no_download: false,
396 no_upload: false,
397 }
398 }
399}