use anyhow::{bail, Result};
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Confirm};
use std::fs;
use std::io::IsTerminal;
use std::path::PathBuf;
const INTEGRATION_MARKER: &str = "stax shell-setup";
fn shell_snippet() -> &'static str {
r#"# Generated by stax shell-setup
export STAX_SHELL_INTEGRATION=1
stax() {
case "$1" in
wtgo)
local dir
dir=$(command stax worktree path "${@:2}" 2>&1)
if [[ $? -eq 0 && -d "$dir" ]]; then
builtin cd "$dir" && echo "$(tput bold)$(tput setaf 6)~$(tput sgr0) $(basename "$dir")"
else
echo "$dir" >&2; return 1
fi ;;
worktree|wt)
if [[ "$2" == "go" ]]; then
local dir
dir=$(command stax worktree path "${@:3}" 2>&1)
if [[ $? -eq 0 && -d "$dir" ]]; then
builtin cd "$dir" && echo "$(tput bold)$(tput setaf 6)~$(tput sgr0) $(basename "$dir")"
else
echo "$dir" >&2; return 1
fi
else
command stax "$@"
fi ;;
*)
command stax "$@" ;;
esac
}
# Quick switch alias: sw <worktree-name>
sw() { stax worktree go "$@"; }"#
}
pub fn run(install: bool) -> Result<()> {
if install {
install_to_shell_config()
} else {
println!("{}", shell_snippet());
Ok(())
}
}
pub fn is_installed() -> bool {
std::env::var("STAX_SHELL_INTEGRATION").is_ok()
}
pub fn prompt_if_missing() -> Result<()> {
if is_installed() {
return Ok(());
}
if !std::io::stdin().is_terminal() {
return Ok(());
}
eprintln!("{} Shell integration not detected.", "stax:".cyan().bold());
eprintln!(
" Run {} for transparent worktree navigation (cd).",
"stax shell-setup --install".cyan()
);
eprintln!();
let install = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Install shell integration now?")
.default(true)
.interact()?;
if install {
install_to_shell_config()?;
eprintln!();
eprintln!(
"{} Restart your shell or run: {}",
"Done.".green().bold(),
shell_source_cmd().cyan()
);
eprintln!();
}
Ok(())
}
fn install_to_shell_config() -> Result<()> {
let config_path = detect_shell_config()?;
let eval_line = format!("eval \"$({})\"", INTEGRATION_MARKER);
let existing = if config_path.exists() {
fs::read_to_string(&config_path)?
} else {
String::new()
};
if existing.lines().any(|l| l.contains(INTEGRATION_MARKER)) {
println!(
"{} Already present in {}",
"OK".green().bold(),
config_path.display()
);
return Ok(());
}
println!("Will append to {}:", config_path.display());
println!();
println!(" {}", eval_line.cyan());
println!();
let proceed = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Proceed?")
.default(true)
.interact()?;
if !proceed {
println!("{}", "Aborted.".dimmed());
return Ok(());
}
let suffix = if existing.is_empty() || existing.ends_with('\n') {
format!("{}\n", eval_line)
} else {
format!("\n{}\n", eval_line)
};
fs::write(&config_path, format!("{}{}", existing, suffix))?;
println!(
"{} Added to {}",
"Done.".green().bold(),
config_path.display()
);
println!(" Restart your shell or run: {}", shell_source_cmd().cyan());
Ok(())
}
fn detect_shell_config() -> Result<PathBuf> {
let shell = std::env::var("SHELL").unwrap_or_default();
let home = std::env::var("HOME").unwrap_or_default();
if home.is_empty() {
bail!("$HOME is not set; cannot detect shell config file.");
}
let path = if shell.ends_with("zsh") {
PathBuf::from(&home).join(".zshrc")
} else if shell.ends_with("bash") {
let profile = PathBuf::from(&home).join(".bash_profile");
if cfg!(target_os = "macos") && !profile.exists() {
PathBuf::from(&home).join(".bashrc")
} else if cfg!(target_os = "macos") {
profile
} else {
PathBuf::from(&home).join(".bashrc")
}
} else if shell.ends_with("fish") {
PathBuf::from(&home).join(".config/fish/config.fish")
} else {
PathBuf::from(&home).join(".profile")
};
Ok(path)
}
fn shell_source_cmd() -> String {
if let Ok(shell) = std::env::var("SHELL") {
let config = detect_shell_config().unwrap_or_else(|_| PathBuf::from("~/.zshrc"));
if shell.ends_with("fish") {
return format!("source {}", config.display());
}
return format!("source {}", config.display());
}
"source ~/.zshrc".to_string()
}