mod cli;
mod color;
mod fsops;
mod table;
use clap::Parser;
use cli::{Cli, Commands, OutputFormat, SortBy, ThemeSubcommand};
use color::{create_sample_config, load_theme};
use fsops::{
get_files, get_files_recursive, matches_extension, matches_pattern, parse_size, FileEntry,
};
use glob::Pattern;
use owo_colors::OwoColorize;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use table::format_table;
#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
enum ConfigError {
InvalidGlobPattern(String),
InvalidMinSize(String),
InvalidMaxSize(String),
SizeRangeInvalid(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::InvalidGlobPattern(e) => write!(f, "invalid glob pattern: {}", e),
ConfigError::InvalidMinSize(e) => write!(f, "invalid --min-size value: {}", e),
ConfigError::InvalidMaxSize(e) => write!(f, "invalid --max-size value: {}", e),
ConfigError::SizeRangeInvalid(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for ConfigError {}
struct FilterConfig {
exts: Option<Vec<String>>,
name_pattern: Option<Pattern>,
min_size: Option<u64>,
max_size: Option<u64>,
}
impl FilterConfig {
fn from_cli(cli: &Cli) -> Result<Self, ConfigError> {
let exts = cli.filter_ext.as_ref().map(|ext_filter| {
ext_filter
.split(',')
.map(|s| s.trim().trim_start_matches('.').to_lowercase())
.collect::<Vec<_>>()
});
let name_pattern = match cli.filter_name.as_deref() {
Some(pattern_str) => match Pattern::new(pattern_str) {
Ok(pattern) => Some(pattern),
Err(e) => {
return Err(ConfigError::InvalidGlobPattern(format!(
"invalid glob pattern '{}': {}",
pattern_str, e
)))
}
},
None => None,
};
let min_size = if let Some(min_str) = cli.min_size.as_deref() {
match parse_size(min_str) {
Ok(size) => Some(size),
Err(e) => return Err(ConfigError::InvalidMinSize(e.to_string())),
}
} else {
None
};
let max_size = if let Some(max_str) = cli.max_size.as_deref() {
match parse_size(max_str) {
Ok(size) => Some(size),
Err(e) => return Err(ConfigError::InvalidMaxSize(e.to_string())),
}
} else {
None
};
if let (Some(min), Some(max)) = (min_size, max_size) {
if min > max {
return Err(ConfigError::SizeRangeInvalid(format!(
"--min-size ({}) must be less than or equal to --max-size ({})",
min, max
)));
}
}
Ok(FilterConfig {
exts,
name_pattern,
min_size,
max_size,
})
}
}
fn passes_filters(f: &FileEntry, cfg: &FilterConfig) -> bool {
if let Some(ref exts) = cfg.exts {
if !matches_extension(&f.name, exts) {
return false;
}
}
if let Some(ref name_pattern) = cfg.name_pattern {
if !matches_pattern(&f.name, name_pattern) {
return false;
}
}
if let Some(min) = cfg.min_size {
if f.len_bytes < min {
return false;
}
}
if let Some(max) = cfg.max_size {
if f.len_bytes > max {
return false;
}
}
true
}
fn load_files(cli: &Cli, path: &Path, include_hidden: bool) -> std::io::Result<Vec<FileEntry>> {
if cli.tree {
get_files_recursive(path, include_hidden, cli.depth)
} else {
get_files(path, include_hidden)
}
}
fn handle_theme_command(subcommand: &ThemeSubcommand) {
match subcommand {
ThemeSubcommand::Init { show } => match create_sample_config() {
Ok(path) => {
println!("Theme config created at: {}", path.display());
if *show {
match std::fs::read_to_string(&path) {
Ok(content) => println!("\n{}", content),
Err(e) => eprintln!("Error reading config: {}", e),
}
}
}
Err(e) => eprintln!("Error creating config: {}", e),
},
ThemeSubcommand::Path => {
if let Some(config_dir) = dirs::config_dir() {
let config_path = config_dir.join("bestls").join("config.toml");
println!("{}", config_path.display());
} else {
eprintln!("Could not determine config directory");
}
}
ThemeSubcommand::Reset => {
if let Some(config_dir) = dirs::config_dir() {
let config_path = config_dir.join("bestls").join("config.toml");
if config_path.exists() {
match std::fs::remove_file(&config_path) {
Ok(_) => println!("Theme reset to default (config file removed)"),
Err(e) => eprintln!("Error removing config: {}", e),
}
} else {
println!("Theme already at default (no config file found)");
}
} else {
eprintln!("Could not determine config directory");
}
}
}
}
fn main() {
let cli: Cli = Cli::parse();
if let Some(command) = &cli.command {
match command {
Commands::Completion { shell } => {
Cli::generate_completion(*shell);
return;
}
Commands::Theme { subcommand } => {
handle_theme_command(subcommand);
return;
}
}
}
let theme = load_theme();
let path: PathBuf = cli
.path
.as_deref()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."));
let include_hidden: bool = cli.all;
let filter_cfg = match FilterConfig::from_cli(&cli) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(2);
}
};
let get_result = load_files(&cli, &path, include_hidden);
match get_result {
Ok(mut files) => {
files.retain(|f| passes_filters(f, &filter_cfg));
match cli.sort_by {
SortBy::Name => {
files.sort_by(|a: &fsops::FileEntry, b: &fsops::FileEntry| a.name.cmp(&b.name))
}
SortBy::Size => files.sort_by(|a: &fsops::FileEntry, b: &fsops::FileEntry| {
a.len_bytes.cmp(&b.len_bytes)
}),
SortBy::Date => files.sort_by(|a: &fsops::FileEntry, b: &fsops::FileEntry| {
a.modified.cmp(&b.modified)
}),
}
let effective_format = cli.effective_format();
let output = match effective_format {
OutputFormat::Json => {
serde_json::to_string(&files).unwrap_or_else(|_| "cannot parse to JSON".into())
}
OutputFormat::JsonPretty => serde_json::to_string_pretty(&files)
.unwrap_or_else(|_| "cannot parse to JSON".into()),
OutputFormat::Table => {
format_table(
&files,
cli.columns.clone(),
cli.compact,
!cli.no_color,
Some(&theme),
)
}
};
if let Some(file_path) = &cli.output_file {
match File::create(file_path) {
Ok(mut file) => {
if let Err(e) = writeln!(file, "{}", output) {
eprintln!("{}: {}", "Failed to write to file".red(), e);
}
}
Err(e) => {
eprintln!("{}: {}", "Failed to create output file".red(), e);
}
}
} else {
println!("{}", output);
}
}
Err(e) => eprintln!("{}: {}", "Failed to read directory".red(), e),
}
}