sdd-layer 0.19.0

Spec-Driven Development CLI and agent harness
//! Abertura do `$EDITOR` com suspensão/restauração do terminal (T-16, RF-08, R-07).

use anyhow::{Context, Result};
use crossterm::execute;
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use std::io::stdout;
use std::path::Path;
use std::process::{Command, Stdio};

/// Programa de editor a usar.
///
/// Precedência cross-platform: `$VISUAL` → `$EDITOR` → padrão por SO
/// (Windows = `notepad`, demais = `vi`). Delega ao helper de plataforma.
pub fn editor_program() -> String {
    crate::runtime::platform::default_editor()
}

/// Suspende a TUI (sai do alternate screen + raw mode), abre o editor no `path`
/// e restaura o terminal ao fechar. O `path` é passado como argumento separado —
/// nunca via shell (sem injeção). Restaura o terminal mesmo em caso de erro.
pub fn open_editor(path: &Path) -> Result<()> {
    let program = editor_program();

    // `$EDITOR`/`$VISUAL` podem carregar argumentos (ex.: "code --wait"): o
    // primeiro token é o executável (resolvido via PATH/PATHEXT) e o restante
    // são argumentos do editor. Sem isso, "code --wait" seria tratado como um
    // único nome de programa e o spawn falharia.
    let mut tokens = program.split_whitespace();
    let editor_bin = tokens.next().unwrap_or(program.as_str());
    let editor_args: Vec<&str> = tokens.collect();
    let editor_exe = crate::runtime::platform::find_executable(editor_bin)
        .unwrap_or_else(|| std::path::PathBuf::from(editor_bin));

    // Suspende a TUI antes de devolver o terminal ao editor.
    disable_raw_mode().ok();
    execute!(stdout(), LeaveAlternateScreen).ok();

    let status = Command::new(&editor_exe)
        .args(&editor_args)
        .arg(path)
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status();

    // Restaura a TUI independentemente do resultado do editor.
    let _ = enable_raw_mode();
    let _ = execute!(stdout(), EnterAlternateScreen);

    let status = status.with_context(|| format!("executando editor '{program}'"))?;
    if !status.success() {
        anyhow::bail!("editor '{program}' encerrou com código {:?}", status.code());
    }
    Ok(())
}

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

    #[test]
    fn fallback_is_platform_aware_when_unset() {
        // Serializa com os demais testes que mutam EDITOR/VISUAL (cargo roda em
        // paralelo no mesmo processo) e não altera env global de forma duradoura.
        let _env = crate::runtime::platform::lock_env();
        let saved_visual = std::env::var("VISUAL").ok();
        let saved_editor = std::env::var("EDITOR").ok();
        std::env::remove_var("VISUAL");
        std::env::remove_var("EDITOR");
        assert_eq!(editor_program(), crate::runtime::platform::default_editor());
        match saved_visual {
            Some(v) => std::env::set_var("VISUAL", v),
            None => std::env::remove_var("VISUAL"),
        }
        match saved_editor {
            Some(v) => std::env::set_var("EDITOR", v),
            None => std::env::remove_var("EDITOR"),
        }
    }

    #[test]
    fn uses_editor_env_when_set() {
        let _env = crate::runtime::platform::lock_env();
        let saved_visual = std::env::var("VISUAL").ok();
        let saved_editor = std::env::var("EDITOR").ok();
        std::env::remove_var("VISUAL");
        std::env::set_var("EDITOR", "nano");
        assert_eq!(editor_program(), "nano");
        match saved_visual {
            Some(v) => std::env::set_var("VISUAL", v),
            None => std::env::remove_var("VISUAL"),
        }
        match saved_editor {
            Some(v) => std::env::set_var("EDITOR", v),
            None => std::env::remove_var("EDITOR"),
        }
    }
}