use std::path::{Path, PathBuf};
use crate::core::CliError;
const BROWSER_PATH_ENV: &str = "SUNO_BROWSER_PATH";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TargetOs {
Macos,
Linux,
Windows,
}
pub fn locate_chromium_browser() -> Result<String, CliError> {
if let Some(path) = configured_browser_path(|key| std::env::var(key).ok())? {
return Ok(path);
}
for candidate in chromium_browser_candidates(current_target_os(), |key| {
std::env::var_os(key).map(PathBuf::from)
}) {
if candidate.exists() {
return Ok(candidate.display().to_string());
}
}
Err(CliError::Config(
"Could not find a Chrome, Edge, or Chromium binary. Install a Chromium-based browser or set SUNO_BROWSER_PATH."
.into(),
))
}
fn configured_browser_path<F>(env_var: F) -> Result<Option<String>, CliError>
where
F: Fn(&str) -> Option<String>,
{
let Some(path) = env_var(BROWSER_PATH_ENV) else {
return Ok(None);
};
let path = path.trim();
if path.is_empty() {
return Ok(None);
}
if Path::new(path).exists() {
return Ok(Some(path.to_string()));
}
Err(CliError::Config(format!(
"{BROWSER_PATH_ENV} points to a missing file: {path}"
)))
}
fn current_target_os() -> TargetOs {
if cfg!(target_os = "macos") {
TargetOs::Macos
} else if cfg!(target_os = "linux") {
TargetOs::Linux
} else {
TargetOs::Windows
}
}
fn chromium_browser_candidates<F>(target_os: TargetOs, env_var: F) -> Vec<PathBuf>
where
F: Fn(&str) -> Option<PathBuf>,
{
match target_os {
TargetOs::Macos => [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
]
.into_iter()
.map(PathBuf::from)
.collect(),
TargetOs::Linux => [
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/microsoft-edge",
"/usr/bin/microsoft-edge-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
]
.into_iter()
.map(PathBuf::from)
.collect(),
TargetOs::Windows => {
let mut candidates = Vec::new();
if let Some(local_app_data) = env_var("LOCALAPPDATA") {
candidates.push(PathBuf::from(format!(
r"{}\Google\Chrome\Application\chrome.exe",
local_app_data.display()
)));
candidates.push(PathBuf::from(format!(
r"{}\Microsoft\Edge\Application\msedge.exe",
local_app_data.display()
)));
}
candidates.extend([
PathBuf::from(r"C:\Program Files\Google\Chrome\Application\chrome.exe"),
PathBuf::from(r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"),
PathBuf::from(r"C:\Program Files\Microsoft\Edge\Application\msedge.exe"),
PathBuf::from(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"),
]);
candidates
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn browser_path_env_uses_configured_browser_path() {
let browser_path =
std::env::temp_dir().join(format!("sunox-browser-path-test-{}", uuid::Uuid::new_v4()));
std::fs::write(&browser_path, "").expect("browser stub");
let configured = configured_browser_path(|key| match key {
BROWSER_PATH_ENV => Some(browser_path.display().to_string()),
_ => None,
})
.expect("configured path")
.expect("path");
assert_eq!(configured, browser_path.display().to_string());
let _ = std::fs::remove_file(browser_path);
}
#[test]
fn chrome_path_env_is_ignored() {
let browser_path =
std::env::temp_dir().join(format!("sunox-browser-path-test-{}", uuid::Uuid::new_v4()));
std::fs::write(&browser_path, "").expect("browser stub");
let configured = configured_browser_path(|key| match key {
"SUNO_CHROME_PATH" => Some(browser_path.display().to_string()),
_ => None,
})
.expect("configured path");
assert!(configured.is_none());
let _ = std::fs::remove_file(browser_path);
}
#[test]
fn windows_chrome_candidates_include_per_user_install_path() {
let candidates = chromium_browser_candidates(TargetOs::Windows, |key| match key {
"LOCALAPPDATA" => Some(PathBuf::from(r"C:\Users\alice\AppData\Local")),
_ => None,
});
assert!(candidates.contains(&PathBuf::from(
r"C:\Users\alice\AppData\Local\Google\Chrome\Application\chrome.exe"
)));
assert!(candidates.contains(&PathBuf::from(
r"C:\Program Files\Google\Chrome\Application\chrome.exe"
)));
}
#[test]
fn windows_browser_candidates_include_edge_install_paths() {
let candidates = chromium_browser_candidates(TargetOs::Windows, |key| match key {
"LOCALAPPDATA" => Some(PathBuf::from(r"C:\Users\alice\AppData\Local")),
_ => None,
});
assert!(candidates.contains(&PathBuf::from(
r"C:\Users\alice\AppData\Local\Microsoft\Edge\Application\msedge.exe"
)));
assert!(candidates.contains(&PathBuf::from(
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe"
)));
assert!(candidates.contains(&PathBuf::from(
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
)));
}
#[test]
fn macos_and_linux_candidates_include_edge() {
let macos = chromium_browser_candidates(TargetOs::Macos, |_| None);
let linux = chromium_browser_candidates(TargetOs::Linux, |_| None);
assert!(macos.contains(&PathBuf::from(
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
)));
assert!(linux.contains(&PathBuf::from("/usr/bin/microsoft-edge")));
assert!(linux.contains(&PathBuf::from("/usr/bin/microsoft-edge-stable")));
}
}