use std::path::{Path, PathBuf};
use std::process::Stdio;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use super::events::{EventHandler, TuiEvent};
use super::runner::{self, TuiTerminal};
pub(crate) const EDITOR_ALLOWLIST: &[&str] = &["vi", "vim", "nano", "emacs"];
pub(crate) fn handoff_target(cmd: &str) -> Option<&'static str> {
let argv0 = cmd.split_whitespace().next()?;
EDITOR_ALLOWLIST.iter().copied().find(|ed| *ed == argv0)
}
pub(crate) fn split_argv(cmd: &str) -> Option<(String, Vec<String>)> {
let mut it = cmd.split_whitespace();
let program = it.next()?.to_string();
let args = it.map(str::to_string).collect();
Some((program, args))
}
pub(crate) fn resolve_editor(name: &str, path_dirs: Option<&str>, cwd: &Path) -> Option<PathBuf> {
let effective;
let path: Option<&str> = match path_dirs {
Some(p) => Some(p),
None => {
effective = std::env::var("PATH").unwrap_or_default();
Some(effective.as_str())
}
};
which::which_in(name, path, cwd).ok()
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum WaitOutcome {
Exited(i32),
Signaled(i32),
Stopped(i32),
}
pub(crate) fn decode_wait_status(status: i32) -> WaitOutcome {
if status & 0x7f == 0 {
WaitOutcome::Exited((status >> 8) & 0xff)
} else if status & 0x7f == 0x7f {
WaitOutcome::Stopped((status >> 8) & 0xff)
} else {
WaitOutcome::Signaled(status & 0x7f)
}
}
#[derive(Debug)]
pub(crate) struct EditorResult {
pub text: String,
}
pub(crate) async fn run_editor_handoff(
terminal: &mut TuiTerminal,
event_sender: mpsc::UnboundedSender<TuiEvent>,
listener_handle: &mut JoinHandle<()>,
cmd: &str,
cwd: &Path,
) -> EditorResult {
let (program, args) = match split_argv(cmd) {
Some(pair) => pair,
None => {
return EditorResult {
text: "empty editor command".to_string(),
};
}
};
let resolved = match resolve_editor(&program, None, cwd) {
Some(p) => p,
None => {
return EditorResult {
text: format!("{program} not found in PATH — editor handoff aborted."),
};
}
};
listener_handle.abort();
runner::force_restore_terminal();
let spawn = std::process::Command::new(&resolved)
.args(&args)
.current_dir(cwd)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn();
let child = match spawn {
Ok(c) => c,
Err(e) => {
let r = finish_handoff(terminal, event_sender, listener_handle);
return EditorResult {
text: if let Err(e2) = r {
format!("failed to start {program}: {e}; terminal restore failed: {e2}")
} else {
format!("failed to start {program}: {e}")
},
};
}
};
let pid = child.id() as libc::pid_t;
std::mem::forget(child);
reap_editor(pid).await;
let result = finish_handoff(terminal, event_sender, listener_handle);
let text = format!("{} exited.", program);
match result {
Ok(()) => EditorResult { text },
Err(e) => EditorResult {
text: format!("{text} (terminal re-init failed: {e})"),
},
}
}
pub(crate) async fn reap_editor(pid: libc::pid_t) {
let mut status: libc::c_int = 0;
loop {
let r = unsafe { libc::waitpid(pid, &mut status, libc::WUNTRACED | libc::WNOHANG) };
if r == pid {
match decode_wait_status(status) {
WaitOutcome::Stopped(_sig) => {
unsafe {
libc::kill(pid, libc::SIGCONT);
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
unsafe {
libc::kill(pid, libc::SIGTERM);
}
if !reap_exit(pid, std::time::Duration::from_secs(1)).await {
unsafe {
libc::kill(pid, libc::SIGKILL);
}
reap_exit(pid, std::time::Duration::from_secs(2)).await;
}
return;
}
_ => return,
}
}
if r < 0 {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
async fn reap_exit(pid: libc::pid_t, timeout: std::time::Duration) -> bool {
let mut status: libc::c_int = 0;
let start = tokio::time::Instant::now();
loop {
let r = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) };
if r == pid || r < 0 {
return true; }
if start.elapsed() >= timeout {
return false;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
}
fn finish_handoff(
terminal: &mut TuiTerminal,
event_sender: mpsc::UnboundedSender<TuiEvent>,
listener_handle: &mut JoinHandle<()>,
) -> anyhow::Result<()> {
runner::reinit_terminal_for_editor(terminal)?;
*listener_handle = EventHandler::start_terminal_listener(event_sender);
Ok(())
}