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";
pub const CONFIG_FILE: &str = "config.toml";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Profile {
Production,
Dev,
}
impl Profile {
#[must_use]
pub fn current() -> Self {
static CURRENT: OnceLock<Profile> = OnceLock::new();
*CURRENT.get_or_init(Self::detect)
}
#[must_use]
pub fn agent_service_label(self) -> String {
match self {
Self::Production => crate::brand::AGENT_SERVICE_LABEL.to_owned(),
Self::Dev => crate::brand::dev_id(crate::brand::AGENT_SERVICE_LABEL),
}
}
fn detect() -> Self {
match std::env::var(crate::env::PROFILE) {
Ok(value) if value == Self::Dev.env_value() => return Self::Dev,
Ok(value) if value == Self::Production.env_value() || value == "production" => {
return Self::Production;
}
_ => {}
}
#[cfg(target_os = "macos")]
{
if let Some(identifier) = current_bundle_identifier()
&& crate::brand::is_dev_id(&identifier)
{
return Self::Dev;
}
}
Self::Production
}
#[must_use]
pub const fn env_value(self) -> &'static str {
match self {
Self::Production => "prod",
Self::Dev => "dev",
}
}
const fn app_dir(self) -> &'static str {
match self {
Self::Production => APP_DIR,
Self::Dev => DEV_APP_DIR,
}
}
}
#[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 {
Profile::current().app_dir()
}
#[must_use]
pub fn is_dev_profile() -> bool {
Profile::current() == Profile::Dev
}
#[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 xdg_data_home() -> Result<PathBuf, PathsError> {
Ok(xdg()?.data_dir())
}
pub fn config_dir() -> Result<PathBuf, PathsError> {
config_dir_for(Profile::current())
}
pub fn config_dir_for(profile: Profile) -> Result<PathBuf, PathsError> {
Ok(xdg_config_home()?.join(profile.app_dir()))
}
pub fn config_path() -> Result<PathBuf, PathsError> {
Ok(config_dir()?.join(CONFIG_FILE))
}
pub fn data_dir() -> Result<PathBuf, PathsError> {
Ok(xdg()?.data_dir().join(app_dir()))
}
pub fn state_dir() -> Result<PathBuf, PathsError> {
let xdg = xdg()?;
Ok(xdg
.state_dir()
.map_or_else(|| xdg.data_dir().join(app_dir()), |dir| dir.join(app_dir())))
}
pub fn runtime_dir() -> Result<PathBuf, PathsError> {
runtime_dir_for(Profile::current())
}
pub fn runtime_dir_for(profile: Profile) -> Result<PathBuf, PathsError> {
let xdg = xdg()?;
Ok(xdg.runtime_dir().map_or_else(
|| xdg.config_dir().join(profile.app_dir()),
|dir| dir.join(profile.app_dir()),
))
}
pub fn agent_socket_path() -> Result<PathBuf, PathsError> {
agent_socket_path_for(Profile::current())
}
pub fn agent_socket_path_for(profile: Profile) -> Result<PathBuf, PathsError> {
Ok(runtime_dir_for(profile)?.join("agent.sock"))
}
#[cfg(test)]
#[cfg(unix)]
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"));
}
#[test]
fn a_named_profile_resolves_its_own_directories() {
assert!(
config_dir_for(Profile::Dev)
.expect("dev config dir")
.ends_with("openlogi-dev")
);
assert!(
agent_socket_path_for(Profile::Dev)
.expect("dev socket")
.ends_with("openlogi-dev/agent.sock")
);
assert!(
agent_socket_path_for(Profile::Production)
.expect("production socket")
.ends_with("openlogi/agent.sock")
);
}
}