diff --git a/crates/hyfetch/src/neofetch_util.rs b/crates/hyfetch/src/neofetch_util.rs
index dac766f2..09ba7b0b 100644
--- a/crates/hyfetch/src/neofetch_util.rs
+++ b/crates/hyfetch/src/neofetch_util.rs
@@ -15,9 +15,10 @@ use itertools::Itertools as _;
use anyhow::anyhow;
#[cfg(feature = "macchina")]
use crate::models::Palette;
-#[cfg(feature = "macchina")]
#[cfg(windows)]
use crate::utils::find_file;
+#[cfg(any(feature = "macchina", windows))]
+use crate::utils::find_in_path;
#[cfg(windows)]
use std::path::Path;
#[cfg(windows)]
@@ -36,7 +37,7 @@ use crate::color_util::{printc, NeofetchAsciiIndexedColor, PresetIndexedColor};
use crate::distros::Distro;
use crate::types::{AnsiMode, Backend};
#[cfg(feature = "macchina")]
-use crate::utils::{find_in_path, get_cache_path, input, process_command_status};
+use crate::utils::{get_cache_path, input, process_command_status};
#[cfg(not(feature = "macchina"))]
use crate::utils::{get_cache_path, input, process_command_status};
@@ -356,56 +357,140 @@ where
Ok((width, height))
}
+#[cfg(windows)]
+fn find_all_in_path
(program: P) -> Result>
+where
+ P: AsRef,
+{
+ let program = program.as_ref();
+
+ if program.parent() != Some(Path::new("")) {
+ return Err(anyhow!("invalid command name {program:?}"));
+ };
+
+ let path_env = env::var_os("PATH").context("`PATH` env var is not set or invalid")?;
+ let mut matches = Vec::new();
+
+ for search_path in env::split_paths(&path_env) {
+ let path = search_path.join(program);
+ if let Some(path) = find_file(&path)
+ .with_context(|| format!("failed to check existence of file {path:?}"))?
+ {
+ push_unique_path(&mut matches, path);
+ }
+ }
+
+ Ok(matches)
+}
+
+#[cfg(windows)]
+fn push_unique_path(paths: &mut Vec, path: PathBuf) {
+ if paths.iter().any(|existing| {
+ existing == &path || is_same_file(existing, &path).unwrap_or(false)
+ }) {
+ return;
+ }
+
+ paths.push(path);
+}
+
+#[cfg(windows)]
+fn push_existing_path(paths: &mut Vec, path: PathBuf) {
+ if let Ok(Some(path)) = find_file(path) {
+ push_unique_path(paths, path);
+ }
+}
+
+#[cfg(windows)]
+fn is_windows_path_compatible_bash(path: &Path) -> bool {
+ let mut script = match tempfile::Builder::new()
+ .prefix("hyfetch-bash-probe")
+ .suffix(".sh")
+ .tempfile()
+ {
+ Ok(script) => script,
+ Err(_) => return false,
+ };
+
+ if script.write_all(b"exit 86\n").is_err() {
+ return false;
+ }
+
+ let script_path = script.path().to_string_lossy().replace('\\', "/");
+ let status = Command::new(path)
+ .arg(script_path)
+ .stdin(std::process::Stdio::null())
+ .stdout(std::process::Stdio::null())
+ .stderr(std::process::Stdio::null())
+ .status();
+
+ matches!(status.ok().and_then(|status| status.code()), Some(86))
+}
+
+#[cfg(windows)]
+fn bash_paths_from_git(git_path: &Path) -> Vec {
+ let mut candidates = Vec::new();
+
+ let mut current = Some(git_path);
+ for _ in 0..6 {
+ let Some(parent) = current.and_then(Path::parent) else {
+ break;
+ };
+
+ push_existing_path(&mut candidates, parent.join(r"bin\bash.exe"));
+ push_existing_path(&mut candidates, parent.join(r"usr\bin\bash.exe"));
+ current = Some(parent);
+ }
+
+ if let Some(scoop_root) = scoop_root_from_git_path(git_path) {
+ for app in ["git-with-openssh", "git"] {
+ let app_root = scoop_root.join("apps").join(app).join("current");
+ push_existing_path(&mut candidates, app_root.join(r"bin\bash.exe"));
+ push_existing_path(&mut candidates, app_root.join(r"usr\bin\bash.exe"));
+ }
+ }
+
+ candidates
+}
+
+#[cfg(windows)]
+fn scoop_root_from_git_path(git_path: &Path) -> Option {
+ let parent = git_path.parent()?;
+ let parent_name = parent.file_name()?.to_string_lossy();
+ if !parent_name.eq_ignore_ascii_case("shims") {
+ return None;
+ }
+
+ parent.parent().map(Path::to_path_buf)
+}
+
/// Gets the absolute path of the bash command.
#[cfg(windows)]
fn bash_path() -> Result {
- // 1. Try to find a good bash.exe in PATH
- let bash_in_path = find_in_path("bash.exe").unwrap_or(None);
- if let Some(pth) = &bash_in_path {
- // Check if it's not WSL bash
- // See https://github.com/hykilpikonna/hyfetch/issues/233
- let is_wsl = (|| {
- let windir = env::var_os("windir")?;
- let wsl_bash = Path::new(&windir).join(r"System32\bash.exe");
- Some(is_same_file(pth, &wsl_bash).unwrap_or(false))
- })()
- .unwrap_or(false);
+ // 1. Try to find a good bash.exe in PATH.
+ let bash_candidates = find_all_in_path("bash.exe").unwrap_or_default();
+ if let Some(pth) = bash_candidates
+ .iter()
+ .find(|pth| is_windows_path_compatible_bash(pth))
+ {
+ return Ok(pth.clone());
+ }
- if !is_wsl {
- // Check if it's not MSYS bash https://stackoverflow.com/a/58418686/1529493
- // We prefer the Git wrapper bash if possible, but we'll accept this if it's all we have.
- if !pth.ends_with(r"Git\usr\bin\bash.exe") {
- return Ok(pth.clone());
+ // 2. Try to find git.exe in PATH and look for bash.exe relative to it.
+ let git_candidates = find_all_in_path("git.exe")
+ .or_else(|_| find_in_path("git.exe").map(|pth| pth.into_iter().collect()))
+ .unwrap_or_default();
+ for git_path in git_candidates {
+ for pth in bash_paths_from_git(&git_path) {
+ if is_windows_path_compatible_bash(&pth) {
+ return Ok(pth);
}
}
}
- // 2. Try to find git.exe in PATH and look for bash.exe relative to it
- if let Ok(Some(git_path)) = find_in_path("git.exe") {
- let mut current = git_path.clone();
- for _ in 0..3 {
- if let Some(parent) = current.parent() {
- let bin_bash = parent.join(r"bin\bash.exe");
- if bin_bash.is_file() {
- return Ok(bin_bash);
- }
- let usr_bin_bash = parent.join(r"usr\bin\bash.exe");
- if usr_bin_bash.is_file() {
- return Ok(usr_bin_bash);
- }
- current = parent.to_path_buf();
- } else {
- break;
- }
- }
- }
-
- // 3. Fallback to whatever bash we found in PATH (even if it was the MSYS one)
- if let Some(pth) = bash_in_path {
- return Ok(pth);
- }
-
- Err(anyhow!("bash.exe not found. Please ensure Git for Windows is installed and in your PATH."))
+ Err(anyhow!(
+ "compatible bash.exe not found. Please ensure Git for Windows is installed and in your PATH."
+ ))
}
/// Runs neofetch command, returning the piped stdout output.