use std::path::{Path, PathBuf};
use crate::domain::errors::{AgentError, AgentResult, ErrorCode};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct PathEnvironment {
pub home: Option<PathBuf>,
pub xdg_config_home: Option<PathBuf>,
pub xdg_data_home: Option<PathBuf>,
pub xdg_cache_home: Option<PathBuf>,
}
impl PathEnvironment {
pub(crate) fn from_env() -> Self {
Self {
home: std::env::var_os("HOME").map(PathBuf::from),
xdg_config_home: std::env::var_os("XDG_CONFIG_HOME").map(PathBuf::from),
xdg_data_home: std::env::var_os("XDG_DATA_HOME").map(PathBuf::from),
xdg_cache_home: std::env::var_os("XDG_CACHE_HOME").map(PathBuf::from),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AppPaths {
pub config_file: PathBuf,
pub data_dir: PathBuf,
pub server_key_file: PathBuf,
pub clients_file: PathBuf,
pub cache_dir: PathBuf,
}
impl AppPaths {
pub(crate) fn resolve(
config_override: Option<PathBuf>,
environment: &PathEnvironment,
) -> AgentResult<Self> {
let config_file = match usable_path(config_override.as_deref()) {
Some(path) => path.to_path_buf(),
None => resolve_base(
environment.xdg_config_home.as_deref(),
environment.home.as_deref(),
".config",
"XDG_CONFIG_HOME",
)?
.join("regy/pc-agent.toml"),
};
let data_dir = resolve_base(
environment.xdg_data_home.as_deref(),
environment.home.as_deref(),
".local/share",
"XDG_DATA_HOME",
)?
.join("regy/pc-agent");
let cache_dir = resolve_base(
environment.xdg_cache_home.as_deref(),
environment.home.as_deref(),
".cache",
"XDG_CACHE_HOME",
)?
.join("regy");
let server_key_file = data_dir.join("server-key");
let clients_file = data_dir.join("clients.json");
Ok(Self {
config_file,
data_dir,
server_key_file,
clients_file,
cache_dir,
})
}
pub(crate) fn server_lock_file(&self) -> PathBuf {
self.data_dir.join("server.lock")
}
pub(crate) fn daemon_log_file(&self) -> PathBuf {
self.cache_dir.join("pc-agent.log")
}
pub(crate) fn iroh_endpoint_key_file(&self) -> PathBuf {
self.data_dir.join("iroh-endpoint.key")
}
}
fn resolve_base(
xdg: Option<&Path>,
home: Option<&Path>,
fallback: &str,
variable: &str,
) -> AgentResult<PathBuf> {
if let Some(path) = usable_path(xdg).filter(|path| path.is_absolute()) {
return Ok(path.to_path_buf());
}
let home = usable_path(home).ok_or_else(|| {
AgentError::new(
ErrorCode::InvalidMessage,
format!("HOME must be set and non-empty when {variable} is not usable"),
)
})?;
Ok(home.join(fallback))
}
fn usable_path(path: Option<&Path>) -> Option<&Path> {
path.filter(|path| !path.as_os_str().is_empty() && !path.to_string_lossy().trim().is_empty())
}