#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
use anyhow::{Context, Result, anyhow};
use clap::Parser;
use colored::{Color, Colorize};
use crossbeam::channel::unbounded;
use rayon::{
ThreadPoolBuilder,
iter::{ParallelBridge, ParallelIterator},
};
use std::{
env::{split_paths, var_os},
process::exit,
sync::Arc,
};
use superwhich::{SearchCtx, find_files};
#[derive(Debug, Parser)]
#[command(author,version,about,long_about=None)]
struct App {
#[arg(help = "The search pattern")]
pattern: String,
#[arg(
short,
long,
help = "Color of the highlighted text (off or set `NO_COLOR` env var to disable)",
default_value = "blue"
)]
color: Color,
#[arg(short, long, help = "Number of threads to use (0 for auto)", default_value_t = get_threads())]
threads: usize,
#[arg(
short = 'T',
long,
help = "String similarity threshold (0.0 to 1.0)",
default_value_t = 0.7
)]
threshold: f64,
}
fn main() {
if let Err(e) = main_impl() {
eprintln!("{}", format!("superwhich: {e:?}").red());
exit(1);
}
}
fn main_impl() -> Result<()> {
let args = App::parse();
if args.pattern.trim().is_empty() {
return Err(anyhow!("search pattern cannot be empty"));
}
if args.threads > 0 {
ThreadPoolBuilder::new()
.num_threads(args.threads)
.build_global()
.context("failed to set number of threads")?;
}
let paths_str = var_os("PATH").ok_or(anyhow!("PATH is not set"))?;
let ctx = Arc::new(
SearchCtx::new(args.pattern)
.threshold(args.threshold)
.color(args.color),
);
let (tx, rx) = unbounded();
split_paths(&paths_str).par_bridge().for_each(|path| {
find_files(&path, &ctx, &tx);
});
while let Ok(path) = rx.try_recv() {
println!("{path}");
}
Ok(())
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn get_threads() -> usize {
let cpus = num_cpus::get();
match cpus {
0 => 1,
_ => (cpus as f32 * 0.75).round() as usize,
}
}