use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
#[derive(Parser, Debug)]
#[command(
name = "fsearch",
bin_name = "fsearch",
version,
about = "⚡ Fast file search & duplicate finder",
long_about = "⚡ fsearch — blazingly fast, cross-platform file & content search\n\n\
Subcommands:\n\
\n fsearch find <PATTERN> [PATHS…] Search for files / content\
\n fsearch dup [PATHS…] Find duplicate files\
\n fsearch config Config helpers\
\n\nRun `fsearch <SUBCOMMAND> --help` for full option details.",
author = "Hadi Cahyadi <cumulus13@gmail.com>"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
#[command(alias = "f")]
Find(FindArgs),
#[command(alias = "d", alias = "dupes", alias = "duplicates")]
Dup(DupArgs),
#[command(alias = "cfg")]
Config(ConfigArgs),
}
#[derive(Args, Debug)]
#[command(
about = "🔍 Search for files by name or content",
after_help = "EXAMPLES:\n\
\n # Single directory (default = current dir)\
\n fsearch find '*.rs'\
\n\n # Multiple directories as positional args\
\n fsearch find '*.rs' ./src ./tests ./benches\
\n\n # Multiple directories via -p flag (repeatable)\
\n fsearch find '*.rs' -p ./src -p ./tests\
\n\n # Mix positional + flag\
\n fsearch find TODO -f -i '*.py' ./lib -p ./scripts -d 5\
\n\n # Case-sensitive, depth 5\
\n fsearch find README -C -d 5\n"
)]
pub struct FindArgs {
pub pattern: String,
#[arg(value_name = "PATH", num_args = 0..)]
pub paths: Vec<String>,
#[arg(short = 'p', long = "path", value_name = "PATH", action = ArgAction::Append)]
pub path_flags: Vec<String>,
#[arg(short = 'm', long, value_name = "1|2", default_value = "1")]
pub method: SearchMethod,
#[arg(short = 'c', long = "case-insensitive", action = ArgAction::SetTrue)]
pub case_insensitive: bool,
#[arg(short = 'C', long = "case-sensitive", action = ArgAction::SetTrue)]
pub case_sensitive: bool,
#[arg(short = 'd', long = "deep", value_name = "DEPTH", default_value = "1")]
pub depth: u32,
#[arg(short = 'D', long = "no-dir", action = ArgAction::SetTrue)]
pub no_dir: bool,
#[arg(short = 'f', long = "file", action = ArgAction::SetTrue)]
pub search_in_files: bool,
#[arg(short = 'i', long, value_name = "GLOBS", default_value = "")]
pub include: String,
#[arg(short = 'x', long, value_name = "DIRS", default_value = "")]
pub exclude: String,
#[arg(short = 'n', long, value_name = "N", default_value = "0")]
pub max_results: usize,
#[arg(short = 'v', long, action = ArgAction::SetTrue)]
pub verbose: bool,
}
impl FindArgs {
pub fn resolved_paths(&self) -> Vec<String> {
let mut all: Vec<String> = self
.paths
.iter()
.chain(self.path_flags.iter())
.cloned()
.collect();
let mut seen = std::collections::HashSet::new();
all.retain(|p| seen.insert(p.clone()));
if all.is_empty() {
vec![".".into()]
} else {
all
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum SearchMethod {
#[value(name = "1")]
Walkdir = 1,
#[value(name = "2")]
Recursive = 2,
}
#[derive(Args, Debug)]
#[command(
about = "🔁 Find duplicate files",
after_help = "EXAMPLES:\n\
\n # Single directory\
\n fsearch dup ~/Downloads\
\n\n # Multiple directories (union — dupes across all)\
\n fsearch dup ~/Documents ~/Downloads ~/Desktop\
\n\n # Multiple via -p flag\
\n fsearch dup -p ~/Music -p /mnt/backup/Music\
\n\n # Mix positional + flag\
\n fsearch dup ~/Photos -p /mnt/nas/Photos\
\n\n # Name mode, deep search\
\n fsearch dup ./src ./vendor --mode name -d 10\
\n\n # Images >= 100 KiB, MD5\
\n fsearch dup ~/Photos --algo md5 -i '*.jpg,*.png' --min-size 102400\n"
)]
pub struct DupArgs {
#[arg(value_name = "PATH", num_args = 0..)]
pub paths: Vec<String>,
#[arg(short = 'p', long = "path", value_name = "PATH", action = ArgAction::Append)]
pub path_flags: Vec<String>,
#[arg(long, value_name = "MODE", default_value = "content")]
pub mode: DupMode,
#[arg(long, value_name = "ALGO", default_value = "sha256")]
pub algo: DupAlgo,
#[arg(short = 'd', long = "deep", value_name = "DEPTH", default_value = "10")]
pub depth: u32,
#[arg(short = 'i', long, value_name = "GLOBS", default_value = "")]
pub include: String,
#[arg(short = 'x', long, value_name = "DIRS", default_value = "")]
pub exclude: String,
#[arg(long, value_name = "BYTES", default_value = "1")]
pub min_size: u64,
#[arg(long, value_name = "BYTES", default_value = "0")]
pub max_size: u64,
#[arg(long, action = ArgAction::SetTrue)]
pub skip_binary: bool,
#[arg(short = 'n', long, value_name = "N", default_value = "0")]
pub max_results: usize,
#[arg(short = 'v', long, action = ArgAction::SetTrue)]
pub verbose: bool,
}
impl DupArgs {
pub fn resolved_paths(&self) -> Vec<String> {
let mut all: Vec<String> = self
.paths
.iter()
.chain(self.path_flags.iter())
.cloned()
.collect();
let mut seen = std::collections::HashSet::new();
all.retain(|p| seen.insert(p.clone()));
if all.is_empty() {
vec![".".into()]
} else {
all
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum DupMode {
Content,
Name,
Size,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum DupAlgo {
Md5,
Sha256,
}
#[derive(Args, Debug)]
#[command(about = "⚙️ Configuration helpers")]
pub struct ConfigArgs {
#[command(subcommand)]
pub action: ConfigAction,
}
#[derive(Subcommand, Debug)]
pub enum ConfigAction {
Init,
Show,
Path,
}