use std::io::IsTerminal;
use std::path::PathBuf;
use std::process;
use clap::{Parser, ValueEnum};
use fruit::{
GitignoreFilter, OutputConfig, StreamingFormatter, StreamingWalker, TreeWalker, WalkerConfig,
print_json,
};
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
enum ColorMode {
#[default]
Auto,
Always,
Never,
}
fn should_use_color(mode: ColorMode) -> bool {
match mode {
ColorMode::Always => true,
ColorMode::Never => false,
ColorMode::Auto => {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
if std::env::var_os("FORCE_COLOR").is_some() {
return true;
}
if std::env::var("TERM").map(|t| t == "dumb").unwrap_or(false) {
return false;
}
std::io::stdout().is_terminal()
}
}
}
#[derive(Parser, Debug)]
#[command(name = "fruit")]
#[command(about = "A tree command that respects .gitignore and shows file comments")]
#[command(version)]
struct Args {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(short, long)]
all: bool,
#[arg(short = 'L', long = "level")]
level: Option<usize>,
#[arg(short = 'd', long = "dirs-only")]
dirs_only: bool,
#[arg(short = 'f', long = "full-comment")]
full_comment: bool,
#[arg(short = 'I', long = "ignore")]
ignore: Vec<String>,
#[arg(long = "color", value_name = "WHEN", default_value = "auto")]
color: ColorMode,
#[arg(long = "no-comments")]
no_comments: bool,
#[arg(short = 'w', long = "wrap", default_value = "100")]
wrap: usize,
#[arg(long = "json")]
json: bool,
}
fn main() {
let args = Args::parse();
let walker_config = WalkerConfig {
show_all: args.all,
max_depth: args.level,
dirs_only: args.dirs_only,
extract_comments: !args.no_comments,
ignore_patterns: args.ignore.clone(),
};
let root = if args.path.is_absolute() {
args.path.clone()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(&args.path)
};
let result = if args.json {
let mut walker = TreeWalker::new(walker_config);
if !args.all {
if let Some(filter) = GitignoreFilter::new(&args.path) {
walker = walker.with_gitignore_filter(filter);
} else {
eprintln!("fruit: warning: not a git repository, showing all files");
}
}
let tree = match walker.walk(&root) {
Some(t) => t,
None => {
eprintln!(
"fruit: cannot access '{}': No such file or directory",
args.path.display()
);
process::exit(1);
}
};
print_json(&tree)
} else {
let mut walker = StreamingWalker::new(walker_config);
if !args.all {
if let Some(filter) = GitignoreFilter::new(&args.path) {
walker = walker.with_gitignore_filter(filter);
} else {
eprintln!("fruit: warning: not a git repository, showing all files");
}
}
let output_config = OutputConfig {
use_color: should_use_color(args.color),
show_full_comment: args.full_comment,
wrap_width: if args.wrap == 0 {
None
} else {
Some(args.wrap)
},
};
let mut formatter = StreamingFormatter::new(output_config);
match walker.walk_streaming(&root, &mut formatter) {
Ok(Some(_)) => Ok(()),
Ok(None) => {
eprintln!(
"fruit: cannot access '{}': No such file or directory",
args.path.display()
);
process::exit(1);
}
Err(e) => Err(e),
}
};
if let Err(e) = result {
eprintln!("fruit: error writing output: {}", e);
process::exit(1);
}
}