use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::Result;
use crossterm::event::{Event, EventStream, KeyEventKind};
use futures_util::StreamExt;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::engine::models::Server;
use crate::engine::{Engine, EngineEvent};
use crate::tui::renderer::Renderer;
use crate::tui::terminal::Tui;
use super::action::Command;
use super::controller;
use super::event::AppEvent;
use super::history;
use super::state::{AppState, Screen};
const MIN_SPLASH: Duration = Duration::from_millis(1200);
const ENGINE_CHANNEL_CAPACITY: usize = 64;
pub struct App {
engine: Arc<Engine>,
state: AppState,
renderer: Renderer,
tick_rate: Duration,
engine_rx: Option<mpsc::Receiver<EngineEvent>>,
cancel: CancellationToken,
}
impl App {
#[must_use]
pub fn new(engine: Engine, state: AppState, renderer: Renderer, tick_rate: Duration) -> Self {
Self {
engine: Arc::new(engine),
state,
renderer,
tick_rate,
engine_rx: None,
cancel: CancellationToken::new(),
}
}
pub async fn run(mut self, tui: &mut Tui) -> Result<()> {
let mut servers_rx = Some(self.spawn_discovery());
let mut info_rx: Option<mpsc::Receiver<String>> = None;
let mut input = EventStream::new();
let mut ticker = tokio::time::interval(self.tick_rate);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut last_frame = Instant::now();
self.draw(tui, 0.0)?;
loop {
let event = tokio::select! {
maybe = input.next() => match maybe {
Some(Ok(Event::Key(key))) if key.kind == KeyEventKind::Press => {
AppEvent::Key(key)
}
Some(Ok(Event::Resize(_, _))) => AppEvent::Resize,
Some(Ok(_)) => continue,
Some(Err(_)) | None => break,
},
_ = ticker.tick() => AppEvent::Tick,
maybe = recv_or_pending(&mut self.engine_rx) => match maybe {
Some(engine_event) => AppEvent::Engine(engine_event),
None => {
self.engine_rx = None;
continue;
}
},
maybe = recv_or_pending(&mut servers_rx) => match maybe {
Some(result) => AppEvent::ServersLoaded(result),
None => {
servers_rx = None;
continue;
}
},
maybe = recv_or_pending(&mut info_rx) => match maybe {
Some(info) => AppEvent::ClientInfo(info),
None => {
info_rx = None;
continue;
}
},
};
match event {
AppEvent::Key(key) => {
let Some(action) = controller::map_key(self.state.screen, key) else {
continue;
};
match controller::handle(&mut self.state, action) {
Command::Quit => break,
Command::StartTest => self.start_test(),
Command::None => {}
}
}
AppEvent::Resize => self.state.request_redraw(),
AppEvent::Engine(engine_event) => {
if let EngineEvent::Finished { report } = &engine_event {
history::record_report(report);
}
self.state.apply_engine_event(engine_event);
}
AppEvent::ServersLoaded(result) => {
self.state.apply_servers(result);
if info_rx.is_none() {
info_rx = self.spawn_info_fetch();
}
}
AppEvent::ClientInfo(info) => {
self.state.client_info = Some(info);
self.state.request_redraw();
}
AppEvent::Tick => {
self.state.on_tick();
self.maybe_leave_splash();
if self.state.take_redraw() {
let dt = last_frame.elapsed().as_secs_f64();
last_frame = Instant::now();
self.draw(tui, dt)?;
}
}
}
}
self.cancel.cancel();
Ok(())
}
fn spawn_discovery(&self) -> mpsc::Receiver<Result<Vec<Server>, String>> {
let (tx, rx) = mpsc::channel(1);
let engine = Arc::clone(&self.engine);
tokio::spawn(async move {
let result = engine.load_servers().await.map_err(|err| err.to_string());
let _ = tx.send(result).await;
});
rx
}
fn spawn_info_fetch(&self) -> Option<mpsc::Receiver<String>> {
let server = self.state.current_server().cloned()?;
let (tx, rx) = mpsc::channel(1);
let engine = Arc::clone(&self.engine);
tokio::spawn(async move {
if let Some(info) = engine.client_info(&server).await {
let _ = tx.send(info).await;
}
});
Some(rx)
}
fn maybe_leave_splash(&mut self) {
let ready = self.state.screen == Screen::Splash
&& !self.state.servers_loading
&& !self.state.testing
&& !self.state.servers.is_empty()
&& self.state.splash_since.elapsed() >= MIN_SPLASH;
if ready {
self.start_test();
}
}
fn start_test(&mut self) {
let Some(server) = self.state.current_server().cloned() else {
return;
};
self.cancel.cancel();
self.cancel = CancellationToken::new();
self.state.begin_test();
let (tx, rx) = mpsc::channel(ENGINE_CHANNEL_CAPACITY);
self.engine_rx = Some(rx);
let engine = Arc::clone(&self.engine);
let cancel = self.cancel.clone();
tokio::spawn(async move {
let _ = engine.run_test(&server, tx, cancel).await;
});
}
fn draw(&mut self, tui: &mut Tui, dt: f64) -> Result<()> {
let renderer = &mut self.renderer;
let state = &self.state;
tui.draw(|frame| renderer.render(frame, state, dt))?;
Ok(())
}
}
async fn recv_or_pending<T>(rx: &mut Option<mpsc::Receiver<T>>) -> Option<T> {
match rx {
Some(receiver) => receiver.recv().await,
None => std::future::pending().await,
}
}