stax 0.29.4

Fast stacked Git branches and PRs
Documentation
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";

/// The shell function block that users source in their shell config.
/// It intercepts `stax worktree go` / `stax wt go` / `stax wtgo` and
/// `sw <name>` and performs the actual `cd` in the calling shell process.
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 "$@"; }"#
}

/// Print the shell snippet to stdout (for `eval "$(stax shell-setup)"`).
pub fn run(install: bool) -> Result<()> {
    if install {
        install_to_shell_config()
    } else {
        println!("{}", shell_snippet());
        Ok(())
    }
}

/// Returns true if `STAX_SHELL_INTEGRATION` is set in the environment,
/// which means the shell function has been sourced.
pub fn is_installed() -> bool {
    std::env::var("STAX_SHELL_INTEGRATION").is_ok()
}

/// If shell integration is not installed and we're in an interactive terminal,
/// offer to install it. Silent in non-interactive contexts.
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") {
        // Prefer .bash_profile on macOS, .bashrc on Linux
        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 {
        // Fallback to .profile
        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()
}