use anyhow::Result;
use clap::{Parser, Subcommand};
mod app;
mod collect;
mod config;
mod insights;
mod recording;
mod report;
mod snapshot;
mod tabs;
mod ui;
use config::SyswatchConfig;
#[derive(Subcommand, Debug)]
enum Command {
Snapshot {
#[arg(long)]
json: bool,
},
Insights {
#[arg(long)]
json: bool,
#[arg(long, value_parser = recording::parse_retention, default_value = "30s")]
since: std::time::Duration,
},
Diff {
a: std::path::PathBuf,
b: Option<std::path::PathBuf>,
#[arg(long)]
json: bool,
},
Why {
#[arg(long, value_parser = recording::parse_retention, default_value = "30s")]
since: std::time::Duration,
},
}
#[derive(Parser, Debug)]
#[command(
name = "syswatch",
version,
about = "Single-host system diagnostics TUI"
)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[arg(long)]
tick: Option<u64>,
#[arg(long)]
tab: Option<String>,
#[arg(long)]
lite: bool,
#[arg(long)]
dense: bool,
#[arg(long, value_name = "PATH", conflicts_with = "record")]
replay: Option<std::path::PathBuf>,
#[arg(long, requires = "keep")]
record: bool,
#[arg(long, value_name = "DURATION", value_parser = recording::parse_retention, requires = "record")]
keep: Option<std::time::Duration>,
}
#[cfg(unix)]
fn reset_sigpipe() {
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
#[cfg(not(unix))]
fn reset_sigpipe() {}
fn main() -> Result<()> {
reset_sigpipe();
let cli = Cli::parse();
let mut cfg = SyswatchConfig::load();
if let Some(t) = cli.tick {
cfg.tick_ms = t;
cfg.validate();
}
if let Some(command) = cli.command {
return match command {
Command::Snapshot { json } => report::run_snapshot(json),
Command::Insights { json, since } => report::run_insights(json, since, cfg.tick_ms),
Command::Diff { a, b, json } => report::run_diff(&a, b.as_deref(), json),
Command::Why { since } => report::run_why(since, cfg.tick_ms),
};
}
if cli.record {
let keep = cli.keep.expect("clap requires --keep with --record");
let ring_dir = recording::ring_dir()
.ok_or_else(|| anyhow::anyhow!("could not determine the data directory"))?;
let tick = std::time::Duration::from_millis(cfg.tick_ms.clamp(100, 5000));
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
{
let stop = std::sync::Arc::clone(&stop);
ctrlc::set_handler(move || {
stop.store(true, std::sync::atomic::Ordering::Relaxed);
})
.map_err(|e| anyhow::anyhow!("could not install Ctrl-C handler: {e}"))?;
}
eprintln!(
"syswatch: recording to {} (keeping {:?}, Ctrl-C to stop)",
ring_dir.display(),
keep
);
return recording::run_ring(&ring_dir, keep, recording::RING_CHUNK_DURATION, tick, &stop);
}
ui::theme::set_by_name(&cfg.theme);
let start_tab = cli.tab.or_else(|| Some(cfg.default_tab.clone()));
let replay = if let Some(path) = cli.replay {
let total = recording::count(&path)
.map_err(|e| anyhow::anyhow!("could not read recording {}: {}", path.display(), e))?;
if total == 0 {
return Err(anyhow::anyhow!(
"recording {} contains no snapshots",
path.display()
));
}
eprintln!(
"syswatch: replaying {} snapshots from {}",
total,
path.display()
);
Some(app::ReplaySource { path, total })
} else {
None
};
app::run(app::Options {
start_tab,
config: cfg,
replay,
lite: cli.lite,
dense: cli.dense,
})
}