use crate::ViewerError;
use std::path::PathBuf;
use std::sync::OnceLock;
pub trait AppLocator {
fn locate_app_binary(&self) -> Result<PathBuf, ViewerError>;
}
static BINARY_CACHE: OnceLock<PathBuf> = OnceLock::new();
#[derive(Debug, Clone)]
struct AttemptedLocation {
path: PathBuf,
reason: String,
}
pub struct DefaultAppLocator;
impl AppLocator for DefaultAppLocator {
fn locate_app_binary(&self) -> Result<PathBuf, ViewerError> {
if let Some(cached_path) = BINARY_CACHE.get().filter(|p| p.exists() && p.is_file()) {
return Ok(cached_path.clone());
}
let mut attempted = Vec::new();
let binary_name = if cfg!(target_os = "windows") {
"html_view_app.exe"
} else {
"html_view_app"
};
if let Some(embedded_path) = option_env!("HTML_VIEW_APP_PATH") {
let path = PathBuf::from(embedded_path);
if path.exists() && path.is_file() {
let _ = BINARY_CACHE.set(path.clone());
return Ok(path);
}
attempted.push(AttemptedLocation {
path: path.clone(),
reason: if path.exists() {
"exists but is not a file".to_string()
} else {
"does not exist".to_string()
},
});
}
if let Ok(path_str) = std::env::var("HTML_VIEW_APP_PATH") {
let path = PathBuf::from(&path_str);
if path.exists() && path.is_file() {
let _ = BINARY_CACHE.set(path.clone());
return Ok(path);
}
attempted.push(AttemptedLocation {
path: path.clone(),
reason: if path.exists() {
"exists but is not a file".to_string()
} else {
"does not exist".to_string()
},
});
}
if let Some(home) = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(|h| PathBuf::from(h).join(".cargo"))
})
{
let candidate = home.join("bin").join(binary_name);
if candidate.exists() && candidate.is_file() {
let _ = BINARY_CACHE.set(candidate.clone());
return Ok(candidate);
}
attempted.push(AttemptedLocation {
path: candidate.clone(),
reason: if candidate.exists() {
"exists but is not a file".to_string()
} else {
"does not exist".to_string()
},
});
} else {
attempted.push(AttemptedLocation {
path: PathBuf::from("~/.cargo/bin").join(binary_name),
reason: "could not determine home directory".to_string(),
});
}
if let Ok(exe_path) = std::env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
let candidate = exe_dir.join(binary_name);
if candidate.exists() && candidate.is_file() {
let _ = BINARY_CACHE.set(candidate.clone());
return Ok(candidate);
}
attempted.push(AttemptedLocation {
path: candidate.clone(),
reason: if candidate.exists() {
"exists but is not a file".to_string()
} else {
"does not exist".to_string()
},
});
}
} else {
attempted.push(AttemptedLocation {
path: PathBuf::from("<current_exe_dir>").join(binary_name),
reason: "could not determine current executable path".to_string(),
});
}
if let Ok(current_dir) = std::env::current_dir() {
for profile in &["debug", "release"] {
let candidate = current_dir.join("target").join(profile).join(binary_name);
if candidate.exists() && candidate.is_file() {
let _ = BINARY_CACHE.set(candidate.clone());
return Ok(candidate);
}
attempted.push(AttemptedLocation {
path: candidate.clone(),
reason: if candidate.exists() {
"exists but is not a file".to_string()
} else {
"does not exist".to_string()
},
});
}
} else {
attempted.push(AttemptedLocation {
path: PathBuf::from("<current_dir>/target/debug").join(binary_name),
reason: "could not determine current directory".to_string(),
});
attempted.push(AttemptedLocation {
path: PathBuf::from("<current_dir>/target/release").join(binary_name),
reason: "could not determine current directory".to_string(),
});
}
let mut error_msg = format!(
"Could not locate html_view_app binary. Searched {} locations:\n\n",
attempted.len()
);
for (i, attempt) in attempted.iter().enumerate() {
error_msg.push_str(&format!(
" {}. {}\n → {}\n",
i + 1,
attempt.path.display(),
attempt.reason
));
}
error_msg.push_str(&format!(
"\nTo fix this issue:\n\
• Install the viewer: cargo install html_view_app\n\
• Or set HTML_VIEW_APP_PATH environment variable to the binary location\n\
• Or ensure html_view_app is in your PATH\n\n\
Platform: {}\n\
Architecture: {}",
std::env::consts::OS,
std::env::consts::ARCH
));
Err(ViewerError::BinaryNotFound(error_msg))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_env_var_locator() {
let locator = DefaultAppLocator;
let _result = locator.locate_app_binary();
}
}