use std::time::{Duration, Instant};
use crate::config::Settings;
use crate::engine::metrics::Sampler;
use crate::engine::models::{LatencyStats, Server, TestPhase, TestReport, TransferStats};
use crate::engine::EngineEvent;
const HISTORY_CAPACITY: usize = 120;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Screen {
Splash,
Testing,
Results,
Help,
Settings,
ServerSelect,
ThemeSelect,
Error,
}
impl Screen {
#[must_use]
pub fn is_overlay(self) -> bool {
matches!(
self,
Self::Help | Self::Settings | Self::ServerSelect | Self::ThemeSelect
)
}
#[must_use]
pub fn is_animated(self) -> bool {
matches!(self, Self::Splash | Self::Testing)
}
}
#[derive(Debug, Clone)]
pub struct PingView {
pub last_ms: Option<f64>,
pub samples_done: u32,
pub stats: Option<LatencyStats>,
pub history: Sampler,
}
impl PingView {
fn new() -> Self {
Self {
last_ms: None,
samples_done: 0,
stats: None,
history: Sampler::new(HISTORY_CAPACITY),
}
}
fn reset(&mut self) {
self.last_ms = None;
self.samples_done = 0;
self.stats = None;
self.history.clear();
}
}
#[derive(Debug, Clone)]
pub struct TransferView {
pub bytes: u64,
pub current_bps: f64,
pub average_bps: f64,
pub peak_bps: f64,
pub eta: Duration,
pub ratio: f64,
pub history: Sampler,
pub stats: Option<TransferStats>,
}
impl TransferView {
fn new() -> Self {
Self {
bytes: 0,
current_bps: 0.0,
average_bps: 0.0,
peak_bps: 0.0,
eta: Duration::ZERO,
ratio: 0.0,
history: Sampler::new(HISTORY_CAPACITY),
stats: None,
}
}
fn reset(&mut self) {
self.bytes = 0;
self.current_bps = 0.0;
self.average_bps = 0.0;
self.peak_bps = 0.0;
self.eta = Duration::ZERO;
self.ratio = 0.0;
self.history.clear();
self.stats = None;
}
}
#[derive(Debug)]
pub struct AppState {
pub screen: Screen,
pub return_to: Screen,
pub phase: Option<TestPhase>,
pub ping: PingView,
pub download: TransferView,
pub upload: TransferView,
pub report: Option<TestReport>,
pub servers: Vec<Server>,
pub server_index: usize,
pub server_cursor: usize,
pub theme_names: Vec<String>,
pub theme_index: usize,
pub theme_cursor: usize,
pub error: Option<String>,
pub servers_loading: bool,
pub testing: bool,
pub tick: u64,
pub started_at: Option<Instant>,
pub elapsed: Duration,
pub splash_since: Instant,
pub settings: Settings,
pub provider_name: &'static str,
pub client_info: Option<String>,
dirty: bool,
}
impl AppState {
#[must_use]
pub fn new(settings: Settings, theme_names: Vec<String>, provider_name: &'static str) -> Self {
let theme_index = theme_names
.iter()
.position(|name| name.eq_ignore_ascii_case(&settings.theme))
.unwrap_or(0);
Self {
screen: Screen::Splash,
return_to: Screen::Splash,
phase: None,
ping: PingView::new(),
download: TransferView::new(),
upload: TransferView::new(),
report: None,
servers: Vec::new(),
server_index: 0,
server_cursor: 0,
theme_names,
theme_index,
theme_cursor: theme_index,
error: None,
servers_loading: true,
testing: false,
tick: 0,
started_at: None,
elapsed: Duration::ZERO,
splash_since: Instant::now(),
settings,
provider_name,
client_info: None,
dirty: true,
}
}
#[must_use]
pub fn current_server(&self) -> Option<&Server> {
self.servers.get(self.server_index)
}
#[must_use]
pub fn server_name(&self) -> &str {
self.current_server().map_or("—", |server| &server.name)
}
pub fn request_redraw(&mut self) {
self.dirty = true;
}
pub fn take_redraw(&mut self) -> bool {
std::mem::take(&mut self.dirty)
}
pub fn reset_test(&mut self) {
self.phase = None;
self.ping.reset();
self.download.reset();
self.upload.reset();
self.report = None;
self.error = None;
self.elapsed = Duration::ZERO;
self.started_at = None;
self.request_redraw();
}
pub fn on_tick(&mut self) {
self.tick = self.tick.wrapping_add(1);
if self.testing {
if let Some(started) = self.started_at {
self.elapsed = started.elapsed();
}
}
if self.screen.is_animated() {
self.request_redraw();
}
}
pub fn apply_servers(&mut self, result: Result<Vec<Server>, String>) {
self.servers_loading = false;
match result {
Ok(servers) => {
self.servers = servers;
self.server_index = 0;
self.server_cursor = 0;
}
Err(message) => {
self.error = Some(message);
self.screen = Screen::Error;
}
}
self.request_redraw();
}
pub fn apply_engine_event(&mut self, event: EngineEvent) {
match event {
EngineEvent::PhaseStarted { phase } => {
self.phase = Some(phase);
if !self.screen.is_overlay() {
self.screen = Screen::Testing;
}
}
EngineEvent::PingSample {
sequence,
latency_ms,
} => {
self.ping.last_ms = Some(latency_ms);
self.ping.samples_done = sequence;
self.ping.history.push(latency_ms);
}
EngineEvent::PingFinished { stats } => {
self.ping.stats = Some(stats);
}
EngineEvent::Progress { progress } => {
let view = match progress.phase {
TestPhase::Download => &mut self.download,
TestPhase::Upload => &mut self.upload,
TestPhase::Ping => return,
};
view.bytes = progress.bytes_transferred;
view.current_bps = progress.current_bps;
view.average_bps = progress.average_bps;
view.peak_bps = progress.peak_bps;
view.eta = progress.eta;
view.ratio = progress.ratio;
view.history.push(progress.current_bps);
}
EngineEvent::TransferFinished { phase, stats } => {
let view = match phase {
TestPhase::Download => &mut self.download,
TestPhase::Upload => &mut self.upload,
TestPhase::Ping => return,
};
view.stats = Some(stats);
view.ratio = 1.0;
view.current_bps = stats.average_bps;
}
EngineEvent::Finished { report } => {
self.report = Some(report);
self.testing = false;
self.phase = None;
if !self.screen.is_overlay() {
self.screen = Screen::Results;
}
}
EngineEvent::Failed { message } => {
self.error = Some(message);
self.testing = false;
self.phase = None;
self.screen = Screen::Error;
}
}
self.request_redraw();
}
pub fn begin_test(&mut self) {
self.reset_test();
self.testing = true;
self.started_at = Some(Instant::now());
self.screen = Screen::Testing;
}
}