mod analysis;
mod config;
mod monitor;
mod ui;
mod utils;
use crate::analysis::aggregate_stats;
use crate::config::HISTORY_WINDOW_SIZE;
use crate::monitor::net_info::{self, NetworkInfo};
use crate::monitor::prober::run_probe_loop;
use crate::ui::parse_args;
use crate::utils::logger::SessionLogger;
use crossterm::event::{Event, EventStream, KeyCode, KeyModifiers};
use futures::StreamExt;
use std::collections::VecDeque;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use tokio::sync::mpsc;
use tokio::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = parse_args();
if cli.check {
run_cli_mode(cli.format, cli.verbose).await?;
} else {
run_tui_mode().await?;
}
Ok(())
}
async fn run_tui_mode() -> Result<(), Box<dyn std::error::Error>> {
ui::tui::setup_panic_hook();
let mut terminal = ui::tui::init_terminal()?;
let mut logger = SessionLogger::new()?;
let log_path = logger.log_path().clone();
let (probe_tx, mut probe_rx) = mpsc::channel(100);
let (stats_tx, mut stats_rx) =
mpsc::channel::<(crate::monitor::NetworkStats, crate::monitor::ProbeRound)>(100);
let (net_info_tx, mut net_info_rx) = mpsc::channel::<NetworkInfo>(10);
let _probe_handle = tokio::spawn(async move {
run_probe_loop(probe_tx).await;
});
let _net_info_handle = tokio::spawn(async move {
let info = net_info::refresh_network_info().await;
let _ = net_info_tx.send(info).await;
let mut interval = tokio::time::interval(Duration::from_secs(5));
interval.tick().await;
loop {
interval.tick().await;
let info = net_info::refresh_network_info().await;
if net_info_tx.send(info).await.is_err() {
break;
}
}
});
let _agg_handle = tokio::spawn(async move {
let mut history = VecDeque::with_capacity(HISTORY_WINDOW_SIZE);
while let Some(round) = probe_rx.recv().await {
history.push_back(round.clone());
if history.len() > HISTORY_WINDOW_SIZE {
history.pop_front();
}
let rounds: Vec<_> = history.iter().cloned().collect();
let stats = aggregate_stats(&rounds);
let _ = stats_tx.send((stats, round)).await;
}
});
let mut tui_state = ui::tui::TuiState::new();
let should_quit = Arc::new(AtomicBool::new(false));
let should_quit_clone = should_quit.clone();
tokio::spawn(async move {
let mut event_stream = EventStream::new();
while let Some(maybe_event) = event_stream.next().await {
if let Ok(Event::Key(key)) = maybe_event {
match key.code {
KeyCode::Char('q') | KeyCode::Char('Q') => {
should_quit_clone.store(true, Ordering::SeqCst);
break;
}
KeyCode::Char('c') | KeyCode::Char('C')
if key.modifiers.contains(KeyModifiers::CONTROL) =>
{
should_quit_clone.store(true, Ordering::SeqCst);
break;
}
KeyCode::Char('d') | KeyCode::Char('D')
if key.modifiers.contains(KeyModifiers::CONTROL) =>
{
should_quit_clone.store(true, Ordering::SeqCst);
break;
}
KeyCode::Esc => {
should_quit_clone.store(true, Ordering::SeqCst);
break;
}
_ => {}
}
}
}
});
let result = async {
loop {
if should_quit.load(Ordering::SeqCst) {
tui_state.should_quit = true;
}
if tui_state.should_quit {
break;
}
terminal.draw(|f| ui::tui::ui(f, &tui_state))?;
tokio::select! {
Some((stats, latest_round)) = stats_rx.recv() => {
logger.log_stats(&stats)?;
tui_state.update_stats(stats, latest_round);
}
Some(info) = net_info_rx.recv() => {
tui_state.network_info = Some(info);
}
_ = tokio::signal::ctrl_c() => {
tui_state.should_quit = true;
}
}
if tui_state.should_quit {
break;
}
}
Ok::<(), Box<dyn std::error::Error>>(())
}
.await;
ui::tui::restore_terminal(&mut terminal)?;
println!("\nSession log saved to: {}", log_path.display());
result
}
async fn run_cli_mode(
format: ui::OutputFormat,
verbose: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let net_info_task = tokio::spawn(async { net_info::refresh_network_info().await });
let (tx, mut rx) = mpsc::channel(100);
let _probe_handle = tokio::spawn(async move {
run_probe_loop(tx).await;
});
let mut history = VecDeque::with_capacity(HISTORY_WINDOW_SIZE);
let min_rounds = 3;
let mut rounds_collected = 0;
while let Some(round) = rx.recv().await {
history.push_back(round);
rounds_collected += 1;
if history.len() > HISTORY_WINDOW_SIZE {
history.pop_front();
}
if rounds_collected >= min_rounds {
let rounds: Vec<_> = history.iter().cloned().collect();
let stats = aggregate_stats(&rounds);
let network_info = net_info_task.await.ok();
ui::cli::display_cli_stats(&stats, &format, verbose, network_info.as_ref());
let exit_code = ui::cli::get_exit_code(stats.status);
std::process::exit(exit_code);
}
}
Ok(())
}