use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::config::Config;
use crate::network::{self, providers::Provider, SpeedTestResult};
use crate::storage;
#[derive(Parser)]
#[command(name = "speedtest-tui")]
#[command(author, version, about = "A beautiful TUI network speed test tool", long_about = None)]
pub struct Cli {
#[arg(short, long)]
pub simple: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub csv: bool,
#[arg(short, long, default_value = "cloudflare")]
pub provider: String,
#[arg(long)]
pub download_only: bool,
#[arg(long)]
pub upload_only: bool,
#[arg(long)]
pub no_upload: bool,
#[arg(long)]
pub server: Option<String>,
#[arg(long)]
pub list_servers: bool,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
Diagnose,
Ping {
host: String,
},
Traceroute {
host: String,
},
Dns {
domain: String,
},
Interfaces,
History {
#[arg(long)]
export: bool,
#[arg(long)]
clear: bool,
},
}
pub async fn run_simple_test(cli: &Cli) -> Result<()> {
println!("Speedtest TUI - Network Speed Test\n");
let config = Config::load()?;
let provider = get_provider(&cli.provider)?;
print!("Retrieving connection info... ");
let conn_info = network::connection::get_connection_info().await?;
println!("Done");
println!(" IP: {}", conn_info.ip);
if let Some(ref isp) = conn_info.isp {
println!(" ISP: {}", isp);
}
if let Some(ref location) = conn_info.location {
println!(" Location: {}", location);
}
println!();
if !cli.download_only && !cli.upload_only {
print!("Testing ping... ");
let ping_result = network::ping::measure_ping(&provider.get_ping_url()).await?;
println!("Done");
println!(" Latency: {:.2} ms", ping_result.latency_ms);
println!(" Jitter: {:.2} ms", ping_result.jitter_ms);
println!();
}
if !cli.upload_only {
print!("Testing download speed... ");
std::io::Write::flush(&mut std::io::stdout())?;
let download_result = network::download::measure_download(
&provider.get_download_url(),
config.general.test_duration_seconds,
|_| {},
)
.await?;
println!("Done");
println!(" Download: {:.2} Mbps", download_result.speed_mbps);
println!(" Peak: {:.2} Mbps", download_result.peak_speed_mbps);
println!();
}
if !cli.no_upload && !cli.download_only {
print!("Testing upload speed... ");
std::io::Write::flush(&mut std::io::stdout())?;
let upload_result = network::upload::measure_upload(
&provider.get_upload_url(),
config.general.test_duration_seconds,
|_| {},
)
.await?;
println!("Done");
println!(" Upload: {:.2} Mbps", upload_result.speed_mbps);
println!(" Peak: {:.2} Mbps", upload_result.peak_speed_mbps);
}
Ok(())
}
pub async fn run_json_test(cli: &Cli) -> Result<()> {
let config = Config::load()?;
let provider = get_provider(&cli.provider)?;
let mut result = SpeedTestResult {
provider: cli.provider.clone(),
timestamp: chrono::Utc::now(),
..Default::default()
};
if let Ok(conn_info) = network::connection::get_connection_info().await {
result.connection_info = Some(conn_info);
}
if !cli.download_only && !cli.upload_only {
if let Ok(ping) = network::ping::measure_ping(&provider.get_ping_url()).await {
result.ping = Some(ping);
}
}
if !cli.upload_only {
if let Ok(download) = network::download::measure_download(
&provider.get_download_url(),
config.general.test_duration_seconds,
|_| {},
)
.await
{
result.download = Some(download);
}
}
if !cli.no_upload && !cli.download_only {
if let Ok(upload) = network::upload::measure_upload(
&provider.get_upload_url(),
config.general.test_duration_seconds,
|_| {},
)
.await
{
result.upload = Some(upload);
}
}
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
pub async fn run_csv_test(cli: &Cli) -> Result<()> {
let config = Config::load()?;
let provider = get_provider(&cli.provider)?;
let mut result = SpeedTestResult {
provider: cli.provider.clone(),
timestamp: chrono::Utc::now(),
..Default::default()
};
if !cli.download_only && !cli.upload_only {
if let Ok(ping) = network::ping::measure_ping(&provider.get_ping_url()).await {
result.ping = Some(ping);
}
}
if !cli.upload_only {
if let Ok(download) = network::download::measure_download(
&provider.get_download_url(),
config.general.test_duration_seconds,
|_| {},
)
.await
{
result.download = Some(download);
}
}
if !cli.no_upload && !cli.download_only {
if let Ok(upload) = network::upload::measure_upload(
&provider.get_upload_url(),
config.general.test_duration_seconds,
|_| {},
)
.await
{
result.upload = Some(upload);
}
}
println!("timestamp,provider,ping_ms,jitter_ms,download_mbps,upload_mbps");
println!(
"{},{},{:.2},{:.2},{:.2},{:.2}",
result.timestamp.format("%Y-%m-%d %H:%M:%S"),
result.provider,
result.ping.as_ref().map(|p| p.latency_ms).unwrap_or(0.0),
result.ping.as_ref().map(|p| p.jitter_ms).unwrap_or(0.0),
result
.download
.as_ref()
.map(|d| d.speed_mbps)
.unwrap_or(0.0),
result.upload.as_ref().map(|u| u.speed_mbps).unwrap_or(0.0),
);
Ok(())
}
pub async fn run_diagnose() -> Result<()> {
println!("Network Diagnostics\n");
println!("==================\n");
println!("Connection Information:");
match network::connection::get_connection_info().await {
Ok(info) => {
println!(" Public IP: {}", info.ip);
if let Some(ref isp) = info.isp {
println!(" ISP: {}", isp);
}
if let Some(ref location) = info.location {
println!(" Location: {}", location);
}
}
Err(e) => println!(" Error: {}", e),
}
println!();
println!("DNS Resolution (google.com):");
match network::dns::measure_dns("google.com").await {
Ok(result) => {
println!(" Resolution time: {:.2} ms", result.resolution_time_ms);
println!(" Resolved IPs: {:?}", result.resolved_ips);
}
Err(e) => println!(" Error: {}", e),
}
println!();
println!("Network Interfaces:");
let interfaces = network::interfaces::list_interfaces()?;
for iface in interfaces {
println!(
" {} - {} ({})",
iface.name,
iface.ip,
if iface.is_up { "up" } else { "down" }
);
}
println!();
println!("Latency Tests:");
for (name, url) in &[
("Cloudflare", "https://speed.cloudflare.com"),
("Google", "https://www.google.com"),
] {
match network::ping::measure_ping(url).await {
Ok(result) => {
println!(
" {}: {:.2} ms (jitter: {:.2} ms)",
name, result.latency_ms, result.jitter_ms
);
}
Err(e) => println!(" {}: Error - {}", name, e),
}
}
Ok(())
}
pub async fn run_ping(host: &str) -> Result<()> {
let url = if host.starts_with("http") {
host.to_string()
} else {
format!("https://{}", host)
};
println!("Pinging {}...\n", host);
for i in 1..=5 {
match network::ping::single_ping(&url).await {
Ok(ms) => println!(" [{}/5] {:.2} ms", i, ms),
Err(e) => println!(" [{}/5] Error: {}", i, e),
}
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
let result = network::ping::measure_ping(&url).await?;
println!("\nStatistics:");
println!(" Average: {:.2} ms", result.latency_ms);
println!(" Jitter: {:.2} ms", result.jitter_ms);
println!(" Min: {:.2} ms", result.min_ms);
println!(" Max: {:.2} ms", result.max_ms);
Ok(())
}
pub async fn run_traceroute(host: &str) -> Result<()> {
println!("Traceroute to {}\n", host);
let hops = network::traceroute::trace_route(host).await?;
for hop in hops {
if let Some(ref addr) = hop.address {
println!(
"{:>2}. {:>15} {:.2} ms",
hop.hop_number, addr, hop.latency_ms
);
} else {
println!("{:>2}. * Request timed out", hop.hop_number);
}
}
Ok(())
}
pub async fn run_dns(domain: &str) -> Result<()> {
println!("DNS Lookup for {}\n", domain);
let result = network::dns::measure_dns(domain).await?;
println!("Resolution time: {:.2} ms", result.resolution_time_ms);
println!("\nResolved addresses:");
for ip in &result.resolved_ips {
println!(" {}", ip);
}
Ok(())
}
pub fn run_interfaces() -> Result<()> {
println!("Network Interfaces\n");
let interfaces = network::interfaces::list_interfaces()?;
for iface in interfaces {
let status = if iface.is_up { "UP" } else { "DOWN" };
println!("{}", iface.name);
println!(" Status: {}", status);
println!(" IP: {}", iface.ip);
if let Some(ref mac) = iface.mac {
println!(" MAC: {}", mac);
}
println!();
}
Ok(())
}
pub async fn run_history(export: bool, clear: bool) -> Result<()> {
if clear {
storage::history::clear_history()?;
println!("History cleared.");
return Ok(());
}
if export {
let path = storage::export::export_to_csv()?;
println!("History exported to: {}", path.display());
return Ok(());
}
let history = storage::history::load_history()?;
if history.is_empty() {
println!("No test history found.");
return Ok(());
}
println!("Test History\n");
println!(
"{:<20} {:<12} {:<12} {:<12} {:<8}",
"Date", "Provider", "Download", "Upload", "Ping"
);
println!("{}", "-".repeat(70));
for entry in history.iter().rev().take(20) {
let download = entry
.download
.as_ref()
.map(|d| format!("{:.1} Mbps", d.speed_mbps))
.unwrap_or_else(|| "-".to_string());
let upload = entry
.upload
.as_ref()
.map(|u| format!("{:.1} Mbps", u.speed_mbps))
.unwrap_or_else(|| "-".to_string());
let ping = entry
.ping
.as_ref()
.map(|p| format!("{:.0} ms", p.latency_ms))
.unwrap_or_else(|| "-".to_string());
println!(
"{:<20} {:<12} {:<12} {:<12} {:<8}",
entry.timestamp.format("%Y-%m-%d %H:%M"),
entry.provider,
download,
upload,
ping
);
}
Ok(())
}
fn get_provider(name: &str) -> Result<Box<dyn Provider>> {
use crate::config::Config;
use crate::network::providers::{
cloudflare::CloudflareProvider, custom::CustomProvider, librespeed::LibrespeedProvider,
};
match name.to_lowercase().as_str() {
"cloudflare" => Ok(Box::new(CloudflareProvider::new())),
"librespeed" => Ok(Box::new(LibrespeedProvider::new())),
"custom" => {
let config = Config::load()?;
Ok(Box::new(CustomProvider::new(
config.providers.custom.download_url,
config.providers.custom.upload_url,
)))
}
_ => anyhow::bail!(
"Unknown provider: {}. Use 'cloudflare', 'librespeed', or 'custom'",
name
),
}
}