use std::env;
use std::path::Path;
use std::sync::OnceLock;
const MARKER_FILE: &str = "install.toml";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstallChannel {
Script,
Direct,
Brew,
Cargo,
Unknown,
}
impl InstallChannel {
pub fn self_update_eligible(self) -> bool {
matches!(self, Self::Script | Self::Direct)
}
pub fn upgrade_hint(self) -> Option<&'static str> {
match self {
Self::Brew => Some("brew upgrade kimun"),
Self::Cargo => Some("cargo install kimun-notes"),
_ => None,
}
}
}
#[derive(serde::Deserialize)]
struct InstallMarker {
channel: String,
}
pub fn detect(config_dir: &Path) -> InstallChannel {
if let Some(channel) = channel_from_marker(config_dir) {
return channel;
}
static EXE_CHANNEL: OnceLock<InstallChannel> = OnceLock::new();
*EXE_CHANNEL.get_or_init(channel_from_exe_path)
}
fn channel_from_marker(config_dir: &Path) -> Option<InstallChannel> {
let raw = std::fs::read_to_string(config_dir.join(MARKER_FILE)).ok()?;
let marker: InstallMarker = toml::from_str(&raw).ok()?;
match marker.channel.as_str() {
"script" => Some(InstallChannel::Script),
"direct" => Some(InstallChannel::Direct),
"brew" => Some(InstallChannel::Brew),
"cargo" => Some(InstallChannel::Cargo),
_ => None,
}
}
fn channel_from_exe_path() -> InstallChannel {
let exe = match env::current_exe().and_then(|p| p.canonicalize()) {
Ok(p) => p,
Err(_) => return InstallChannel::Unknown,
};
let path = exe.to_string_lossy();
if let Ok(prefix) = env::var("HOMEBREW_PREFIX")
&& !prefix.is_empty()
&& path.starts_with(prefix.as_str())
{
return InstallChannel::Brew;
}
if path.contains("/Cellar/") || path.contains("/homebrew/") {
return InstallChannel::Brew;
}
if let Ok(cargo_home) = env::var("CARGO_HOME")
&& !cargo_home.is_empty()
&& exe.starts_with(&cargo_home)
{
return InstallChannel::Cargo;
}
if let Ok(home) = crate::settings::get_home_dir()
&& exe.starts_with(home.join(".cargo").join("bin"))
{
return InstallChannel::Cargo;
}
match exe.parent() {
Some(dir) if dir_is_writable(dir) => InstallChannel::Direct,
_ => InstallChannel::Unknown,
}
}
fn dir_is_writable(dir: &Path) -> bool {
let probe = dir.join(format!(".kimun-write-probe-{}", std::process::id()));
match std::fs::File::create(&probe) {
Ok(_) => {
let _ = std::fs::remove_file(&probe);
true
}
Err(_) => false,
}
}