kabu 0.7.1

CLI tool to enhance git worktree and jj workspace with automated setup
use crate::cli::InitArgs;
use crate::error::{Error, Result};

use clap_complete::Shell;

use std::env;
use std::path::PathBuf;

const KABU_PATH_PLACEHOLDER: &str = "::KABU::";

const BASH_INIT: &str = include_str!("init/kabu.bash");
const ZSH_INIT: &str = include_str!("init/kabu.zsh");
const FISH_INIT: &str = include_str!("init/kabu.fish");
const POWERSHELL_INIT: &str = include_str!("init/kabu.ps1");
const ELVISH_INIT: &str = include_str!("init/kabu.elv");

pub(crate) fn run(args: InitArgs) -> Result<()> {
    if args.print_full_init {
        let script = init_main(args.shell)?;
        print!("{script}");
    } else {
        let script = init_stub(args.shell)?;
        print!("{script}");
    }
    Ok(())
}

fn init_stub(shell: Shell) -> Result<String> {
    let kabu = KabuPath::init()?;
    let stub = match shell {
        Shell::Bash => format!(
            r#"eval -- "$({} init bash --print-full-init)""#,
            kabu.sprint_posix()?
        ),
        Shell::Zsh => format!(
            r#"source <({} init zsh --print-full-init)"#,
            kabu.sprint_posix()?
        ),
        Shell::Fish => format!(
            r#"source ({} init fish --print-full-init | psub)"#,
            kabu.sprint_posix()?
        ),
        Shell::PowerShell => format!(
            r#"Invoke-Expression (& {} init powershell --print-full-init | Out-String)"#,
            kabu.sprint_pwsh()?
        ),
        Shell::Elvish => format!(
            r#"eval ({} init elvish --print-full-init | slurp)"#,
            kabu.sprint_posix()?
        ),
        _ => {
            return Err(Error::Internal(format!(
                "Unsupported shell for init: {shell:?}"
            )));
        }
    };
    Ok(stub)
}

fn init_main(shell: Shell) -> Result<String> {
    let kabu = KabuPath::init()?;
    let script = match shell {
        Shell::Bash => print_script(BASH_INIT, &kabu.sprint_posix()?),
        Shell::Zsh => print_script(ZSH_INIT, &kabu.sprint_posix()?),
        Shell::Fish => print_script(FISH_INIT, &kabu.sprint_posix()?),
        Shell::PowerShell => print_script(POWERSHELL_INIT, &kabu.sprint_pwsh()?),
        Shell::Elvish => print_script(ELVISH_INIT, &kabu.sprint_posix()?),
        _ => {
            return Err(Error::Internal(format!(
                "Unsupported shell for init: {shell:?}"
            )));
        }
    };
    Ok(script)
}

fn print_script(script: &str, path: &str) -> String {
    script.replace(KABU_PATH_PLACEHOLDER, path)
}

struct KabuPath {
    native_path: PathBuf,
}

impl KabuPath {
    fn init() -> Result<Self> {
        let exe = env::current_exe().map_err(|e| {
            Error::Internal(format!("Failed to determine kabu executable path: {}", e))
        })?;
        Ok(Self { native_path: exe })
    }

    fn str_path(&self) -> Result<&str> {
        self.native_path
            .to_str()
            .ok_or_else(|| Error::Internal("Failed to convert kabu path to string".to_string()))
    }

    fn sprint_posix(&self) -> Result<String> {
        let path = self.str_path()?;
        Ok(posix_quote(path))
    }

    fn sprint_pwsh(&self) -> Result<String> {
        let path = self.str_path()?;
        Ok(format!("'{}'", path.replace('\'', "''")))
    }
}

fn posix_quote(input: &str) -> String {
    if input.is_empty() {
        return "''".to_string();
    }
    if !input.contains('\'') {
        return format!("'{input}'");
    }
    let mut out = String::with_capacity(input.len() + 2);
    out.push('\'');
    for ch in input.chars() {
        if ch == '\'' {
            out.push_str("'\"'\"'");
        } else {
            out.push(ch);
        }
    }
    out.push('\'');
    out
}

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

    #[test]
    fn test_posix_quote_empty_string() {
        assert_eq!(posix_quote(""), "''");
    }

    #[test]
    fn test_posix_quote_no_special_chars() {
        assert_eq!(posix_quote("/usr/bin/kabu"), "'/usr/bin/kabu'");
    }

    #[test]
    fn test_posix_quote_single_quote() {
        assert_eq!(posix_quote("it's"), "'it'\"'\"'s'");
    }

    #[test]
    fn test_posix_quote_multiple_single_quotes() {
        assert_eq!(posix_quote("'a'b'"), "''\"'\"'a'\"'\"'b'\"'\"''");
    }

    #[test]
    fn test_posix_quote_mixed_special_chars() {
        assert_eq!(
            posix_quote("/path/to/'file'"),
            "'/path/to/'\"'\"'file'\"'\"''"
        );
    }
}