use super::terminal::{TerminalOps, get_terminal_impl};
use crate::utils::get_current_path;
use crate::{Environment, RuntimeErrorKind, childman};
#[cfg(unix)]
use nix::sys::signal::{self, SaFlags, SigAction, SigHandler, SigSet};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use std::io::{self, Read, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
#[cfg(unix)]
const PTY_CMDS: &[&str] = &[
"lume", "bash", "sh", "fish", "top", "btop", "vi", "passwd", "ssh", "script", "expect",
"telnet", "screen", "tmux", "ftp", "sftp",
];
#[cfg(windows)]
const PTY_CMDS: &[&str] = &[
"lume",
"fish",
"ssh",
"telnet",
"screen",
"tmux",
"cmd.exe",
"PowerShell",
"Cygwin",
"WinPTY",
"ConPTY",
];
pub fn needs_pty(cmdstr: &str) -> bool {
PTY_CMDS.contains(&cmdstr)
}
#[derive(Clone, Copy)]
pub struct PtyProfile {
pub low_latency_output: bool,
pub enter_insert: Option<&'static [u8]>,
pub exit_insert: Option<&'static [u8]>,
}
const DEFAULT_PROFILE: PtyProfile = PtyProfile {
low_latency_output: false,
enter_insert: None,
exit_insert: None,
};
const VI_LIKE: &[&str] = &["vi", "vim", "nvim"];
const LOW_LATENCY_SHELLS: &[&str] = &[
"bash", "lume", "sh", "fish", "zsh", "ssh", "scp", "sftp", "top", "btop",
];
fn pty_profile(cmdstr: &str) -> PtyProfile {
if VI_LIKE.contains(&cmdstr) {
return PtyProfile {
low_latency_output: true,
enter_insert: Some(b"i"),
exit_insert: Some(&[27u8]), };
}
if LOW_LATENCY_SHELLS.contains(&cmdstr) {
return PtyProfile {
low_latency_output: true,
enter_insert: None,
exit_insert: None,
};
}
DEFAULT_PROFILE
}
struct TerminalGuard {
terminal: Box<dyn TerminalOps>,
#[cfg(unix)]
saved_sigint: Option<SigAction>,
}
impl TerminalGuard {
fn new(terminal: Box<dyn TerminalOps>) -> Result<Self, RuntimeErrorKind> {
#[cfg(unix)]
let saved_sigint = unsafe {
match signal::sigaction(
signal::Signal::SIGINT,
&SigAction::new(SigHandler::SigIgn, SaFlags::SA_RESTART, SigSet::empty()),
) {
Ok(prev) => {
let _ = signal::sigaction(
signal::Signal::SIGINT,
&SigAction::new(SigHandler::SigDfl, SaFlags::SA_RESTART, SigSet::empty()),
);
Some(prev)
}
Err(_) => None,
}
};
terminal.enable_raw_mode()?;
Ok(Self {
terminal,
#[cfg(unix)]
saved_sigint,
})
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = self.terminal.disable_raw_mode();
#[cfg(unix)]
if let Some(ref prev) = self.saved_sigint {
unsafe {
let _ = signal::sigaction(signal::Signal::SIGINT, prev);
}
}
}
}
pub fn exec_in_pty(
cmdstr: &String,
args: Option<Vec<String>>,
env: &mut Environment,
input: Option<Vec<u8>>,
) -> Result<Option<Vec<u8>>, RuntimeErrorKind> {
let terminal = get_terminal_impl();
let (w, h) = terminal.get_terminal_size();
let running = Arc::new(AtomicBool::new(false));
let running_clone = Arc::clone(&running);
let running_clone2 = Arc::clone(&running);
#[cfg(windows)]
terminal.handle_ctrl_c(Arc::clone(&running))?;
let _terminal_guard = TerminalGuard::new(terminal)?;
let profile = pty_profile(cmdstr.as_str());
let pty_system = native_pty_system();
let pair = pty_system
.openpty(PtySize {
rows: h,
cols: w,
pixel_width: 0,
pixel_height: 0,
})
.map_err(|e| RuntimeErrorKind::CustomError(e.to_string().into()))?;
#[cfg(unix)]
let pair_master_fd = pair.master.as_raw_fd();
#[cfg(unix)]
{
if let Some(master_fd) = pair_master_fd {
unsafe {
let mut termios = std::mem::zeroed();
if libc::tcgetattr(master_fd, &mut termios) == 0 {
termios.c_lflag |= libc::ECHO | libc::ICANON;
termios.c_lflag |= libc::ISIG;
termios.c_oflag |= libc::OPOST;
libc::tcsetattr(master_fd, libc::TCSANOW, &termios);
}
}
}
}
let mut cmd = CommandBuilder::new(cmdstr);
let current_dir = get_current_path(env);
cmd.cwd(current_dir);
if let Some(ag) = args {
cmd.args(ag);
}
for (k, v) in env.get_bindings_string() {
cmd.env(k, v);
}
let mut child = pair
.slave
.spawn_command(cmd)
.map_err(|e| RuntimeErrorKind::CustomError(e.to_string().into()))?;
if let Some(pid) = child.process_id() {
childman::set_child(pid);
}
let mut master_reader = pair
.master
.try_clone_reader()
.map_err(|e| RuntimeErrorKind::CustomError(e.to_string().into()))?;
let mut master_writer = pair
.master
.take_writer()
.map_err(|e| RuntimeErrorKind::CustomError(e.to_string().into()))?;
let use_low_latency = profile.low_latency_output;
let _output_thread = if use_low_latency {
thread::spawn(move || {
loop {
if running_clone2.load(Ordering::SeqCst) {
break;
}
let mut buffer = [0u8; 1024];
match master_reader.read(&mut buffer) {
Ok(_) => io::stdout().write_all(&buffer).unwrap(),
Err(_) => break,
}
let _ = io::stdout().flush();
thread::yield_now();
}
})
} else {
thread::spawn(move || {
let _ = io::copy(&mut master_reader, &mut io::stdout());
})
};
let enter_insert = profile.enter_insert;
let exit_insert = profile.exit_insert;
let input_thread = thread::spawn(move || {
if let Some(last_input) = input {
if let Some(seq) = enter_insert {
#[cfg(unix)]
unsafe {
for _ in 0..100 {
let mut termios: libc::termios = std::mem::zeroed();
if let Some(master_fd) = pair_master_fd
&& libc::tcgetattr(master_fd, &mut termios) == 0
&& termios.c_lflag & libc::ECHO == 0
{
break; }
thread::sleep(Duration::from_millis(10));
}
}
let _ = master_writer.write_all(seq);
}
if let Err(e) = master_writer.write_all(&last_input) {
eprintln!("Failed to write to master: {e}");
}
if let Err(e) = master_writer.flush() {
eprintln!("Failed to flush master: {e}");
}
if let Some(seq) = exit_insert {
let _ = master_writer.write_all(b"\n");
let _ = master_writer.write_all(seq);
thread::sleep(Duration::from_millis(50));
let _ = master_writer.flush();
}
}
let mut input_buffer = [0u8];
loop {
if running_clone.load(Ordering::SeqCst) {
break;
}
match io::stdin().read_exact(&mut input_buffer) {
Ok(_) => {
let _ = master_writer.write_all(&input_buffer);
}
Err(_) => break,
}
let _ = master_writer.flush();
thread::yield_now();
}
});
child.wait()?;
running.store(true, Ordering::SeqCst);
let _ = input_thread.join();
if use_low_latency {
let _ = _output_thread.join();
}
Ok(None)
}