onpath 0.2.0

Get your tools on the PATH — cross-shell, cross-platform, zero fuss
Documentation
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use crate::error::{Error, Result};

/// System context providing home directory and environment variables.
///
/// Abstracted for testability — tests inject a fake home dir instead of
/// touching the real filesystem.
#[derive(Debug, Clone)]
pub struct SystemContext {
    home_dir: PathBuf,
    env_vars: HashMap<String, String>,
}

impl SystemContext {
    /// Detect the real system context.
    ///
    /// # Errors
    ///
    /// Returns [`Error::HomeDirNotFound`] if the home directory cannot be determined.
    pub fn detect() -> Result<Self> {
        let home_dir = dirs::home_dir().ok_or(Error::HomeDirNotFound)?;

        let mut env_vars = HashMap::new();
        for key in ["SHELL", "ZDOTDIR", "XDG_CONFIG_HOME"] {
            if let Ok(val) = std::env::var(key) {
                env_vars.insert(key.to_owned(), val);
            }
        }

        Ok(Self { home_dir, env_vars })
    }

    /// Create a context with a custom home directory (for testing).
    #[must_use]
    pub fn with_home(home_dir: PathBuf) -> Self {
        Self {
            home_dir,
            env_vars: HashMap::new(),
        }
    }

    /// Create a context with a custom home directory and environment variables.
    #[must_use]
    pub fn with_home_and_env(home_dir: PathBuf, env_vars: HashMap<String, String>) -> Self {
        Self { home_dir, env_vars }
    }

    /// Returns the home directory path.
    #[must_use]
    pub fn home_dir(&self) -> &Path {
        &self.home_dir
    }

    /// Returns the value of an environment variable, if set.
    pub fn env_var(&self, key: &str) -> Option<&str> {
        self.env_vars.get(key).map(String::as_str)
    }

    /// Returns the XDG config home, defaulting to `~/.config`.
    pub fn xdg_config_home(&self) -> PathBuf {
        self.env_vars
            .get("XDG_CONFIG_HOME")
            .map_or_else(|| self.home_dir.join(".config"), PathBuf::from)
    }

    /// Returns the user's default shell name from `$SHELL` (e.g., "bash", "zsh").
    pub fn user_shell_name(&self) -> Option<&str> {
        self.env_vars
            .get("SHELL")
            .map(String::as_str)
            .map(|s| s.rsplit('/').next().unwrap_or(s))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn with_home_sets_home_dir() {
        let ctx = SystemContext::with_home(PathBuf::from("/fake/home"));
        assert_eq!(ctx.home_dir(), Path::new("/fake/home"));
    }

    #[test]
    fn xdg_config_home_defaults_to_dot_config() {
        let ctx = SystemContext::with_home(PathBuf::from("/home/user"));
        assert_eq!(ctx.xdg_config_home(), PathBuf::from("/home/user/.config"));
    }

    #[test]
    fn xdg_config_home_uses_env_var() {
        let mut env = HashMap::new();
        env.insert("XDG_CONFIG_HOME".to_owned(), "/custom/config".to_owned());
        let ctx = SystemContext::with_home_and_env(PathBuf::from("/home/user"), env);
        assert_eq!(ctx.xdg_config_home(), PathBuf::from("/custom/config"));
    }

    #[test]
    fn user_shell_name_extracts_basename() {
        let mut env = HashMap::new();
        env.insert("SHELL".to_owned(), "/usr/bin/zsh".to_owned());
        let ctx = SystemContext::with_home_and_env(PathBuf::from("/home/user"), env);
        assert_eq!(ctx.user_shell_name(), Some("zsh"));
    }

    #[test]
    fn user_shell_name_returns_none_when_unset() {
        let ctx = SystemContext::with_home(PathBuf::from("/home/user"));
        assert_eq!(ctx.user_shell_name(), None);
    }
}