regy 0.1.0

Private-by-default desktop agent for the Regy web interface
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())
}