use std::path::PathBuf;
use std::sync::OnceLock;
use etcetera::{BaseStrategy, base_strategy::Xdg};
use thiserror::Error;
const APP_DIR: &str = "openlogi";
const DEV_APP_DIR: &str = "openlogi-dev";
#[derive(Debug, Error)]
pub enum PathsError {
#[error("could not resolve a home directory for the current user")]
HomeNotFound,
}
fn xdg() -> Result<Xdg, PathsError> {
Xdg::new().map_err(|_| PathsError::HomeNotFound)
}
fn app_dir() -> &'static str {
static IS_DEV_PROFILE: OnceLock<bool> = OnceLock::new();
if *IS_DEV_PROFILE.get_or_init(is_dev_profile) {
DEV_APP_DIR
} else {
APP_DIR
}
}
fn is_dev_profile() -> bool {
match std::env::var("OPENLOGI_PROFILE") {
Ok(value) if value == "dev" => return true,
Ok(value) if matches!(value.as_str(), "prod" | "production") => return false,
_ => {}
}
#[cfg(target_os = "macos")]
{
if let Some(identifier) = current_bundle_identifier() {
return identifier
.rsplit_once('.')
.is_some_and(|(_, suffix)| suffix.eq_ignore_ascii_case("dev"));
}
}
false
}
#[cfg(target_os = "macos")]
fn current_bundle_identifier() -> Option<String> {
let exe = std::env::current_exe().ok()?;
for ancestor in exe.ancestors() {
if !ancestor
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("app"))
{
continue;
}
let info = ancestor.join("Contents/Info.plist");
let Ok(plist) = plist::Value::from_file(info) else {
continue;
};
let Some(identifier) = plist
.as_dictionary()
.and_then(|dictionary| dictionary.get("CFBundleIdentifier"))
.and_then(plist::Value::as_string)
else {
continue;
};
return Some(identifier.to_owned());
}
None
}
pub fn home_dir() -> Result<PathBuf, PathsError> {
Ok(xdg()?.home_dir().to_path_buf())
}
pub fn xdg_config_home() -> Result<PathBuf, PathsError> {
Ok(xdg()?.config_dir())
}
pub fn config_dir() -> Result<PathBuf, PathsError> {
Ok(xdg_config_home()?.join(app_dir()))
}
pub fn config_path() -> Result<PathBuf, PathsError> {
Ok(config_dir()?.join("config.toml"))
}
pub fn data_dir() -> Result<PathBuf, PathsError> {
Ok(xdg()?.data_dir().join(app_dir()))
}
pub fn runtime_dir() -> Result<PathBuf, PathsError> {
let xdg = xdg()?;
Ok(xdg.runtime_dir().map_or_else(
|| xdg.config_dir().join(app_dir()),
|dir| dir.join(app_dir()),
))
}
pub fn agent_socket_path() -> Result<PathBuf, PathsError> {
Ok(runtime_dir()?.join("agent.sock"))
}
#[cfg(all(test, unix))]
#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
mod tests {
use super::*;
#[test]
fn config_dir_keeps_openlogi_under_xdg_config_home() {
assert!(config_dir().expect("config dir").ends_with("openlogi"));
}
#[test]
fn data_dir_keeps_openlogi_under_xdg_data_home() {
assert!(data_dir().expect("data dir").ends_with("openlogi"));
}
#[test]
fn runtime_dir_keeps_openlogi_suffix() {
assert!(runtime_dir().expect("runtime dir").ends_with("openlogi"));
}
}