use crate::shell::EnvContext;
use std::path::{Path, PathBuf};
pub fn profile_path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".bashrc"))
}
pub fn env_script(ctx: &EnvContext<'_>) -> String {
let gvsn_dir = ctx.gvsn_dir.display().to_string();
let path_stmt = ctx.active_bin.map_or_else(String::new, |bin| {
format!("export PATH=\"{}:$PATH\"\n", bin.display())
});
let goroot_stmt = ctx.active_root.map_or_else(String::new, |root| {
format!("export GOROOT=\"{}\"\n", root.display())
});
format!(
r#"export GVSN_DIR="{gvsn_dir}"
{goroot_stmt}{path_stmt}
if ! declare -f _gvsn_hook > /dev/null 2>&1; then
_gvsn_hook() {{
[ -n "$GVSN_SHELL_VERSION" ] && return
[ "$PWD" = "${{_GVSN_PREV_PWD-}}" ] && return
export _GVSN_PREV_PWD="$PWD"
local p; p=$(gvsn path 2>/dev/null)
if [ -n "$p" ]; then
export GOROOT="$(dirname "$p")"
PATH="$p:$(printenv PATH | tr ':' '\n' | grep -v "$GVSN_DIR/versions" | tr '\n' ':' | sed 's/:$//')"
export PATH
fi
}}
cd() {{ builtin cd "$@" && _gvsn_hook; }}
PROMPT_COMMAND="_gvsn_hook${{PROMPT_COMMAND:+;$PROMPT_COMMAND}}"
fi"#,
gvsn_dir = gvsn_dir,
goroot_stmt = goroot_stmt,
path_stmt = path_stmt,
)
}
pub fn wrapper_function() -> &'static str {
r#"gvsn() {
local _gvsn_exit
case "$1" in
shell)
local _gvsn_shell_script
_gvsn_shell_script="$(command gvsn "$@")"
_gvsn_exit=$?
if [ "$_gvsn_exit" -eq 0 ]; then
eval "$_gvsn_shell_script"
fi
;;
use|default|local)
command gvsn "$@"
_gvsn_exit=$?
eval "$(command gvsn env --shell bash 2>/dev/null)"
;;
*)
command gvsn "$@"
_gvsn_exit=$?
;;
esac
return $_gvsn_exit
}"#
}
pub fn shell_version_script(tag: &str, bin: &Path, root: &Path) -> String {
format!(
r#"export GVSN_SHELL_VERSION="{tag}"
export GOROOT="{root}"
export PATH="{bin}:$(printenv PATH | tr ':' '\n' | grep -v "$GVSN_DIR/versions" | tr '\n' ':' | sed 's/:$//')"
"#,
tag = tag,
root = root.display(),
bin = bin.display(),
)
}
pub fn shell_unset_script() -> &'static str {
"unset GVSN_SHELL_VERSION\n_gvsn_hook 2>/dev/null || true\n"
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shell::EnvContext;
use std::path::Path;
fn ctx_with_version() -> (String, String) {
let bin = Path::new("/home/user/.gvsn/versions/go1.23.4/bin");
let root = Path::new("/home/user/.gvsn/versions/go1.23.4");
let ctx = EnvContext {
gvsn_dir: Path::new("/home/user/.gvsn"),
active_bin: Some(bin),
active_root: Some(root),
};
(
env_script(&ctx),
shell_version_script("go1.23.4", bin, root),
)
}
#[test]
fn hook_checks_gvsn_shell_version() {
let (script, _) = ctx_with_version();
assert!(
script.contains("GVSN_SHELL_VERSION"),
"env_script hook must check GVSN_SHELL_VERSION"
);
}
#[test]
fn hook_returns_early_when_shell_version_set() {
let (script, _) = ctx_with_version();
let hook_start = script.find("_gvsn_hook()").unwrap_or(0);
let guard_pos = script.find("GVSN_SHELL_VERSION").unwrap_or(usize::MAX);
let path_pos = script.find("gvsn path").unwrap_or(usize::MAX);
assert!(
guard_pos > hook_start && guard_pos < path_pos,
"GVSN_SHELL_VERSION guard must appear before gvsn path call inside the hook"
);
}
#[test]
fn bash_shell_version_script_includes_bin_path() {
let bin = Path::new("/home/user/.gvsn/versions/go1.23.4/bin");
let root = Path::new("/home/user/.gvsn/versions/go1.23.4");
let ctx = EnvContext {
gvsn_dir: Path::new("/home/user/.gvsn"),
active_bin: Some(bin),
active_root: Some(root),
};
let script = env_script(&ctx);
assert!(
script.contains(bin.to_str().unwrap()),
"env_script must include the bin path in PATH export"
);
}
#[test]
fn bash_hook_registered_to_prompt_command() {
let ctx = EnvContext {
gvsn_dir: Path::new("/home/user/.gvsn"),
active_bin: None,
active_root: None,
};
let script = env_script(&ctx);
assert!(
script.contains("PROMPT_COMMAND"),
"env_script must register hook via PROMPT_COMMAND for startup detection"
);
}
#[test]
fn wrapper_handles_shell_subcommand() {
let w = wrapper_function();
assert!(
w.contains("shell)"),
"wrapper must handle the shell subcommand"
);
assert!(w.contains("eval"), "wrapper must eval shell output");
}
#[test]
fn shell_version_script_has_required_exports() {
let bin = Path::new("/home/user/.gvsn/versions/go1.23.4/bin");
let root = Path::new("/home/user/.gvsn/versions/go1.23.4");
let script = shell_version_script("go1.23.4", bin, root);
assert!(script.contains("GVSN_SHELL_VERSION=\"go1.23.4\""));
assert!(script.contains("GOROOT="));
assert!(script.contains("PATH="));
}
#[test]
fn shell_unset_clears_variable_and_triggers_hook() {
let s = shell_unset_script();
assert!(s.contains("unset GVSN_SHELL_VERSION"));
assert!(s.contains("_gvsn_hook"));
}
}