Skip to main content

cli_speedtest/
menu.rs

1// src/menu.rs
2
3use 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}