use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
use futures::StreamExt;
use ratatui::DefaultTerminal;
use std::time::Duration;
use tokio::time::interval;
use crate::collectors::disk_health::DiskHealthData;
use crate::collectors::drivers::{DriverData, DriverScanStatus};
use crate::collectors::network_diag::NetworkDiagData;
use crate::collectors::{DiagnosticWarning, SystemSnapshot, WarningSeverity};
use crate::error::Result;
use crate::history::HistoryBuffer;
use crate::types::{DiagnosticMode, HealthStatus, ProcessSortKey, Section, TempUnit};
use crate::ui;
const REFRESH_FAST: Duration = Duration::from_secs(1);
const REFRESH_SLOW: Duration = Duration::from_secs(5);
const REFRESH_MEDIUM: Duration = Duration::from_secs(3);
const REFRESH_DIAG: Duration = Duration::from_secs(15);
const REFRESH_HEALTH: Duration = Duration::from_secs(60);
const HISTORY_SAMPLES: usize = 60;
pub struct App {
pub mode: Option<DiagnosticMode>,
pub current_section: Section,
pub should_quit: bool,
pub show_help: bool,
pub snapshot: SystemSnapshot,
pub cpu_history: HistoryBuffer,
pub mem_history: HistoryBuffer,
pub net_down_history: HistoryBuffer,
pub net_up_history: HistoryBuffer,
pub process_scroll: usize,
pub process_sort: ProcessSortKey,
pub too_small: bool,
pub per_core_history: Vec<HistoryBuffer>,
pub swap_history: HistoryBuffer,
pub gpu_history: HistoryBuffer,
pub temp_history: HistoryBuffer,
pub temp_unit: TempUnit,
pub connection_scroll: usize,
pub disk_read_history: HistoryBuffer,
pub disk_write_history: HistoryBuffer,
pub driver_scroll: usize,
pub disk_scroll: usize,
driver_scan_handle: Option<tokio::task::JoinHandle<DriverData>>,
connectivity_check_handle:
Option<tokio::task::JoinHandle<(NetworkDiagData, Vec<DiagnosticWarning>)>>,
disk_health_handle: Option<tokio::task::JoinHandle<(DiskHealthData, Vec<DiagnosticWarning>)>>,
}
impl App {
pub fn new(initial_mode: Option<DiagnosticMode>) -> Self {
Self {
mode: initial_mode,
current_section: Section::Overview,
should_quit: false,
show_help: false,
snapshot: SystemSnapshot::default(),
cpu_history: HistoryBuffer::new(HISTORY_SAMPLES),
mem_history: HistoryBuffer::new(HISTORY_SAMPLES),
net_down_history: HistoryBuffer::new(HISTORY_SAMPLES),
net_up_history: HistoryBuffer::new(HISTORY_SAMPLES),
process_scroll: 0,
process_sort: ProcessSortKey::Cpu,
too_small: false,
per_core_history: Vec::new(),
swap_history: HistoryBuffer::new(HISTORY_SAMPLES),
gpu_history: HistoryBuffer::new(HISTORY_SAMPLES),
temp_history: HistoryBuffer::new(HISTORY_SAMPLES),
temp_unit: TempUnit::Celsius,
connection_scroll: 0,
disk_read_history: HistoryBuffer::new(HISTORY_SAMPLES),
disk_write_history: HistoryBuffer::new(HISTORY_SAMPLES),
driver_scroll: 0,
disk_scroll: 0,
driver_scan_handle: None,
connectivity_check_handle: None,
disk_health_handle: None,
}
}
pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
self.snapshot.refresh_static();
self.snapshot.refresh_fast();
self.snapshot.refresh_slow();
self.snapshot.refresh_connections();
self.start_driver_scan();
self.start_connectivity_check();
self.start_disk_health_scan();
let mut fast_tick = interval(REFRESH_FAST);
let mut slow_tick = interval(REFRESH_SLOW);
let mut medium_tick = interval(REFRESH_MEDIUM);
let mut diag_tick = interval(REFRESH_DIAG);
let mut health_tick = interval(REFRESH_HEALTH);
let mut event_stream = crossterm::event::EventStream::new();
loop {
self.poll_background_scans().await;
let size = terminal.size()?;
self.too_small = size.width < 80 || size.height < 24;
terminal.draw(|frame| ui::render(frame, self))?;
if self.should_quit {
return Ok(());
}
tokio::select! {
_ = fast_tick.tick() => {
self.snapshot.refresh_fast();
self.update_fast_history();
}
_ = slow_tick.tick() => {
self.snapshot.refresh_slow();
}
_ = medium_tick.tick() => {
self.snapshot.refresh_connections();
}
_ = diag_tick.tick() => {
if self.connectivity_check_handle.is_none() {
self.start_connectivity_check();
}
}
_ = health_tick.tick() => {
if self.disk_health_handle.is_none() {
self.start_disk_health_scan();
}
}
event = event_stream.next() => {
if let Some(Ok(evt)) = event {
self.handle_event(evt);
}
}
}
}
}
fn start_driver_scan(&mut self) {
self.snapshot.drivers.scan_status = DriverScanStatus::Scanning;
self.driver_scan_handle = Some(tokio::task::spawn_blocking(
crate::collectors::drivers::collect,
));
}
fn start_connectivity_check(&mut self) {
self.connectivity_check_handle = Some(tokio::task::spawn_blocking(
crate::collectors::network_diag::collect_connectivity,
));
}
fn start_disk_health_scan(&mut self) {
self.disk_health_handle = Some(tokio::task::spawn_blocking(
crate::collectors::disk_health::collect,
));
}
async fn poll_background_scans(&mut self) {
if self
.driver_scan_handle
.as_ref()
.is_some_and(tokio::task::JoinHandle::is_finished)
{
if let Some(handle) = self.driver_scan_handle.take() {
if let Ok(data) = handle.await {
self.snapshot.warnings.retain(|w| w.source != "Drivers");
if let DriverScanStatus::ScanFailed(ref msg) = data.scan_status {
self.snapshot.warnings.push(DiagnosticWarning {
source: "Drivers".into(),
message: msg.clone(),
severity: WarningSeverity::Warning,
});
}
self.snapshot.drivers = data;
}
}
}
if self
.connectivity_check_handle
.as_ref()
.is_some_and(tokio::task::JoinHandle::is_finished)
{
if let Some(handle) = self.connectivity_check_handle.take() {
if let Ok((diag_data, diag_warnings)) = handle.await {
self.snapshot.network_diag.gateway = diag_data.gateway;
self.snapshot.network_diag.dns = diag_data.dns;
self.snapshot.network_diag.internet = diag_data.internet;
self.snapshot.warnings.retain(|w| w.source != "Network");
self.snapshot.warnings.extend(diag_warnings);
}
}
}
if self
.disk_health_handle
.as_ref()
.is_some_and(tokio::task::JoinHandle::is_finished)
{
if let Some(handle) = self.disk_health_handle.take() {
if let Ok((health_data, health_warnings)) = handle.await {
self.snapshot.disk_health = health_data;
self.snapshot.warnings.retain(|w| w.source != "Disk Health");
self.snapshot.warnings.extend(health_warnings);
}
}
}
}
fn update_fast_history(&mut self) {
self.cpu_history.push(self.snapshot.cpu.total_usage as f64);
while self.per_core_history.len() < self.snapshot.cpu.per_core_usage.len() {
self.per_core_history
.push(HistoryBuffer::new(HISTORY_SAMPLES));
}
for (i, usage) in self.snapshot.cpu.per_core_usage.iter().enumerate() {
if let Some(buf) = self.per_core_history.get_mut(i) {
buf.push(*usage as f64);
}
}
let mem_pct = if self.snapshot.memory.total_bytes > 0 {
(self.snapshot.memory.used_bytes as f64 / self.snapshot.memory.total_bytes as f64)
* 100.0
} else {
0.0
};
self.mem_history.push(mem_pct);
let swap_pct = if self.snapshot.memory.swap_total_bytes > 0 {
(self.snapshot.memory.swap_used_bytes as f64
/ self.snapshot.memory.swap_total_bytes as f64)
* 100.0
} else {
0.0
};
self.swap_history.push(swap_pct);
self.net_down_history
.push(self.snapshot.network.total_download_rate as f64);
self.net_up_history
.push(self.snapshot.network.total_upload_rate as f64);
self.gpu_history
.push(self.snapshot.gpu.utilization_percent as f64);
if let Some(cpu_temp) = self.snapshot.thermals.cpu_temp {
self.temp_history.push(cpu_temp);
}
if let Some(drive) = self.snapshot.disk_health.drives.first() {
if let Some(ref io) = drive.io_stats {
self.disk_read_history.push(io.read_bytes_per_sec as f64);
self.disk_write_history.push(io.write_bytes_per_sec as f64);
}
}
}
fn handle_event(&mut self, event: Event) {
if let Event::Key(key) = event {
if key.kind != KeyEventKind::Press {
return;
}
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
self.should_quit = true;
return;
}
if self.show_help {
match key.code {
KeyCode::Char('?') | KeyCode::Esc => self.show_help = false,
_ => {}
}
return;
}
if self.mode.is_none() {
match key.code {
KeyCode::Char('1') => self.mode = Some(DiagnosticMode::User),
KeyCode::Char('2') => self.mode = Some(DiagnosticMode::Technician),
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
_ => {}
}
return;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
KeyCode::Char('m') => self.mode = None,
KeyCode::Char('?') => self.show_help = true,
KeyCode::Char(c @ '1'..='9') => {
if let Some(section) = Section::from_number(c as u8 - b'0') {
self.current_section = section;
self.process_scroll = 0;
self.connection_scroll = 0;
self.driver_scroll = 0;
self.disk_scroll = 0;
}
}
KeyCode::Char('j') | KeyCode::Down => {
match self.current_section {
Section::Processes => {
let max = self.snapshot.processes.list.len().saturating_sub(1);
self.process_scroll = (self.process_scroll + 1).min(max);
}
Section::Network => {
let max = self
.snapshot
.network_diag
.active_connections
.len()
.saturating_sub(1);
self.connection_scroll = (self.connection_scroll + 1).min(max);
}
Section::Drivers => {
self.driver_scroll = self.driver_scroll.saturating_add(1);
}
Section::Disk => {
self.disk_scroll = self.disk_scroll.saturating_add(1);
}
_ => {}
}
}
KeyCode::Char('k') | KeyCode::Up => match self.current_section {
Section::Processes => {
self.process_scroll = self.process_scroll.saturating_sub(1);
}
Section::Network => {
self.connection_scroll = self.connection_scroll.saturating_sub(1);
}
Section::Drivers => {
self.driver_scroll = self.driver_scroll.saturating_sub(1);
}
Section::Disk => {
self.disk_scroll = self.disk_scroll.saturating_sub(1);
}
_ => {}
},
KeyCode::Char('c') if self.current_section == Section::Processes => {
self.process_sort = ProcessSortKey::Cpu;
}
KeyCode::Char('M') if self.current_section == Section::Processes => {
self.process_sort = ProcessSortKey::Memory;
}
KeyCode::Char('n') if self.current_section == Section::Processes => {
self.process_sort = ProcessSortKey::Name;
}
KeyCode::Char('p') if self.current_section == Section::Processes => {
self.process_sort = ProcessSortKey::Pid;
}
KeyCode::Char('f') => {
self.temp_unit = self.temp_unit.toggle();
}
KeyCode::Char('r')
if self.current_section == Section::Drivers
&& self.driver_scan_handle.is_none() =>
{
self.start_driver_scan();
}
_ => {}
}
}
}
pub fn overall_health(&self) -> HealthStatus {
let cpu_status = HealthStatus::from_percent(self.snapshot.cpu.total_usage as f64);
let mem_pct = if self.snapshot.memory.total_bytes > 0 {
(self.snapshot.memory.used_bytes as f64 / self.snapshot.memory.total_bytes as f64)
* 100.0
} else {
0.0
};
let mem_status = HealthStatus::from_percent(mem_pct);
if cpu_status == HealthStatus::Critical || mem_status == HealthStatus::Critical {
HealthStatus::Critical
} else if cpu_status == HealthStatus::Warning || mem_status == HealthStatus::Warning {
HealthStatus::Warning
} else {
HealthStatus::Good
}
}
}