1pub mod client;
4pub mod menu;
5pub mod models;
6pub mod theme;
7pub mod utils;
8
9use chrono::Utc;
10use models::{AppConfig, RunArgs, Server, SpeedTestResult};
11use reqwest::Client;
12use std::sync::Arc;
13use utils::WARMUP_SECS;
14
15const DEFAULT_SERVER_URL: &str = "https://speed.cloudflare.com";
16
17pub async fn run(
20 args: RunArgs,
21 config: Arc<AppConfig>,
22 client: Client,
23) -> anyhow::Result<SpeedTestResult> {
24 if args.duration_secs <= WARMUP_SECS as u64 {
25 anyhow::bail!(
26 "Duration must be greater than {} seconds (warm-up period). Got: {}s",
27 WARMUP_SECS,
28 args.duration_secs
29 );
30 }
31
32 if args.ping_count == 0 {
33 anyhow::bail!("--ping-count must be at least 1");
34 }
35
36 let server = Server {
39 name: if args.server_url == DEFAULT_SERVER_URL {
40 "Cloudflare".into()
41 } else {
42 args.server_url.clone()
43 },
44 base_url: args.server_url.clone(),
45 };
46
47 if !config.quiet {
48 println!("🔍 Using server: {}\n", server.name);
49 }
50
51 let ping_stats = client::test_ping_stats(
53 &client,
54 &server.base_url,
55 args.ping_count,
56 Arc::clone(&config),
57 )
58 .await?;
59
60 let down_speed: Option<f64> = if args.no_download {
62 if !config.quiet {
63 println!("⬇️ Download: skipped\n");
64 }
65 None
66 } else {
67 let conns = args.connections.unwrap_or(8);
68 let speed = client::test_download(
69 &client,
70 &server.base_url,
71 args.duration_secs,
72 conns,
73 Arc::clone(&config),
74 )
75 .await?;
76 if !config.quiet {
77 println!("⬇️ Download Speed: {:.2} Mbps\n", speed);
78 }
79 Some(speed)
80 };
81
82 let up_speed: Option<f64> = if args.no_upload {
84 if !config.quiet {
85 println!("⬆️ Upload: skipped\n");
86 }
87 None
88 } else {
89 let conns = args.connections.unwrap_or(4);
90 let speed = client::test_upload(
91 &client,
92 &server.base_url,
93 args.duration_secs,
94 conns,
95 Arc::clone(&config),
96 )
97 .await?;
98 if !config.quiet {
99 println!("⬆️ Upload Speed: {:.2} Mbps\n", speed);
100 }
101 Some(speed)
102 };
103
104 if !config.quiet {
106 let term_cols = console::Term::stdout().size().1 as usize;
107 let box_width = term_cols.saturating_sub(4).clamp(44, 60);
108 let inner_width = box_width - 2;
109
110 println!("╔{}╗", "═".repeat(inner_width));
111 println!("║{:^width$}║", "📊 Test Summary", width = inner_width);
112 println!("╠{}╣", "═".repeat(inner_width));
113
114 let server_label = " Server : ";
116 let server_val_width = inner_width - server_label.len() - 1;
117 let truncated_server = theme::truncate_to(&server.name, server_val_width);
118 println!(
119 "║{}{} ║",
120 server_label,
121 theme::pad_to(&truncated_server, server_val_width),
122 );
123
124 println!("╠{}╣", "═".repeat(inner_width));
125
126 let labels = [
128 (
129 " Ping : ",
130 theme::color_ping(ping_stats.avg_ms, &config),
131 ),
132 (
133 " Jitter : ",
134 theme::color_jitter(ping_stats.jitter_ms, &config),
135 ),
136 (" Min Ping : ", format!("{} ms", ping_stats.min_ms)),
137 (" Max Ping : ", format!("{} ms", ping_stats.max_ms)),
138 (
139 " Packet Loss: ",
140 theme::color_loss(ping_stats.packet_loss_pct, &config),
141 ),
142 ];
143
144 for (label, val) in labels {
145 let val_width = inner_width - label.len() - 1;
146 println!("║{}{} ║", label, theme::pad_to(&val, val_width));
147 }
148
149 println!("╠{}╣", "═".repeat(inner_width));
150
151 match down_speed {
153 Some(s) => {
154 let label = " Download : ";
155 let speed_str = theme::color_speed(s, &config);
156 let rating = theme::speed_rating(s, &config);
157 let combined = format!("{} {}", speed_str, rating);
158 let val_width = inner_width - label.len() - 1;
159 println!("║{}{} ║", label, theme::pad_to(&combined, val_width));
160 }
161 None => {
162 let label = " Download : ";
163 let val_width = inner_width - label.len() - 1;
164 println!("║{}{} ║", label, theme::pad_to("skipped", val_width));
165 }
166 }
167
168 match up_speed {
170 Some(s) => {
171 let label = " Upload : ";
172 let speed_str = theme::color_speed(s, &config);
173 let rating = theme::speed_rating(s, &config);
174 let combined = format!("{} {}", speed_str, rating);
175 let val_width = inner_width - label.len() - 1;
176 println!("║{}{} ║", label, theme::pad_to(&combined, val_width));
177 }
178 None => {
179 let label = " Upload : ";
180 let val_width = inner_width - label.len() - 1;
181 println!("║{}{} ║", label, theme::pad_to("skipped", val_width));
182 }
183 }
184
185 println!("╚{}╝", "═".repeat(inner_width));
186 }
187
188 Ok(SpeedTestResult {
189 timestamp: Utc::now().to_rfc3339(),
190 version: env!("CARGO_PKG_VERSION").to_string(),
191 server_name: server.name,
192 ping: ping_stats,
193 download_mbps: down_speed,
194 upload_mbps: up_speed,
195 })
196}