use clap::Parser;
use colored::*;
use git2::Repository;
use regex::Regex;
use std::path::PathBuf;
mod config;
mod display;
mod ignores;
mod search;
mod summary;
mod utils;
use crate::config::{add_config_pattern, clear_config_patterns, list_config_patterns, load_config_patterns, remove_config_pattern};
use display::{display_tree, get_git_tracked_files, get_git_untracked_files, get_git_staged_files, get_git_changed_files, GitMode, StructConfig};
use search::search_files;
use summary::display_summary;
#[derive(Parser, Debug)]
#[command(name = "struct")]
#[command(version)]
#[command(about = "A smarter tree command with intelligent defaults", long_about = None)]
struct Args {
#[command(subcommand)]
command: Option<Commands>,
#[arg(value_name = "DEPTH")]
depth: Option<usize>,
#[arg(short = 'p', long = "path", default_value = ".")]
path: PathBuf,
#[arg(short = 'g', long = "git")]
git_tracked: bool,
#[arg(long = "gu")]
git_untracked: bool,
#[arg(long = "gs")]
git_staged: bool,
#[arg(long = "gc")]
git_changed: bool,
#[arg(long = "gh")]
git_history: bool,
#[arg(long = "gr")]
git_root: bool,
#[arg(long = "gur")]
git_untracked_root: bool,
#[arg(long = "gsr")]
git_staged_root: bool,
#[arg(long = "gcr")]
git_changed_root: bool,
#[arg(long = "ghr")]
git_history_root: bool,
#[arg(short = 'i', long = "ignore")]
ignore_patterns: Option<String>,
#[arg(short = 's', long = "skip-large")]
max_size_mb: Option<u64>,
#[arg(short = 'z', long = "size")]
show_size: bool,
#[arg(short = 'n', long = "no-ignore")]
no_ignore: Option<String>,
}
#[derive(clap::Subcommand, Debug)]
enum Commands {
Add {
pattern: String,
},
Remove {
pattern: String,
},
List,
Clear,
Search {
pattern: String,
#[arg(short = 'd', long = "depth", default_value = "0")]
depth: usize,
#[arg(short = 'f', long = "flat")]
flat: bool,
#[arg(default_value = ".")]
path: PathBuf,
},
}
fn main() {
let args = Args::parse();
if let Some(command) = args.command {
match command {
Commands::Add { pattern } => {
add_config_pattern(pattern);
return;
}
Commands::Remove { pattern } => {
remove_config_pattern(pattern);
return;
}
Commands::List => {
list_config_patterns();
return;
}
Commands::Clear => {
clear_config_patterns();
return;
}
Commands::Search { pattern, depth, flat, path } => {
let max_depth = if depth == 0 { usize::MAX } else { depth };
let config_patterns = load_config_patterns();
let mut custom_ignores = Vec::new();
for pattern in config_patterns {
let pattern = pattern.replace("*", ".*");
if let Ok(re) = Regex::new(&format!("^{}$", pattern)) {
custom_ignores.push(re);
}
}
search_files(&pattern, &path, max_depth, flat, &custom_ignores);
return;
}
}
}
let depth = match args.depth {
None => usize::MAX, Some(0) => 1, Some(d) => d, };
let max_size_bytes = args.max_size_mb.map(|mb| mb * 1024 * 1024);
let git_mode = if args.git_changed || args.git_changed_root {
Some(GitMode::Changed)
} else if args.git_staged || args.git_staged_root {
Some(GitMode::Staged)
} else if args.git_untracked || args.git_untracked_root {
Some(GitMode::Untracked)
} else if args.git_tracked || args.git_root {
Some(GitMode::Tracked)
} else if args.git_history || args.git_history_root {
Some(GitMode::History)
} else {
None
};
let use_git_root = args.git_root || args.git_untracked_root
|| args.git_staged_root || args.git_changed_root || args.git_history_root;
if git_mode.is_some() {
if Repository::discover(&args.path).is_err() {
eprintln!("Not in a git repository");
return;
}
}
let start_path = if use_git_root {
if let Ok(repo) = Repository::discover(&args.path) {
if let Some(workdir) = repo.workdir() {
workdir.to_path_buf()
} else {
args.path.clone()
}
} else {
eprintln!("Not in a git repository");
return;
}
} else {
args.path.clone()
};
let (skip_defaults, skip_config, skip_specific) = match args.no_ignore {
Some(ref mode) => match mode.as_str() {
"all" => (true, true, None),
"defaults" => (true, false, None),
"config" => (false, true, None),
pattern => (false, false, Some(pattern.to_string())),
},
None => (false, false, None),
};
let config_patterns = if skip_config {
Vec::new()
} else {
load_config_patterns()
};
let mut custom_ignores = Vec::new();
for pattern in config_patterns {
let pattern = pattern.replace("*", ".*");
if let Ok(re) = Regex::new(&format!("^{}$", pattern)) {
custom_ignores.push(re);
}
}
if let Some(patterns) = args.ignore_patterns {
for pattern in patterns.split(',') {
let pattern = pattern.trim().replace("*", ".*");
if let Ok(re) = Regex::new(&format!("^{}$", pattern)) {
custom_ignores.push(re);
}
}
}
let git_files = if let Some(ref mode) = git_mode {
match mode {
GitMode::Tracked => get_git_tracked_files(&start_path),
GitMode::Untracked => get_git_untracked_files(&start_path),
GitMode::Staged => get_git_staged_files(&start_path),
GitMode::Changed => get_git_changed_files(&start_path),
GitMode::History => None, }
} else {
None
};
let config = StructConfig {
depth,
custom_ignores,
max_size_bytes,
git_files,
git_mode,
show_size: args.show_size,
skip_defaults,
skip_specific,
};
if args.depth == Some(0) {
display_summary(&start_path);
return;
}
println!("{}", start_path.display().to_string().cyan());
display_tree(&start_path, &config, 0, "", true);
}