use anyhow::Result;
use clap::Parser;
mod app;
mod collect;
mod insights;
mod tabs;
mod ui;
#[derive(Parser, Debug)]
#[command(
name = "diskwatch",
version,
about = "Single-host disk diagnostics TUI"
)]
struct Cli {
#[arg(long)]
tab: Option<String>,
#[arg(long, default_value = "dark", value_parser = parse_theme)]
theme: String,
#[arg(long, default_value = "bars", value_parser = parse_graph)]
graph: String,
#[arg(long)]
graph_fade: bool,
#[arg(long)]
lite: bool,
#[arg(long, alias = "btop")]
v2: bool,
#[arg(long, value_parser = parse_view)]
view: Option<String>,
#[arg(long)]
diag: bool,
}
fn parse_theme(raw: &str) -> Result<String, String> {
let resolved = ui::theme::by_name(raw);
if resolved.name == "dark" && !raw.eq_ignore_ascii_case("dark") {
return Err(format!(
"unknown theme {raw:?} (available: {})",
ui::theme::THEME_NAMES.join(", ")
));
}
Ok(raw.to_string())
}
fn parse_graph(raw: &str) -> Result<String, String> {
if ui::graph::by_name(raw) == ui::graph::GraphStyle::Bars && !raw.eq_ignore_ascii_case("bars") {
return Err(format!(
"unknown graph style {raw:?} (available: {})",
ui::graph::GRAPH_STYLE_NAMES.join(", ")
));
}
Ok(raw.to_string())
}
fn parse_view(raw: &str) -> Result<String, String> {
app::ViewMode::from_name(raw)
.map(|_| raw.to_string())
.ok_or_else(|| {
format!(
"unknown view {raw:?} (available: {})",
app::VIEW_MODE_NAMES.join(", ")
)
})
}
fn main() -> Result<()> {
let cli = Cli::parse();
if cli.diag {
return run_diag();
}
ui::theme::set_by_name(&cli.theme);
ui::graph::set_by_name(&cli.graph);
ui::graph::set_fade(cli.graph_fade);
let view = cli
.view
.as_deref()
.and_then(app::ViewMode::from_name)
.unwrap_or(if cli.v2 {
app::ViewMode::V2
} else if cli.lite {
app::ViewMode::Lite
} else {
app::ViewMode::Full
});
app::run(app::Options {
start_tab: cli.tab,
view,
})
}
fn run_diag() -> Result<()> {
let devices = collect::devices::collect();
println!("=== Devices ({}) ===", devices.len());
for d in &devices {
println!(
" {} kind={:?} size={} used={} model={:?} smart={:?}",
d.name, d.kind, d.size_bytes, d.used_bytes, d.model, d.smart_ok
);
}
let total: u64 = devices.iter().map(|d| d.size_bytes).sum();
let used: u64 = devices.iter().map(|d| d.used_bytes).sum();
let pct = if total > 0 {
(used as f64 / total as f64 * 100.0).round() as u32
} else {
0
};
println!(" TOTAL: size={} used={} pct={}%", total, used, pct);
#[cfg(target_os = "macos")]
{
println!("\n=== container_to_physical map ===");
let cmap = collect::macos::container_to_physical_map();
if cmap.is_empty() {
println!(" (empty)");
}
for (synth, phys) in &cmap {
println!(" {} -> {}", synth, phys);
}
}
println!(
"\n=== Filesystems ({}) ===",
collect::filesystems::collect().len()
);
for m in collect::filesystems::collect() {
println!(
" {} -> {} ({}) size={} used={}",
m.device, m.mount, m.fs_type, m.size_bytes, m.used_bytes
);
}
Ok(())
}