mod cli;
mod colors;
mod config;
mod entry;
mod git;
mod icons;
mod render;
mod sorter;
mod theme;
mod util;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{Context, Result};
use clap::{CommandFactory, FromArgMatches};
use clap_version_flag::colorful_version;
use cli::{Cli, ColorWhen};
use entry::{read_dir_entries, FileEntry};
use git::{git_available, status_for_dir, GitRepoInfo};
use render::RenderCtx;
use theme::{init_config_dir, Theme};
use util::PROG_NAME;
fn parse_cli() -> Cli {
let name = PROG_NAME.as_str();
let cmd = Cli::command().name(name).bin_name(name);
let matches = cmd.get_matches();
match Cli::from_arg_matches(&matches) {
Ok(cli) => cli,
Err(e) => e.exit(),
}
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
if args.len() == 2 && (args[1] == "-V" || args[1] == "--version") {
let version = colorful_version!();
version.print_and_exit();
}
#[cfg(windows)]
{
let _ = colored::control::set_virtual_terminal(true);
}
let cli = parse_cli();
match run(cli) {
Ok(code) => code,
Err(e) => {
eprintln!("{}: error: {:#}", PROG_NAME.as_str(), e);
ExitCode::FAILURE
}
}
}
fn run(mut cli: Cli) -> Result<ExitCode> {
let resolved = config::load(cli.config_path.as_deref())?;
if cli.print_config_dir {
match &resolved.dir {
Some(d) => crate::oprintln!("{}", d.display()),
None => crate::oprintln!("(no config directory could be resolved on this platform)"),
}
return Ok(ExitCode::SUCCESS);
}
if cli.init_config {
let dir = resolved
.dir
.clone()
.context("could not resolve a config directory on this platform")?;
let written = init_config_dir(&dir)?;
if written.is_empty() {
crate::oprintln!(
"{}: config already present at {} (nothing overwritten)",
PROG_NAME.as_str(),
dir.display()
);
} else {
crate::oprintln!(
"{}: wrote default config to {}",
PROG_NAME.as_str(),
dir.display()
);
for f in written {
crate::oprintln!(" {}", f);
}
}
return Ok(ExitCode::SUCCESS);
}
let settings = resolved.settings.clone();
if !cli.light && !cli.dark {
if let Some(theme_name) = &settings.theme {
if theme_name.eq_ignore_ascii_case("light") {
cli.light = true;
}
}
}
if !cli.git_status {
cli.git_status = settings.git_status.unwrap_or(false);
}
if !cli.group_directories_first {
cli.group_directories_first = settings.group_directories_first.unwrap_or(false);
}
if !cli.sort_files_first {
cli.sort_files_first = settings.sort_files_first.unwrap_or(false);
}
if !cli.report {
cli.report = settings.report.unwrap_or(false);
}
if !cli.all && !cli.almost_all && settings.all.unwrap_or(false) {
cli.all = true;
}
if !cli.long {
cli.long = settings.long.unwrap_or(false);
}
if let Some(depth) = cli.tree {
if depth == 0 {
cli.tree = Some(settings.tree_depth.unwrap_or(3).max(1));
}
}
let icons_enabled = if cli.no_icons {
false
} else if cli.icons {
true
} else {
settings.icons.unwrap_or(true)
};
let color_enabled = match cli.color {
ColorWhen::Always => true,
ColorWhen::Never => false,
ColorWhen::Auto => {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
};
colored::control::set_override(color_enabled);
let theme = Theme::load(resolved.dir.as_deref(), cli.light)
.context("failed to load color/icon theme")?;
let paths: Vec<PathBuf> = if cli.paths.is_empty() {
vec![PathBuf::from(".")]
} else {
cli.paths.clone()
};
let mut any_error = false;
let show_headers = paths.len() > 1;
for (idx, path) in paths.iter().enumerate() {
if idx > 0 {
crate::oprintln!();
}
if let Err(e) = list_one(
path,
&cli,
&theme,
color_enabled,
icons_enabled,
show_headers,
) {
eprintln!("{}: {}: {:#}", PROG_NAME.as_str(), path.display(), e);
any_error = true;
}
}
if any_error {
Ok(ExitCode::FAILURE)
} else {
Ok(ExitCode::SUCCESS)
}
}
fn list_one(
path: &Path,
cli: &Cli,
theme: &Theme,
color_enabled: bool,
icons_enabled: bool,
show_headers: bool,
) -> Result<()> {
if !path.exists() {
anyhow::bail!("no such file or directory");
}
let git_available_flag = if cli.git_status {
let avail = git_available();
if !avail && !cli.quiet {
eprintln!(
"{}: warning: `git` executable not found; --gs disabled",
PROG_NAME.as_str()
);
}
avail
} else {
false
};
let git_info: Option<GitRepoInfo> = if git_available_flag {
let target_dir = if path.is_dir() {
path
} else {
path.parent().unwrap_or_else(|| Path::new("."))
};
status_for_dir(target_dir)
} else {
None
};
let ctx = RenderCtx {
cli,
theme,
color_enabled,
icons_enabled,
git: git_info.as_ref(),
};
if let Some(info) = &git_info {
if let Some(branch) = &info.branch {
let text = if icons_enabled {
format!("\u{e0a0} {}", branch)
} else {
branch.clone()
};
let label = crate::colors::paint(&text, "git_branch", theme, color_enabled);
crate::oprintln!("{}", label);
}
}
if path.is_file() {
if show_headers {
crate::oprintln!("{}:", path.display());
}
let entry = FileEntry::from_path(path).with_context(|| "reading metadata")?;
if cli.long {
render::long::render(std::slice::from_ref(&entry), &ctx);
} else {
render::grid::render(std::slice::from_ref(&entry), &ctx);
}
return Ok(());
}
if show_headers {
crate::oprintln!("{}:", path.display());
}
if cli.tree.is_some() {
render::tree::render(path, cli, &ctx, git_info.as_ref());
return Ok(());
}
if cli.recursive {
list_dir_recursive(path, cli, &ctx, theme, color_enabled, true)?;
return Ok(());
}
let entries = list_dir_flat(path, cli)?;
if cli.long {
render::long::render(&entries, &ctx);
} else {
render::grid::render(&entries, &ctx);
}
if cli.report {
print_report(&entries, theme, color_enabled);
}
Ok(())
}
fn list_dir_flat(path: &Path, cli: &Cli) -> Result<Vec<FileEntry>> {
let mut entries =
read_dir_entries(path).with_context(|| format!("reading directory {}", path.display()))?;
if !cli.show_hidden() {
entries.retain(|e| !e.is_hidden());
}
sorter::sort_entries(&mut entries, cli);
if cli.include_dot_entries() {
if let Ok(dots) = entry::dot_entries(path) {
let mut out = dots.to_vec();
out.extend(entries);
entries = out;
}
}
Ok(entries)
}
fn list_dir_recursive(
path: &Path,
cli: &Cli,
ctx: &RenderCtx,
theme: &Theme,
color_enabled: bool,
is_first: bool,
) -> Result<()> {
if !is_first {
crate::oprintln!();
crate::oprintln!("{}:", path.display());
}
let entries = list_dir_flat(path, cli)?;
if cli.long {
render::long::render(&entries, ctx);
} else {
render::grid::render(&entries, ctx);
}
if cli.report {
print_report(&entries, theme, color_enabled);
}
let mut subdirs: Vec<PathBuf> = entries
.iter()
.filter(|e| e.is_dir && !e.is_symlink && e.name != "." && e.name != "..")
.map(|e| e.path.clone())
.collect();
subdirs.sort();
for sub in subdirs {
list_dir_recursive(&sub, cli, ctx, theme, color_enabled, false)?;
}
Ok(())
}
fn print_report(entries: &[FileEntry], theme: &Theme, color_enabled: bool) {
let dirs = entries.iter().filter(|e| e.is_dir).count();
let files = entries.len() - dirs;
let total_size: u64 = entries.iter().filter(|e| !e.is_dir).map(|e| e.size).sum();
let line = format!(
"\n{} directories, {} files, {} total",
dirs,
files,
util::human_size(total_size)
);
crate::oprintln!(
"{}",
crate::colors::paint(&line, "report", theme, color_enabled)
);
}