use crossterm::event::{self, Event, KeyEvent, MouseEventKind};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use crate::config::snippets::Snippet;
use crate::ssh::client::{ConnectionStatus, Host};
use crate::ssh::key_setup::KeySetupStep;
use crate::ssh::sftp::{FileEntry, SftpOpKind};
pub type HostId = String;
pub type SessionId = u64;
pub type TransferId = u64;
#[derive(Debug, Clone)]
pub struct Metrics {
pub cpu_percent: Option<f64>,
pub ram_percent: Option<f64>,
pub disk_percent: Option<f64>,
pub uptime: Option<String>,
pub load_avg: Option<String>,
pub os_info: Option<String>,
pub last_updated: Instant,
}
impl Default for Metrics {
fn default() -> Self {
Self {
cpu_percent: None,
ram_percent: None,
disk_percent: None,
uptime: None,
load_avg: None,
os_info: None,
last_updated: Instant::now(),
}
}
}
#[derive(Debug)]
pub enum AppEvent {
Key(KeyEvent),
Tick,
MetricsUpdate(HostId, Metrics),
HostStatusChanged(HostId, ConnectionStatus),
FileTransferProgress(TransferId, u64, u64),
Error(HostId, String),
HostsLoaded(Vec<Host>),
SnippetsLoaded(Vec<Snippet>),
SnippetResult {
host_name: String,
snippet_name: String,
output: Result<String, String>,
},
FileDirListed {
path: String,
entries: Vec<FileEntry>,
},
LocalDirListed {
path: String,
entries: Vec<FileEntry>,
},
SftpConnected { host_name: String },
SftpDisconnected { host_name: String, reason: String },
FilePreviewReady { path: String, content: String },
SftpOpDone {
kind: SftpOpKind,
result: Result<(), String>,
},
PtyOutput(SessionId),
PtyExited(SessionId),
TerminalResized(u16, u16),
TermScroll(i16),
DiscoveryQuickScanDone(HostId, Vec<DetectedService>),
DiscoveryDeepProbeDone(HostId, Vec<DetectedService>),
DiscoveryFailed(HostId, String),
AlertNew(HostId, Alert),
KeySetupProgress(HostId, KeySetupStep),
KeySetupComplete(HostId, std::path::PathBuf),
KeySetupFailed(HostId, String),
KeySetupRollback(HostId, String),
}
#[derive(Debug, Clone)]
pub struct DetectedService {
pub kind: ServiceKind,
pub version: Option<String>,
pub status: ServiceStatus,
pub metrics: Vec<ServiceMetric>,
pub alerts: Vec<Alert>,
pub suggested_snippets: Vec<String>, }
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ServiceKind {
Docker,
Nginx,
PostgreSQL,
Redis,
NodeJS,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceStatus {
Healthy,
Degraded(String), Critical(String), Unknown,
}
#[derive(Debug, Clone)]
pub struct ServiceMetric {
pub name: String, pub value: MetricValue, pub unit: String, pub threshold: Option<Threshold>,
}
#[derive(Debug, Clone)]
pub enum MetricValue {
Integer(i64),
Float(f64),
String(String),
Boolean(bool),
}
#[derive(Debug, Clone)]
pub struct Threshold {
pub warning: Option<f64>,
pub critical: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct Alert {
pub severity: AlertSeverity,
pub message: String,
pub service: ServiceKind,
pub suggested_action: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlertSeverity {
Info,
Warning,
Critical,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiscoveryStatus {
NotStarted,
QuickScanDone,
DeepProbeDone,
Failed(String),
}
pub fn spawn_event_thread(tx: mpsc::Sender<AppEvent>) -> anyhow::Result<()> {
std::thread::spawn(move || {
let tick = Duration::from_millis(33);
loop {
if event::poll(tick).unwrap_or(false) {
match event::read() {
Ok(Event::Key(key)) => {
if tx.blocking_send(AppEvent::Key(key)).is_err() {
break;
}
}
Ok(Event::Resize(cols, rows)) => {
if tx
.blocking_send(AppEvent::TerminalResized(cols, rows))
.is_err()
{
break;
}
}
Ok(Event::Mouse(m)) => {
let delta: Option<i16> = match m.kind {
MouseEventKind::ScrollUp => Some(3),
MouseEventKind::ScrollDown => Some(-3),
_ => None,
};
if let Some(d) = delta {
if tx.blocking_send(AppEvent::TermScroll(d)).is_err() {
break;
}
}
}
Ok(_) => {}
Err(_) => break,
}
} else if tx.blocking_send(AppEvent::Tick).is_err() {
break;
}
}
});
Ok(())
}