runex 0.1.13

Cross-shell abbreviation engine that expands short tokens into full commands
#[cfg(target_family = "unix")]
mod zsh {
    use std::io::Write;
    use std::process::Command;
    use tempfile::NamedTempFile;

    /// Returns false when zsh is not installed on the runner. GitHub
    /// Actions' ubuntu-latest image does not ship zsh by default, so
    /// these tests are skipped there rather than failing the whole
    /// release pipeline. Local Linux developers (Arch / Debian) almost
    /// always have zsh available.
    fn zsh_available() -> bool {
        which::which("zsh").is_ok()
    }

    fn write_config() -> NamedTempFile {
        let mut f = NamedTempFile::new().unwrap();
        write!(
            f,
            "version = 1\n\n[[abbr]]\nkey = \"gcm\"\nexpand = \"echo EXPANDED\"\n"
        )
        .unwrap();
        f.flush().unwrap();
        f
    }

    fn bin_path() -> &'static str {
        env!("CARGO_BIN_EXE_runex")
    }

    fn run_helper(config: &NamedTempFile, left: &str, right: &str) -> String {
        // The new hook-based template exposes `__runex_expand` as a zle
        // widget. Widgets can only be invoked inside zle, so we drive the
        // hook CLI directly — mirroring exactly what the widget would have
        // done with the injected buffer.
        let script = r#"
line="${RUNEX_LEFT}${RUNEX_RIGHT}"
cursor=${#RUNEX_LEFT}
out=$("$RUNEX_BIN" hook --shell zsh --line "$line" --cursor "$cursor" 2>/dev/null)
LBUFFER=""
RBUFFER=""
if [[ -n "$out" ]]; then
    eval "$out"
else
    LBUFFER="${RUNEX_LEFT} "
    RBUFFER="${RUNEX_RIGHT}"
fi
printf '%s|%s\n' "$LBUFFER" "$RBUFFER"
"#;

        let output = Command::new("zsh")
            .args(["-f", "-c", script])
            .env("RUNEX_BIN", bin_path())
            .env("RUNEX_CONFIG", config.path())
            .env("RUNEX_LEFT", left)
            .env("RUNEX_RIGHT", right)
            .output()
            .unwrap();

        assert!(
            output.status.success(),
            "zsh helper should succeed\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );

        String::from_utf8_lossy(&output.stdout).trim().to_string()
    }

    #[test]
    fn expand_at_end() {
        if !zsh_available() { eprintln!("skipping: zsh not found on PATH"); return; }
        let config = write_config();
        assert_eq!(run_helper(&config, "gcm", ""), "echo EXPANDED |");
    }

    #[test]
    fn midline_space_is_plain_insert() {
        if !zsh_available() { eprintln!("skipping: zsh not found on PATH"); return; }
        let config = write_config();
        assert_eq!(run_helper(&config, "g", "cm tail"), "g |cm tail");
    }

    #[test]
    fn expands_after_separator() {
        if !zsh_available() { eprintln!("skipping: zsh not found on PATH"); return; }
        let config = write_config();
        assert_eq!(
            run_helper(&config, "echo foo && gcm", ""),
            "echo foo && echo EXPANDED |"
        );
    }

    #[test]
    fn expands_after_sudo() {
        if !zsh_available() { eprintln!("skipping: zsh not found on PATH"); return; }
        let config = write_config();
        assert_eq!(run_helper(&config, "sudo gcm", ""), "sudo echo EXPANDED |");
    }

    #[test]
    fn argument_position_does_not_expand() {
        if !zsh_available() { eprintln!("skipping: zsh not found on PATH"); return; }
        let config = write_config();
        assert_eq!(run_helper(&config, "echo gcm", ""), "echo gcm |");
    }

    #[test]
    fn unknown_token_stays_as_is() {
        if !zsh_available() { eprintln!("skipping: zsh not found on PATH"); return; }
        let config = write_config();
        assert_eq!(run_helper(&config, "xyz", ""), "xyz |");
    }

    #[test]
    fn option_like_token_stays_intact() {
        if !zsh_available() { eprintln!("skipping: zsh not found on PATH"); return; }
        let config = write_config();
        assert_eq!(
            run_helper(&config, "cargo install --path", ""),
            "cargo install --path |"
        );
    }
}