limb 0.1.0

A focused CLI for git worktree management
Documentation
//! Shell integration emitted by `limb init <shell>`.
//!
//! Each supported shell gets a dedicated function emitting:
//!
//! 1. A `__limb_cd_hook` that runs every prompt. Inside tmux, it consumes a
//!    pending-cd marker file written by [`crate::cmd::mark_cd`] and changes
//!    the shell's cwd. Making editor-driven worktree switches propagate
//!    back to the shell on exit.
//! 2. Wrapper functions (`{prefix}`, `{prefix}a`, …) that invoke the `limb`
//!    binary and act on its stdout (e.g. `cd` into the printed path).
//!
//! The output is meant to be `eval`'d inside the user's rc file; it is
//! self-contained and inspects `$TMUX_PANE` / `$TMPDIR` at hook time rather
//! than at emission time, so the same output works across machines.

use crate::cli::Shell;

/// Returns the shell-rc script that wires up `limb`'s integration for `shell`.
///
/// `prefix` controls the wrapper-function names (default: `gw`, so `gw`,
/// `gwa`, `gwp`, `gws`, `gwr`, `gwu`).
#[must_use]
pub fn integration(shell: Shell, prefix: &str) -> String {
    match shell {
        Shell::Zsh => zsh(prefix),
        Shell::Bash => bash(prefix),
        Shell::Fish => fish(prefix),
        Shell::Pwsh => pwsh(prefix),
    }
}

fn zsh(p: &str) -> String {
    format!(
        r#"__limb_cd_hook() {{
  [[ -z "$TMUX_PANE" ]] && return
  local marker="${{TMPDIR:-/tmp/}}limb-pending-cd-$TMUX_PANE"
  if [[ -r "$marker" ]]; then
    local target
    target="$(head -n1 "$marker" 2>/dev/null)"
    command rm -f "$marker"
    if [[ -n "$target" && -d "$target" ]]; then
      builtin cd "$target"
    fi
  fi
}}
autoload -Uz add-zsh-hook 2>/dev/null
typeset -f add-zsh-hook > /dev/null && add-zsh-hook -Uz precmd __limb_cd_hook

{p}() {{
  local path
  path="$(command limb cd "$@")" || return
  [[ -n "$path" ]] && builtin cd "$path"
}}

{p}a() {{
  command limb add "$@"
}}

{p}r() {{
  command limb remove "$@"
}}

{p}p() {{
  local path
  path="$(command limb pick)" || return
  [[ -n "$path" ]] && builtin cd "$path"
}}

{p}s() {{
  command limb status "$@"
}}

{p}u() {{
  command limb update "$@"
}}
"#
    )
}

fn bash(p: &str) -> String {
    format!(
        r#"__limb_cd_hook() {{
  [[ -z "$TMUX_PANE" ]] && return
  local marker="${{TMPDIR:-/tmp/}}limb-pending-cd-$TMUX_PANE"
  if [[ -r "$marker" ]]; then
    local target
    target="$(head -n1 "$marker" 2>/dev/null)"
    command rm -f "$marker"
    if [[ -n "$target" && -d "$target" ]]; then
      builtin cd "$target"
    fi
  fi
}}
case ";${{PROMPT_COMMAND:-}};" in
  *";__limb_cd_hook;"*) ;;
  *) PROMPT_COMMAND="__limb_cd_hook${{PROMPT_COMMAND:+;$PROMPT_COMMAND}}" ;;
esac

{p}() {{
  local path
  path="$(command limb cd "$@")" || return
  [[ -n "$path" ]] && builtin cd "$path"
}}

{p}a() {{
  command limb add "$@"
}}

{p}r() {{
  command limb remove "$@"
}}

{p}p() {{
  local path
  path="$(command limb pick)" || return
  [[ -n "$path" ]] && builtin cd "$path"
}}

{p}s() {{
  command limb status "$@"
}}

{p}u() {{
  command limb update "$@"
}}
"#
    )
}

fn fish(p: &str) -> String {
    format!(
        r#"function __limb_cd_hook --on-event fish_prompt
    test -z "$TMUX_PANE"; and return
    set -l tmpdir (test -n "$TMPDIR"; and echo $TMPDIR; or echo "/tmp/")
    set -l marker "$tmpdir""limb-pending-cd-$TMUX_PANE"
    if test -r "$marker"
        set -l target (head -n1 "$marker" 2>/dev/null)
        command rm -f "$marker"
        if test -n "$target" -a -d "$target"
            builtin cd "$target"
        end
    end
end

function {p}
    set -l path (command limb cd $argv)
    or return
    if test -n "$path"
        builtin cd $path
    end
end

function {p}a
    command limb add $argv
end

function {p}r
    command limb remove $argv
end

function {p}p
    set -l path (command limb pick)
    or return
    if test -n "$path"
        builtin cd $path
    end
end

function {p}s
    command limb status $argv
end

function {p}u
    command limb update $argv
end
"#
    )
}

fn pwsh(p: &str) -> String {
    format!(
        r#"if (-not (Test-Path Function:\__Limb_OriginalPrompt)) {{
    Copy-Item -Path Function:\prompt -Destination Function:\__Limb_OriginalPrompt
    function prompt {{
        if ($env:TMUX_PANE) {{
            $tmpdir = if ($env:TMPDIR) {{ $env:TMPDIR }} else {{ "/tmp/" }}
            $marker = "$tmpdir" + "limb-pending-cd-$($env:TMUX_PANE)"
            if (Test-Path $marker) {{
                $target = Get-Content $marker -TotalCount 1 -ErrorAction SilentlyContinue
                Remove-Item $marker -ErrorAction SilentlyContinue
                if ($target -and (Test-Path $target)) {{
                    Set-Location $target
                }}
            }}
        }}
        & __Limb_OriginalPrompt
    }}
}}

function {p} {{
    $path = (& limb cd @args)
    if ($LASTEXITCODE -eq 0 -and $path) {{
        Set-Location $path
    }}
}}

function {p}a {{ & limb add @args }}
function {p}r {{ & limb remove @args }}
function {p}s {{ & limb status @args }}
function {p}u {{ & limb update @args }}

function {p}p {{
    $path = (& limb pick)
    if ($LASTEXITCODE -eq 0 -and $path) {{
        Set-Location $path
    }}
}}
"#
    )
}

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

    #[test]
    fn zsh_default_prefix() {
        let out = integration(Shell::Zsh, "gw");
        assert!(out.contains("gw() {"));
        assert!(out.contains("gwa() {"));
        assert!(out.contains("gwp() {"));
        assert!(out.contains("gws() {"));
        assert!(out.contains("gwr() {"));
        assert!(out.contains("gwu() {"));
    }

    #[test]
    fn zsh_custom_prefix() {
        let out = integration(Shell::Zsh, "lm");
        assert!(out.contains("lm() {"));
        assert!(out.contains("lma() {"));
        assert!(!out.contains("gw() {"));
    }

    #[test]
    fn zsh_emits_cd_hook() {
        let out = integration(Shell::Zsh, "gw");
        assert!(out.contains("__limb_cd_hook"));
        assert!(out.contains("TMUX_PANE"));
        assert!(out.contains("add-zsh-hook"));
    }

    #[test]
    fn bash_emits_cd_hook() {
        let out = integration(Shell::Bash, "gw");
        assert!(out.contains("__limb_cd_hook"));
        assert!(out.contains("PROMPT_COMMAND"));
    }

    #[test]
    fn fish_emits_cd_hook() {
        let out = integration(Shell::Fish, "gw");
        assert!(out.contains("__limb_cd_hook"));
        assert!(out.contains("--on-event fish_prompt"));
    }

    #[test]
    fn pwsh_emits_cd_hook() {
        let out = integration(Shell::Pwsh, "gw");
        assert!(out.contains("__Limb_OriginalPrompt"));
        assert!(out.contains("TMUX_PANE"));
    }

    #[test]
    fn fish_uses_function_syntax() {
        let out = integration(Shell::Fish, "gw");
        assert!(out.contains("function gw"));
        assert!(out.contains("function gwa"));
        assert!(!out.contains("gw() {"));
    }

    #[test]
    fn pwsh_uses_function_syntax() {
        let out = integration(Shell::Pwsh, "gw");
        assert!(out.contains("function gw"));
        assert!(out.contains("Set-Location"));
    }

    #[test]
    fn bash_uses_bash_syntax() {
        let out = integration(Shell::Bash, "gw");
        assert!(out.contains("gw() {"));
        assert!(out.contains("builtin cd"));
    }
}