use crate::config::{OutputFormat, ScanFilter, ScanOptions};
use clap::Parser;
use std::env;
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(name = "CreepDir", version)]
#[command(
about = "Scan folders and catalog files by extension",
long_about = "CreepDir recursively scans a folder and catalogs files grouped by extension.
Output formats: text (default), --json, or --csv.
Scanning runs in parallel and skips folders it can't access instead of aborting.
Usage modes:
- Run without arguments to show this help message
- Use --select to open file dialogs for interactive folder/output selection
- Provide a folder path to scan it (output saved in the scanned folder)
- Provide both folder and output paths to specify exact output location"
)]
pub struct Cli {
#[arg(value_name = "FOLDER")]
pub folder: Option<PathBuf>,
#[arg(value_name = "OUTPUT")]
pub output: Option<PathBuf>,
#[arg(long, short = 's')]
pub select: bool,
#[arg(long, short = 'q')]
pub quiet: bool,
#[arg(long, short = 'j', value_name = "N")]
pub threads: Option<usize>,
#[arg(long)]
pub follow_symlinks: bool,
#[arg(long, value_name = "N")]
pub max_depth: Option<usize>,
#[arg(long, value_name = "EXTS", value_delimiter = ',')]
pub ext: Vec<String>,
#[arg(long, short = 'e', value_name = "GLOB")]
pub exclude: Vec<String>,
#[arg(long)]
pub sizes: bool,
#[arg(long, conflicts_with = "csv")]
pub json: bool,
#[arg(long)]
pub csv: bool,
}
impl Cli {
pub fn scan_options(&self) -> ScanOptions {
ScanOptions {
quiet: self.quiet,
threads: self.threads,
follow_symlinks: self.follow_symlinks,
max_depth: self.max_depth,
sizes: self.sizes,
format: OutputFormat::from_flags(self.json, self.csv),
}
}
pub fn scan_filter(&self) -> Result<ScanFilter, String> {
ScanFilter::new(&self.ext, &self.exclude)
}
}
pub fn resolve_output_path(
output: Option<PathBuf>,
folder: &Path,
format: OutputFormat,
) -> PathBuf {
let ext = format.extension();
match output {
Some(output) => {
let normalized = normalize_path(output);
if normalized.is_dir() {
normalized.join(default_filename(folder, ext))
} else {
normalized
}
}
None => folder.join(default_filename(folder, ext)),
}
}
pub fn default_filename(folder: &Path, ext: &str) -> String {
let name = folder
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("output");
format!("{name}.{ext}")
}
pub fn normalize_path(path: PathBuf) -> PathBuf {
#[cfg(windows)]
{
let path_str = path.to_string_lossy();
if path_str.starts_with('/') && path_str.len() > 1 {
let second = path_str.as_bytes()[1];
if second != b':' && second != b'/' && second != b'\\' {
let relative = PathBuf::from(&path_str[1..]);
if let Ok(cwd) = env::current_dir() {
return cwd.join(relative);
}
}
}
}
if path.is_absolute() {
return path;
}
if let Ok(cwd) = env::current_dir() {
return cwd.join(&path);
}
path
}