1#![doc = include_str!("../README.md")]
2
3use common::cache::{self, CacheStatic};
4use dashmap::DashMap;
5use rayon::prelude::*;
6use std::{
7 borrow::Cow,
8 env,
9 path::{Path, PathBuf},
10 sync::LazyLock,
11};
12
13#[derive(Debug)]
15pub enum Error {
16 NotFound(String),
18}
19impl std::fmt::Display for Error {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 Self::NotFound(e) => write!(f, "Could not find {e} in path"),
23 }
24 }
25}
26impl std::error::Error for Error {}
27
28pub static PATH: LazyLock<Vec<PathBuf>> = LazyLock::new(|| {
31 let path = env::var("PATH").unwrap_or("/usr/bin".to_string());
32 path.split(':')
33 .filter(|e| !e.contains("/.local/bin"))
34 .map(PathBuf::from)
35 .filter(|root| root.exists())
36 .collect::<Vec<_>>()
37});
38
39static CACHE: CacheStatic<String, Cow<'static, str>> = LazyLock::new(DashMap::default);
41
42static WHICH: LazyLock<cache::Cache<String, Cow<'static, str>>> =
44 LazyLock::new(|| cache::Cache::new(&CACHE));
45
46pub fn which(path: &str) -> Result<&'static str, Error> {
52 match WHICH.get(path) {
53 Some(resolved) => Ok(resolved),
54 None => {
55 let resolved = if Path::new(path).exists() {
56 path.to_string()
57 } else {
58 PATH.par_iter()
59 .find_map_any(|root: &PathBuf| {
60 let candidate = root.join(path);
61 if candidate.exists() {
62 Some(candidate.to_string_lossy().into_owned())
63 } else {
64 None
65 }
66 })
67 .ok_or(Error::NotFound(path.to_string()))?
68 };
69 Ok(WHICH.insert(path.to_string(), Cow::Owned(resolved)))
70 }
71 }
72}