onpath 0.2.0

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

use crate::config::Position;
use crate::context::SystemContext;
use crate::shell::{Shell, ShellKind};

pub struct Fish;

impl Shell for Fish {
    fn kind(&self) -> ShellKind {
        ShellKind::Fish
    }

    fn env_extension(&self) -> &'static str {
        ".fish"
    }

    fn env_script(&self, dir: &Path, position: Position) -> String {
        let dir = dir.display();
        let flag = match position {
            Position::Prepend => "--prepend",
            Position::Append => "--append",
        };
        format!(
            r#"# Generated by onpath. Do not edit.
if not contains "{dir}" $PATH
    fish_add_path {flag} "{dir}"
end
"#
        )
    }

    fn source_line(&self, env_script_path: &Path) -> String {
        format!("source \"{}\"", env_script_path.display())
    }

    fn rc_candidates(&self, ctx: &SystemContext) -> Vec<PathBuf> {
        let config = ctx.xdg_config_home();
        vec![
            config.join("fish").join("conf.d"), // directory existence = fish installed
            config.join("fish").join("config.fish"),
        ]
    }

    fn primary_rc(&self, ctx: &SystemContext) -> PathBuf {
        // Fish uses conf.d for auto-loaded scripts — this is the preferred location.
        // The env script itself goes into conf.d, so no separate source line is needed
        // for Fish. But we still need a "primary_rc" for the API contract.
        ctx.xdg_config_home().join("fish").join("config.fish")
    }
}

/// Fish is special: it uses conf.d for auto-loaded scripts. Instead of adding a
/// source line to config.fish, we write the env script directly into conf.d/.
/// This function returns the conf.d path for the given tool.
#[cfg(any(not(windows), test))]
pub fn conf_d_path(ctx: &SystemContext, tool_name: &str) -> PathBuf {
    ctx.xdg_config_home()
        .join("fish")
        .join("conf.d")
        .join(format!("onpath-{tool_name}.fish"))
}

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

    #[test]
    fn env_script_uses_fish_add_path() {
        let script = Fish.env_script(Path::new("/app/bin"), Position::Prepend);
        assert!(script.contains("fish_add_path --prepend"));
        assert!(script.contains("/app/bin"));
    }

    #[test]
    fn env_script_append() {
        let script = Fish.env_script(Path::new("/app/bin"), Position::Append);
        assert!(script.contains("fish_add_path --append"));
    }

    #[test]
    fn env_script_contains_guard() {
        let script = Fish.env_script(Path::new("/app/bin"), Position::Prepend);
        assert!(script.contains("if not contains"));
    }

    #[test]
    fn source_line_uses_source_keyword() {
        let line = Fish.source_line(Path::new("/home/user/.myapp/env.fish"));
        assert_eq!(line, "source \"/home/user/.myapp/env.fish\"");
    }

    #[test]
    fn conf_d_path_includes_tool_name() {
        let ctx = SystemContext::with_home(PathBuf::from("/home/user"));
        let path = conf_d_path(&ctx, "myapp");
        assert_eq!(
            path,
            PathBuf::from("/home/user/.config/fish/conf.d/onpath-myapp.fish")
        );
    }
}