use crate::shell::detector::ShellType;
use crate::shell::prompt::get_prompt_env_vars;
use anyhow::Result;
use std::collections::HashMap;
use std::process::Command;
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
pub const STAND_ACTIVE: &str = "STAND_ACTIVE";
pub const STAND_ENVIRONMENT: &str = "STAND_ENVIRONMENT";
pub const STAND_PROJECT_ROOT: &str = "STAND_PROJECT_ROOT";
pub fn build_shell_environment(
user_env: HashMap<String, String>,
env_name: &str,
project_root: &str,
shell_path: &str,
) -> HashMap<String, String> {
let mut env = user_env;
env.insert(STAND_ACTIVE.to_string(), "1".to_string());
env.insert(STAND_ENVIRONMENT.to_string(), env_name.to_string());
env.insert(STAND_PROJECT_ROOT.to_string(), project_root.to_string());
let shell_type = ShellType::from_path(shell_path);
let prompt_vars = get_prompt_env_vars(&shell_type, env_name);
for (key, value) in prompt_vars {
env.insert(key, value);
}
env
}
pub fn spawn_shell(shell_path: &str, env_vars: HashMap<String, String>) -> Result<i32> {
let shell_type = ShellType::from_path(shell_path);
let args = get_shell_args(&shell_type);
let mut cmd = Command::new(shell_path);
cmd.args(&args);
for (key, value) in &env_vars {
cmd.env(key, value);
}
let zdotdir_cleanup = if matches!(shell_type, ShellType::Zsh) {
setup_zsh_zdotdir(&mut cmd, &env_vars)?
} else {
None
};
let status = cmd.status()?;
if let Some(path) = zdotdir_cleanup {
let _ = std::fs::remove_dir_all(path);
}
match status.code() {
Some(code) => Ok(code),
None => {
#[cfg(unix)]
{
if let Some(signal) = status.signal() {
return Ok(128 + signal);
}
}
Ok(1)
}
}
}
fn setup_zsh_zdotdir(
cmd: &mut Command,
env_vars: &HashMap<String, String>,
) -> Result<Option<std::path::PathBuf>> {
use std::io::Write;
let temp_dir = std::env::temp_dir().join(format!("stand-zsh-{}", std::process::id()));
std::fs::create_dir_all(&temp_dir)?;
let color = env_vars
.get("STAND_ENV_COLOR")
.map(|s| s.as_str())
.unwrap_or("green");
let safe_color = match color {
"red" | "green" | "yellow" | "blue" | "magenta" | "purple" | "cyan" | "white" | "black" => {
color
}
_ => "green", };
let zshenv_content = r#"# Stand temporary zshenv
# Source user's original .zshenv if it exists
[[ -f "$HOME/.zshenv" ]] && source "$HOME/.zshenv"
"#;
let zshenv_path = temp_dir.join(".zshenv");
let mut zshenv_file = std::fs::File::create(&zshenv_path)?;
zshenv_file.write_all(zshenv_content.as_bytes())?;
let zshrc_content = format!(
r#"# Stand temporary zshrc
# Restore original ZDOTDIR for child shells
export ZDOTDIR="$HOME"
# Source user's original .zshrc if it exists
[[ -f "$HOME/.zshrc" ]] && source "$HOME/.zshrc"
# Track previous directory for reverting
typeset -g _stand_prev_dir="$PWD"
# Stand chpwd function for directory guard when leaving project directory
# Only active when STAND_AUTO_EXIT=1
# Uses logical paths ($PWD) instead of physical paths to allow symlinks
_stand_chpwd() {{
if [[ "$STAND_AUTO_EXIT" = "1" ]] && [[ -n "$STAND_PROJECT_ROOT" ]]; then
case "$PWD" in
"$STAND_PROJECT_ROOT"|"$STAND_PROJECT_ROOT"/*)
_stand_prev_dir="$PWD"
;;
*)
# Revert to previous directory with fallback
if ! builtin cd "$_stand_prev_dir" 2>/dev/null; then
if ! builtin cd "$STAND_PROJECT_ROOT" 2>/dev/null; then
echo "⚠️ Cannot return to project directory. Exiting Stand shell."
exit 1
fi
fi
echo "⚠️ Cannot leave project directory while in Stand shell."
echo " Type 'exit' to leave the Stand shell first."
;;
esac
fi
}}
# Add to chpwd_functions array (runs on directory change)
chpwd_functions+=(_stand_chpwd)
# Stand precmd function for prompt customization
_stand_precmd() {{
# Save original prompt on first run
if [[ -z "$STAND_ORIGINAL_PROMPT" ]]; then
export STAND_ORIGINAL_PROMPT="$PROMPT"
fi
# Set prompt with Stand indicator (newline, bold, reverse, colored)
local color="{safe_color}"
local env_upper="${{(U)STAND_ENVIRONMENT}}"
PROMPT=$'\n%B%S%F{{'"$color"'}} stand:'"$env_upper"$' %f%s%b'"$STAND_ORIGINAL_PROMPT"
}}
# Add to precmd_functions array (runs after any existing precmd)
precmd_functions+=(_stand_precmd)
"#
);
let zshrc_path = temp_dir.join(".zshrc");
let mut file = std::fs::File::create(&zshrc_path)?;
file.write_all(zshrc_content.as_bytes())?;
cmd.env("ZDOTDIR", &temp_dir);
Ok(Some(temp_dir))
}
fn get_shell_args(shell_type: &ShellType) -> Vec<String> {
match shell_type {
ShellType::Fish => {
let init_cmd = concat!(
"set -g _stand_prev_dir \"$PWD\"; ",
"set -g _stand_reverting 0; ",
"function _stand_check_dir --on-variable PWD; ",
"if test \"$_stand_reverting\" = \"1\"; set -g _stand_reverting 0; return; end; ",
"if test \"$STAND_AUTO_EXIT\" = \"1\" -a -n \"$STAND_PROJECT_ROOT\"; ",
"if not string match -q \"$STAND_PROJECT_ROOT\" \"$PWD\"; ",
"and not string match -q \"$STAND_PROJECT_ROOT/*\" \"$PWD\"; ",
"set -g _stand_reverting 1; ",
"if not builtin cd \"$_stand_prev_dir\" 2>/dev/null; ",
"if not builtin cd \"$STAND_PROJECT_ROOT\" 2>/dev/null; ",
"echo '⚠️ Cannot return to project directory. Exiting Stand shell.'; exit 1; end; end; ",
"echo '⚠️ Cannot leave project directory while in Stand shell.'; ",
"echo ' Type \\'exit\\' to leave the Stand shell first.'; ",
"return; end; end; ",
"set -g _stand_prev_dir \"$PWD\"; end; ",
"functions -c fish_prompt _stand_original_fish_prompt 2>/dev/null; ",
"or function _stand_original_fish_prompt; echo '> '; end; ",
"function fish_prompt; ",
"echo; ",
"set -q STAND_ENV_COLOR; and set_color --bold --reverse $STAND_ENV_COLOR; or set_color --bold --reverse green; ",
"echo -n ' stand:'(string upper $STAND_ENVIRONMENT)' '; ",
"set_color normal; ",
"_stand_original_fish_prompt; end"
);
vec!["-C".to_string(), init_cmd.to_string()]
}
ShellType::Zsh => {
vec!["-i".to_string()]
}
_ => {
vec!["-i".to_string()]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_shell_environment_includes_user_vars() {
let mut user_env = HashMap::new();
user_env.insert(
"DATABASE_URL".to_string(),
"postgres://localhost".to_string(),
);
user_env.insert("API_KEY".to_string(), "secret123".to_string());
let result = build_shell_environment(user_env, "dev", "/home/user/project", "/bin/bash");
assert_eq!(
result.get("DATABASE_URL"),
Some(&"postgres://localhost".to_string())
);
assert_eq!(result.get("API_KEY"), Some(&"secret123".to_string()));
}
#[test]
fn test_build_shell_environment_includes_stand_markers() {
let user_env = HashMap::new();
let result = build_shell_environment(user_env, "production", "/var/www/app", "/bin/bash");
assert_eq!(result.get(STAND_ACTIVE), Some(&"1".to_string()));
assert_eq!(
result.get(STAND_ENVIRONMENT),
Some(&"production".to_string())
);
assert_eq!(
result.get(STAND_PROJECT_ROOT),
Some(&"/var/www/app".to_string())
);
}
#[test]
fn test_build_shell_environment_stand_markers_override_user_vars() {
let mut user_env = HashMap::new();
user_env.insert(STAND_ACTIVE.to_string(), "0".to_string());
let result = build_shell_environment(user_env, "dev", "/home/user/project", "/bin/bash");
assert_eq!(result.get(STAND_ACTIVE), Some(&"1".to_string()));
}
#[test]
fn test_get_shell_args_bash() {
let args = get_shell_args(&ShellType::Bash);
assert_eq!(args, vec!["-i".to_string()]);
}
#[test]
fn test_get_shell_args_zsh() {
let args = get_shell_args(&ShellType::Zsh);
assert_eq!(args, vec!["-i".to_string()]);
}
#[test]
fn test_get_shell_args_fish() {
let args = get_shell_args(&ShellType::Fish);
assert_eq!(args.len(), 2);
assert_eq!(args[0], "-C");
assert!(args[1].contains("fish_prompt"));
assert!(args[1].contains("STAND_ENVIRONMENT"));
}
#[test]
fn test_get_shell_args_other() {
let args = get_shell_args(&ShellType::Other("sh".to_string()));
assert_eq!(args, vec!["-i".to_string()]);
}
}