use crate::category::Category;
use crate::entry::SortOrder;
use crate::output::filter::EntryType;
use crate::scanner::HardlinkPolicy;
use clap::{ArgGroup, Parser};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "duvis",
version,
about = "duvis (/ˈduːvɪs/) is a fast, read-only disk usage analyzer for both AI agents and humans. \
Default output is a colorized terminal tree; pass --summary for a per-category summary, --json for \
structured output, or --ui for an interactive browser treemap. Strictly read-only — duvis never \
deletes anything and never recommends what to delete.",
after_help = "EXAMPLES:
Tree view, depth-limited
$ duvis ~/projects --max-depth 2 --top 10
Per-category summary
$ duvis ~/projects --summary
Structured JSON for scripts and agents
$ duvis ~/projects --json | jq '.children[] | {name, size_human, category}'
Browser UI
$ duvis ~/projects --ui"
)]
#[command(group(
ArgGroup::new("output")
.multiple(false)
.args(["json", "ndjson", "summary", "ui"])
))]
pub struct Cli {
#[arg(default_value = ".", value_name = "PATH")]
pub path: PathBuf,
#[arg(long, help_heading = "Output Format")]
pub json: bool,
#[arg(long, help_heading = "Output Format")]
pub ndjson: bool,
#[arg(long, help_heading = "Output Format")]
pub summary: bool,
#[arg(long, help_heading = "Output Format")]
pub ui: bool,
#[arg(
short = 'd',
long = "max-depth",
value_parser = positive_usize,
value_name = "N",
help_heading = "Display Options"
)]
pub max_depth: Option<usize>,
#[arg(
short = 'n',
long,
value_parser = positive_usize,
value_name = "N",
help_heading = "Display Options"
)]
pub top: Option<usize>,
#[arg(
long,
default_value = "size",
value_name = "size|name",
help_heading = "Display Options"
)]
pub sort: SortOrder,
#[arg(long, help_heading = "Display Options")]
pub reverse: bool,
#[arg(
long,
value_name = "N",
value_parser = positive_usize,
conflicts_with_all = ["summary", "ui"],
help_heading = "Display Options"
)]
pub largest: Option<usize>,
#[arg(
long,
default_value = "count-once",
value_name = "count-once|count-each",
help_heading = "Display Options"
)]
pub hardlinks: HardlinkPolicy,
#[arg(
long,
value_delimiter = ',',
value_name = "CATEGORY",
conflicts_with = "ui",
help_heading = "Filters"
)]
pub category: Vec<Category>,
#[arg(
long,
value_name = "file|dir",
conflicts_with = "ui",
help_heading = "Filters"
)]
pub r#type: Option<EntryType>,
#[arg(
long,
value_name = "SIZE",
conflicts_with = "ui",
help_heading = "Filters"
)]
pub min_size: Option<String>,
#[arg(
long,
value_name = "GLOB",
conflicts_with = "ui",
help_heading = "Filters"
)]
pub name: Vec<String>,
#[arg(
long,
value_name = "DURATION",
conflicts_with = "ui",
help_heading = "Filters"
)]
pub changed_within: Option<String>,
#[arg(
long,
value_name = "DURATION",
conflicts_with = "ui",
help_heading = "Filters"
)]
pub changed_before: Option<String>,
#[arg(
long,
default_value = "7515",
value_name = "PORT",
help_heading = "UI Server Options"
)]
pub port: u16,
#[arg(
long,
value_name = "NAME",
conflicts_with_all = ["ndjson", "summary", "ui", "largest"],
help_heading = "Diagnostics"
)]
pub explain_category: Option<String>,
}
fn positive_usize(s: &str) -> Result<usize, String> {
let n: usize = s
.parse()
.map_err(|e: std::num::ParseIntError| e.to_string())?;
if n == 0 {
Err("must be ≥ 1".to_string())
} else {
Ok(n)
}
}