superwhich 3.0.1

Cross-platform smart which alternative
Documentation
//! # superwhich
//!
//! Smart `which` alternative
//!
//! ## Installation
//!
//! ```bash
//! cargo install superwhich --features cli
//! ```
//!
//! ## Usage
//!
//! ```bash
//! $ swhich -h
//! Cross-platform smart which alternative
//!
//! Usage: swhich [OPTIONS] <PATTERN>
//!
//! Arguments:
//!   <PATTERN>  The search pattern
//!
//! Options:
//!   -c, --color <COLOR>          Color of the highlighted text (off or set `NO_COLOR` env var to disable) [default: blue]
//!   -t, --threads <THREADS>      Number of threads to use (0 for auto) [default: 75% of CPU cores]
//!   -T, --threshold <THRESHOLD>  String similarity threshold (0.0 to 1.0) [default: 0.7]
//!   -h, --help                   Print help
//!   -V, --version                Print version
//! ```

#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]

/*
 * superwhich: smart which alternative
 * Copyright (C) 2024 `DarkCeptor44`
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

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,
    }
}