#![warn(deprecated_in_future)]
#![warn(future_incompatible)]
#![warn(nonstandard_style)]
#![warn(rust_2018_compatibility)]
#![warn(rust_2018_idioms)]
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused)]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::enum_glob_use)]
#![allow(clippy::map_unwrap_or)]
#![allow(clippy::match_same_arms)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::non_ascii_literal)]
#![allow(clippy::option_if_let_else)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::unused_self)]
#![allow(clippy::upper_case_acronyms)]
#![allow(clippy::wildcard_imports)]
use std::env;
use std::ffi::OsString;
use std::io::{self, Write, ErrorKind};
use std::path::{Component, PathBuf};
use nu_ansi_term::{AnsiStrings, Style};
use log::*;
use crate::fs::{Dir, File};
use crate::fs::feature::git::GitCache;
use crate::fs::feature::jj::JjCache;
use crate::fs::feature::VcsCache;
use crate::fs::filter::VcsIgnore;
use crate::options::{Options, VcsBackend, Vars, vars, OptionsResult};
use crate::output::{escape, lines, grid, grid_details, details, View, Mode};
use crate::theme::Theme;
mod config;
mod fs;
mod logger;
mod options;
mod output;
mod theme;
fn main() {
use std::process::exit;
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
logger::configure(env::var_os(vars::LX_DEBUG));
#[cfg(windows)]
if let Err(e) = nu_ansi_term::enable_ansi_support() {
warn!("Failed to enable ANSI support: {}", e);
}
let cli_args: Vec<OsString> = env::args_os().skip(1).collect();
let upgrading = cli_args.iter().any(|a| a == "--upgrade-config");
if !upgrading {
let _ = &*config::CONFIG;
}
let mut args: Vec<OsString> = Vec::new();
let mut active_personality: Option<String> = None;
if !upgrading {
let explicit_personality = find_personality_arg(&cli_args);
let personality_name = explicit_personality.or_else(|| {
let argv0 = env::args().next()?;
let bin_name = std::path::Path::new(&argv0)
.file_name()?
.to_string_lossy()
.to_string();
match config::resolve_personality(&bin_name) {
Ok(Some(_)) | Err(_) => {
debug!("argv[0] dispatch: {bin_name}");
Some(bin_name)
}
Ok(None) => {
debug!("argv[0] is '{bin_name}' but no matching personality found; using default");
None
}
}
});
let personality_name = personality_name.unwrap_or_else(|| "lx".to_string());
{
match config::resolve_personality(&personality_name) {
Ok(Some(personality)) => {
args.extend(personality.to_args());
active_personality = Some(personality_name.clone());
}
Ok(None) => {} Err(e) => {
eprintln!("lx: {e}");
std::process::exit(exits::OPTIONS_ERROR);
}
}
}
}
args.extend(cli_args);
match Options::parse(&args, &LiveVars) {
OptionsResult::Ok(options, mut input_paths) => {
if input_paths.is_empty() {
input_paths = vec![ OsString::from(".") ];
}
let vcs = vcs_cache(&options, &input_paths);
let writer = io::stdout();
let console_width = options.view.width.actual_terminal_width();
let theme = options.theme.to_theme(console_width.is_some());
let lx = Lx { options, writer, input_paths, theme, console_width, vcs };
match lx.run() {
Ok(exit_status) => {
exit(exit_status);
}
Err(e) if e.kind() == ErrorKind::BrokenPipe => {
warn!("Broken pipe error: {e}");
exit(exits::SUCCESS);
}
Err(e) => {
eprintln!("{e}");
exit(exits::RUNTIME_ERROR);
}
}
}
OptionsResult::HelpOrVersion(clap_err) => {
clap_err.exit();
}
OptionsResult::InvalidOptionsClap(clap_err) => {
clap_err.exit();
}
OptionsResult::Completions(shell) => {
let mut cmd = crate::options::parser::build_command();
clap_complete::generate(shell, &mut cmd, "lx", &mut io::stdout());
}
OptionsResult::InitConfig => {
let path = config::init_config_path();
match config::write_init_config(&path) {
Ok(()) => {
eprintln!("Wrote default config to {}", path.display());
}
Err(e) => {
eprintln!("lx: failed to write config to {}: {e}", path.display());
exit(exits::RUNTIME_ERROR);
}
}
}
OptionsResult::ShowConfig => {
let name = active_personality.as_deref().unwrap_or("lx");
config::show_config(name);
}
OptionsResult::DumpClass(ref name) => {
if name.is_empty() {
config::show_class_all();
} else {
config::show_class(name);
}
}
OptionsResult::DumpFormat(ref name) => {
if name.is_empty() {
config::show_format_all();
} else {
config::show_format(name);
}
}
OptionsResult::DumpPersonality(ref name) => {
if name.is_empty() {
config::dump_personality_all();
} else {
config::dump_personality(name);
}
}
OptionsResult::DumpTheme(ref name) => {
if name.is_empty() {
config::dump_theme_all();
} else {
config::dump_theme(name);
}
}
OptionsResult::DumpStyle(ref name) => {
if name.is_empty() {
config::dump_style_all();
} else {
config::dump_style(name);
}
}
OptionsResult::UpgradeConfig => {
let Some(path) = config::find_config_path() else {
eprintln!("lx: no config file found to upgrade");
exit(exits::RUNTIME_ERROR);
};
if let Err(e) = config::upgrade_config(&path) {
eprintln!("lx: {e}");
exit(exits::RUNTIME_ERROR);
}
}
OptionsResult::InvalidOptions(error) => {
eprintln!("lx: {error}");
exit(exits::OPTIONS_ERROR);
}
}
}
pub struct Lx {
pub options: Options,
pub writer: io::Stdout,
pub input_paths: Vec<OsString>,
pub theme: Theme,
pub console_width: Option<usize>,
pub vcs: Option<Box<dyn VcsCache>>,
}
struct LiveVars;
impl Vars for LiveVars {
fn get(&self, name: &'static str) -> Option<OsString> {
env::var_os(name)
}
}
fn discover_jj(paths: &[PathBuf]) -> Option<Box<dyn VcsCache>> {
JjCache::discover(paths).map(|c| {
let b: Box<dyn VcsCache> = Box::new(c);
b
})
}
fn vcs_cache(options: &Options, args: &[OsString]) -> Option<Box<dyn VcsCache>> {
if !options.should_scan_for_vcs() {
return None;
}
let paths: Vec<PathBuf> = args.iter().map(PathBuf::from).collect();
match options.vcs_backend {
VcsBackend::None => None,
VcsBackend::Git => {
let cache: GitCache = paths.into_iter().collect();
Some(Box::new(cache))
}
VcsBackend::Jj => {
discover_jj(&paths)
}
VcsBackend::Auto => {
if let Some(jj) = discover_jj(&paths) {
Some(jj)
} else {
let cache: GitCache = paths.into_iter().collect();
Some(Box::new(cache))
}
}
}
}
impl Lx {
pub fn run(mut self) -> io::Result<i32> {
debug!("Running with options: {:#?}", self.options);
let mut files = Vec::new();
let mut dirs = Vec::new();
let mut exit_status = 0;
for file_path in &self.input_paths {
match File::from_args(PathBuf::from(file_path), None, None) {
Err(e) => {
exit_status = 2;
writeln!(io::stderr(), "{}: {e}", file_path.to_string_lossy())?;
}
Ok(f) => {
if f.points_to_directory() && ! self.options.dir_action.treat_dirs_as_files() {
match f.to_dir() {
Ok(d) => dirs.push(d),
Err(e) => writeln!(io::stderr(), "{}: {e}", file_path.to_string_lossy())?,
}
}
else {
files.push(f);
}
}
}
}
let no_files = files.is_empty();
let is_only_dir = dirs.len() == 1 && no_files;
self.options.filter.filter_argument_files(&mut files);
self.print_files(None, files)?;
self.print_dirs(dirs, no_files, is_only_dir, exit_status)
}
fn print_dirs(&mut self, dir_files: Vec<Dir>, mut first: bool, is_only_dir: bool, exit_status: i32) -> io::Result<i32> {
for dir in dir_files {
if first {
first = false;
}
else {
writeln!(&mut self.writer)?;
}
if ! is_only_dir {
let mut bits = Vec::new();
escape(dir.path.display().to_string(), &mut bits, Style::default(), Style::default());
writeln!(&mut self.writer, "{}:", AnsiStrings(&bits))?;
}
let mut children = Vec::new();
let vcs_ignore = self.options.filter.vcs_ignore == VcsIgnore::CheckAndIgnore;
for file in dir.files(self.options.filter.dot_filter, self.vcs.as_deref(), vcs_ignore) {
match file {
Ok(file) => children.push(file),
Err((path, e)) => writeln!(io::stderr(), "[{}: {}]", path.display(), e)?,
}
};
self.options.filter.filter_child_files(&mut children);
self.options.filter.sort_files(&mut children);
if let Some(recurse_opts) = self.options.dir_action.recurse_options() {
let depth = dir.path.components().filter(|&c| c != Component::CurDir).count() + 1;
if ! recurse_opts.tree && ! recurse_opts.is_too_deep(depth) {
let mut child_dirs = Vec::new();
for child_dir in children.iter().filter(|f| f.is_directory() && ! f.is_all_all && ! self.options.filter.is_pruned(f)) {
match child_dir.to_dir() {
Ok(d) => child_dirs.push(d),
Err(e) => writeln!(io::stderr(), "{}: {}", child_dir.path.display(), e)?,
}
}
self.print_files(Some(&dir), children)?;
match self.print_dirs(child_dirs, false, false, exit_status) {
Ok(_) => (),
Err(e) => return Err(e),
}
continue;
}
}
self.print_files(Some(&dir), children)?;
}
Ok(exit_status)
}
fn print_files(&mut self, dir: Option<&Dir>, files: Vec<File<'_>>) -> io::Result<()> {
if files.is_empty() {
return Ok(());
}
let theme = &self.theme;
let View { mode, file_style, .. } = &self.options.view;
match (mode, self.console_width) {
(Mode::Grid(opts), Some(console_width)) => {
let filter = &self.options.filter;
let r = grid::Render { files, theme, file_style, opts, console_width, filter };
r.render(&mut self.writer)
}
(Mode::Grid(_), None) |
(Mode::Lines, _) => {
let filter = &self.options.filter;
let r = lines::Render { files, theme, file_style, filter };
r.render(&mut self.writer)
}
(Mode::Details(opts), _) => {
let filter = &self.options.filter;
let recurse = self.options.dir_action.recurse_options();
let vcs_ignoring = self.options.filter.vcs_ignore == VcsIgnore::CheckAndIgnore;
let vcs = self.vcs.as_deref();
let r = details::Render { dir, files, theme, file_style, opts, recurse, filter, vcs_ignoring, vcs };
r.render(&mut self.writer)
}
(Mode::GridDetails(opts), Some(console_width)) => {
let grid = &opts.grid;
let details = &opts.details;
let row_threshold = opts.row_threshold;
let filter = &self.options.filter;
let vcs_ignoring = self.options.filter.vcs_ignore == VcsIgnore::CheckAndIgnore;
let vcs = self.vcs.as_deref();
let r = grid_details::Render { dir, files, theme, file_style, grid, details, filter, row_threshold, vcs_ignoring, vcs, console_width };
r.render(&mut self.writer)
}
(Mode::GridDetails(opts), None) => {
let opts = &opts.to_details_options();
let filter = &self.options.filter;
let recurse = self.options.dir_action.recurse_options();
let vcs_ignoring = self.options.filter.vcs_ignore == VcsIgnore::CheckAndIgnore;
let vcs = self.vcs.as_deref();
let r = details::Render { dir, files, theme, file_style, opts, recurse, filter, vcs_ignoring, vcs };
r.render(&mut self.writer)
}
}
}
}
fn find_personality_arg(args: &[OsString]) -> Option<String> {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
let s = arg.to_string_lossy();
if let Some(name) = s.strip_prefix("--personality=") {
return Some(name.to_string());
}
if (s == "--personality" || s == "-p")
&& let Some(next) = iter.next() {
return Some(next.to_string_lossy().to_string());
}
if let Some(name) = s.strip_prefix("-p")
&& !name.is_empty() && !name.starts_with('-') {
return Some(name.to_string());
}
}
None
}
mod exits {
pub const SUCCESS: i32 = 0;
pub const RUNTIME_ERROR: i32 = 1;
pub const OPTIONS_ERROR: i32 = 3;
}