use std::io::{self, IsTerminal, Write};
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use std::thread;
use tracing::debug;
use crate::engine::io::{capture_pty_output, forward_stdin, tee_stderr};
use crate::engine::parser::SimpleCommand;
use crate::engine::pty::create_session_pty;
use crate::engine::terminal::TerminalStateGuard;
use crate::engine::{CommandResult, LoopAction};
pub(super) fn run_single_command_pty_session(simple: &SimpleCommand) -> io::Result<CommandResult> {
if cfg!(test) || !io::stdout().is_terminal() {
return Err(io::Error::other("PTY session not available"));
}
let cmd = &simple.cmd;
let args: Vec<&str> = simple.args.iter().map(|s| s.as_str()).collect();
debug!(command = %cmd, args = ?args, "Spawning external command (PTY session)");
let (master, slave) = create_session_pty()?;
let master_raw_fd = master.as_raw_fd();
let (stderr_read, stderr_write) = os_pipe::pipe()?;
let mut terminal_guard = TerminalStateGuard::new()?;
let slave_raw_fd = slave.as_raw_fd();
let stdin_fd = unsafe { libc::dup(slave_raw_fd) };
let stdout_fd = unsafe { libc::dup(slave_raw_fd) };
if stdin_fd < 0 || stdout_fd < 0 {
return Err(io::Error::last_os_error());
}
let mut child = {
let mut command = Command::new(cmd);
command
.args(&args)
.stdin(unsafe { Stdio::from_raw_fd(stdin_fd) })
.stdout(unsafe { Stdio::from_raw_fd(stdout_fd) })
.stderr(Stdio::from(stderr_write));
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}
if libc::ioctl(0, libc::TIOCSCTTY as libc::c_ulong, 0) == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
match command.spawn() {
Ok(child) => child,
Err(e) => {
return Err(e);
}
}
};
drop(slave);
if let Err(e) = terminal_guard.activate_raw_mode() {
debug!("Failed to set raw mode: {e}");
}
let (shutdown_read, shutdown_write) = os_pipe::pipe()?;
let master_for_stdin = master.try_clone()?;
let stdin_handle = thread::spawn(move || {
forward_stdin(master_for_stdin, shutdown_read, master_raw_fd);
});
let output_handle = thread::spawn(move || capture_pty_output(master));
let stderr_handle = thread::spawn(move || tee_stderr(stderr_read));
let exit_code = match child.wait() {
Ok(status) => status.code().unwrap_or(1),
Err(e) => {
eprintln!("jarvish: wait error: {e}");
1
}
};
drop(shutdown_write);
let _ = stdin_handle.join();
let capture = output_handle.join().unwrap_or_default();
let stderr_bytes = stderr_handle.join().unwrap_or_default();
drop(terminal_guard);
if capture.used_alt_screen {
let _ = io::stdout().flush();
std::thread::sleep(std::time::Duration::from_millis(50));
let _ =
nix::sys::termios::tcflush(io::stdin().as_fd(), nix::sys::termios::FlushArg::TCIFLUSH);
}
debug!(
command = %cmd,
exit_code = exit_code,
stdout_size = capture.bytes.len(),
stderr_size = stderr_bytes.len(),
used_alt_screen = capture.used_alt_screen,
"External command completed (PTY session)"
);
Ok(CommandResult {
stdout: String::from_utf8_lossy(&capture.bytes).to_string(),
stderr: String::from_utf8_lossy(&stderr_bytes).to_string(),
exit_code,
action: LoopAction::Continue,
used_alt_screen: capture.used_alt_screen,
})
}