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 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, cli.silent, cli.short).await?;
} else {
run_tui_mode(cli.silent, cli.short).await?;
}
Ok(())
}
async fn run_tui_mode(silent: bool, short: bool) -> 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 (input_tx, mut input_rx) = mpsc::channel::<Event>(100);
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 keyboard_handle = tokio::spawn(async move {
let mut event_stream = EventStream::new();
while let Some(maybe_event) = event_stream.next().await {
match maybe_event {
Ok(event) => {
if input_tx.send(event).await.is_err() {
break;
}
}
Err(_) => break,
}
}
});
let result = async {
loop {
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);
}
Some(event) = input_rx.recv() => {
if let Event::Key(key) = event {
match key.code {
KeyCode::Char('q') | KeyCode::Char('Q') => {
tui_state.should_quit = true;
}
KeyCode::Char('c') | KeyCode::Char('C')
if key.modifiers.contains(KeyModifiers::CONTROL) =>
{
tui_state.should_quit = true;
}
KeyCode::Char('d') | KeyCode::Char('D')
if key.modifiers.contains(KeyModifiers::CONTROL) =>
{
tui_state.should_quit = true;
}
KeyCode::Esc => {
tui_state.should_quit = true;
}
_ => {}
}
}
}
_ = tokio::signal::ctrl_c() => {
tui_state.should_quit = true;
}
}
if tui_state.should_quit {
break;
}
}
Ok::<(), Box<dyn std::error::Error>>(())
}
.await;
probe_handle.abort();
net_info_handle.abort();
agg_handle.abort();
keyboard_handle.abort();
ui::tui::restore_terminal(&mut terminal)?;
if !silent {
let summary = ui::format_summary(&tui_state, short);
println!("{}", summary);
}
println!("\nSession log saved to: {}", log_path.display());
let final_status = tui_state
.stats
.as_ref()
.map(|s| s.status)
.unwrap_or(crate::monitor::ConnectionStatus::Disconnected);
let had_disconnections = !tui_state.disconnections.is_empty();
let exit_code = ui::cli::get_tui_exit_code(final_status, had_disconnections);
result?;
std::process::exit(exit_code);
}
async fn run_cli_mode(
format: ui::OutputFormat,
verbose: bool,
silent: bool,
short: 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());
if !silent {
let session_duration = 3; let summary =
ui::format_cli_summary(&stats, network_info.as_ref(), session_duration, short);
println!("\n{}", summary);
}
let exit_code = ui::cli::get_exit_code(stats.status);
std::process::exit(exit_code);
}
}
Ok(())
}