cryosnap 0.2.11

CLI for cryosnap.
use std::error::Error;

pub(crate) fn capture_tmux_output(raw_args: Option<&str>) -> Result<String, Box<dyn Error>> {
    let user_args = normalize_tmux_args(raw_args)?;
    let cmd_args = build_tmux_capture_args(&user_args);
    let output = std::process::Command::new("tmux")
        .args(cmd_args)
        .output()
        .map_err(|err| format!("failed to run tmux: {err}"))?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!("tmux capture failed: {}", stderr.trim()).into());
    }
    let text = String::from_utf8_lossy(&output.stdout).to_string();
    if text.is_empty() {
        return Err("tmux returned empty output".into());
    }
    Ok(text)
}

pub(crate) fn tmux_title(raw_args: Option<&str>, format: &str) -> Option<String> {
    let format = format.trim();
    if format.is_empty() {
        return None;
    }
    let user_args = normalize_tmux_args(raw_args).ok()?;
    let target = extract_tmux_target(&user_args);
    let mut cmd = std::process::Command::new("tmux");
    cmd.arg("display-message").arg("-p");
    if let Some(target) = target {
        cmd.arg("-t").arg(target);
    }
    cmd.arg(format);
    let output = cmd.output().ok()?;
    if !output.status.success() {
        return None;
    }
    let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if text.is_empty() {
        None
    } else {
        Some(text)
    }
}

pub(crate) fn extract_tmux_target(args: &[String]) -> Option<String> {
    let mut iter = args.iter().peekable();
    while let Some(arg) = iter.next() {
        if arg == "-t" {
            return iter.next().cloned();
        }
        if arg.starts_with("-t") {
            return Some(arg.trim_start_matches("-t").to_string());
        }
    }
    None
}

pub(crate) fn normalize_tmux_args(raw: Option<&str>) -> Result<Vec<String>, Box<dyn Error>> {
    match raw {
        Some(value) => {
            let args =
                shell_words::split(value).map_err(|err| format!("tmux args parse: {err}"))?;
            Ok(args)
        }
        None => Ok(Vec::new()),
    }
}

pub(crate) fn build_tmux_capture_args(user_args: &[String]) -> Vec<String> {
    let has_p = user_args.iter().any(|arg| arg == "-p");
    let has_e = user_args.iter().any(|arg| arg == "-e");
    let mut args = Vec::new();
    args.push("capture-pane".to_string());
    if !has_p {
        args.push("-p".to_string());
    }
    if !has_e {
        args.push("-e".to_string());
    }
    args.extend(user_args.iter().cloned());
    args
}

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

    #[test]
    fn build_tmux_capture_args_defaults() {
        let args = build_tmux_capture_args(&[]);
        assert!(args.contains(&"capture-pane".to_string()));
        assert!(args.contains(&"-p".to_string()));
        assert!(args.contains(&"-e".to_string()));
    }

    #[test]
    fn build_tmux_capture_args_preserves_flags() {
        let args = build_tmux_capture_args(&[
            "-p".to_string(),
            "-e".to_string(),
            "-t".to_string(),
            "%3".to_string(),
        ]);
        assert_eq!(
            args,
            vec!["capture-pane", "-p", "-e", "-t", "%3",]
                .into_iter()
                .map(String::from)
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn normalize_tmux_args_accepts_raw_string() {
        let args = normalize_tmux_args(Some("-t %3 -S -200 -J")).expect("parse");
        assert_eq!(
            args,
            vec!["-t", "%3", "-S", "-200", "-J"]
                .into_iter()
                .map(String::from)
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn extract_tmux_target_from_separate_flag() {
        let args = vec![
            "-t".to_string(),
            "%7".to_string(),
            "-S".to_string(),
            "-200".to_string(),
        ];
        assert_eq!(extract_tmux_target(&args), Some("%7".to_string()));
    }

    #[test]
    fn extract_tmux_target_from_compact_flag() {
        let args = vec!["-t%9".to_string(), "-S".to_string(), "-200".to_string()];
        assert_eq!(extract_tmux_target(&args), Some("%9".to_string()));
    }

    #[test]
    fn tmux_title_empty_format_returns_none() {
        assert!(tmux_title(None, " ").is_none());
    }

    #[test]
    fn tmux_title_missing_tmux_returns_none() {
        let _lock = env_lock().lock().expect("lock");
        let prev_path = std::env::var("PATH").ok();
        std::env::set_var("PATH", "");
        assert!(tmux_title(None, "#{pane_title}").is_none());
        if let Some(path) = prev_path {
            std::env::set_var("PATH", path);
        } else {
            std::env::remove_var("PATH");
        }
    }

    #[test]
    fn capture_tmux_output_missing_tmux_errors() {
        let _lock = env_lock().lock().expect("lock");
        let prev_path = std::env::var("PATH").ok();
        std::env::set_var("PATH", "");
        let err = capture_tmux_output(None).unwrap_err();
        assert!(err.to_string().contains("failed to run tmux"));
        if let Some(path) = prev_path {
            std::env::set_var("PATH", path);
        } else {
            std::env::remove_var("PATH");
        }
    }
}