use std::ffi::{OsStr, OsString};
use std::fs::OpenOptions;
use std::io::{self, Read, Write};
use std::mem;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
use std::process::ExitCode;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc,
};
use std::thread;
use std::time::Duration;
use is_terminal::IsTerminal;
use nix::libc;
use nix::sys::termios::{
InputFlags, LocalFlags, OutputFlags, SetArg, Termios, cfmakeraw, tcgetattr, tcsetattr,
};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use super::CliError;
use super::args::Options;
use super::profile_selection::dynamic_profile_enabled;
use super::runtime::{ReloadWatcher, RuntimeRegistration};
use super::stream::highlight_stream;
use super::trace::IoTrace;
const STRIPPED_ITERM_ENV: [&str; 6] = [
"TERM_PROGRAM",
"TERM_PROGRAM_VERSION",
"LC_TERMINAL",
"LC_TERMINAL_VERSION",
"ITERM_SESSION_ID",
"ITERM_PROFILE",
];
pub(super) fn run_command(options: Options, command: Vec<OsString>) -> Result<ExitCode, CliError> {
let command_name = command[0].clone();
let command_args = command[1..].to_vec();
let pty_system = native_pty_system();
let pair = pty_system.openpty(current_pty_size())?;
let interactive = io::stdin().is_terminal();
let mut builder = CommandBuilder::new(command_name);
for arg in command_args {
builder.arg(arg);
}
apply_iterm_shell_integration_guard(&mut builder, interactive, parent_terminal_is_iterm());
if interactive {
configure_child_pty(&*pair.master)?;
}
let mut child = pair.slave.spawn_command(builder)?;
drop(pair.slave);
let raw_mode = if interactive {
Some(RawModeGuard::enable()?)
} else {
None
};
let trace = IoTrace::open(options.trace_io.as_deref())?;
let (profile_input_tx, profile_input_rx) = if dynamic_profile_enabled(&options, interactive) {
let (tx, rx) = mpsc::channel();
(Some(tx), Some(rx))
} else {
(None, None)
};
if raw_mode.is_some() {
let mut writer = pair.master.take_writer()?;
let trace = trace.clone();
let local_echo = options.local_echo;
thread::spawn(move || {
let stdin = io::stdin();
let mut stdin = stdin.lock();
let _ =
forward_stdin_to_pty(&mut stdin, &mut writer, local_echo, trace, profile_input_tx);
});
}
let mut reader = pair.master.try_clone_reader()?;
let stop_resize = Arc::new(AtomicBool::new(false));
let resize_thread = {
let stop_resize = Arc::clone(&stop_resize);
let master = pair.master;
thread::spawn(move || poll_pty_size(master, stop_resize))
};
let mut stdout = io::stdout();
let _registration = RuntimeRegistration::register()?;
let reload_watcher = Some(ReloadWatcher::new());
highlight_stream(
&mut reader,
&mut stdout,
&options,
interactive,
reload_watcher,
trace,
profile_input_rx,
)?;
let status = child.wait()?;
stop_resize.store(true, Ordering::Relaxed);
let _ = resize_thread.join();
drop(raw_mode);
Ok(ExitCode::from(status.exit_code() as u8))
}
fn parent_terminal_is_iterm() -> bool {
std::env::var_os("ITERM_SESSION_ID").is_some()
|| std::env::var_os("TERM_PROGRAM").as_deref() == Some(OsStr::new("iTerm.app"))
|| std::env::var_os("LC_TERMINAL").as_deref() == Some(OsStr::new("iTerm.app"))
}
fn apply_iterm_shell_integration_guard(
builder: &mut CommandBuilder,
interactive: bool,
iterm_parent: bool,
) {
if !interactive || !iterm_parent {
return;
}
for key in STRIPPED_ITERM_ENV {
if let Some(value) = builder
.get_env(key)
.map(OsString::from)
.or_else(|| std::env::var_os(key))
{
builder.env(format!("PRISMTTY_PARENT_{key}"), value);
}
builder.env_remove(key);
}
builder.env("ITERM_SHELL_INTEGRATION_INSTALLED", "prismtty");
builder.env("ITERM2_SQUELCH_MARK", "1");
builder.env("PRISMTTY_NESTED_ITERM", "1");
}
#[cfg(unix)]
fn configure_child_pty(master: &dyn portable_pty::MasterPty) -> Result<(), CliError> {
let stdin = io::stdin();
let source = tcgetattr(stdin.as_fd())?;
let Some(tty_name) = master.tty_name() else {
return Ok(());
};
let slave_tty = OpenOptions::new().read(true).write(true).open(tty_name)?;
let mut termios = source;
normalize_child_pty_termios(&mut termios);
tcsetattr(slave_tty.as_fd(), SetArg::TCSANOW, &termios)?;
Ok(())
}
#[cfg(not(unix))]
fn configure_child_pty(_master: &dyn portable_pty::MasterPty) -> Result<(), CliError> {
Ok(())
}
fn normalize_child_pty_termios(termios: &mut Termios) {
let (local, input, output) = normalize_child_pty_flags(
termios.local_flags,
termios.input_flags,
termios.output_flags,
);
termios.local_flags = local;
termios.input_flags = input;
termios.output_flags = output;
}
fn normalize_child_pty_flags(
mut local: LocalFlags,
mut input: InputFlags,
mut output: OutputFlags,
) -> (LocalFlags, InputFlags, OutputFlags) {
local.insert(
LocalFlags::ECHO
| LocalFlags::ECHOE
| LocalFlags::ECHOK
| LocalFlags::ICANON
| LocalFlags::ISIG
| LocalFlags::IEXTEN,
);
input.insert(InputFlags::ICRNL);
output.insert(OutputFlags::OPOST);
(local, input, output)
}
fn forward_stdin_to_pty<R: Read, W: Write>(
reader: &mut R,
writer: &mut W,
local_echo: bool,
trace: IoTrace,
profile_input: Option<mpsc::Sender<Vec<u8>>>,
) -> io::Result<()> {
let mut buffer = [0_u8; 1024];
loop {
let read = reader.read(&mut buffer)?;
if read == 0 {
return Ok(());
}
let input = &buffer[..read];
trace.log("IN", input);
if let Some(sender) = &profile_input {
let _ = sender.send(input.to_vec());
}
writer.write_all(input)?;
writer.flush()?;
if local_echo {
let echo = local_echo_bytes(input);
if !echo.is_empty() {
let mut stdout = io::stdout().lock();
stdout.write_all(&echo)?;
stdout.flush()?;
}
}
}
}
fn local_echo_bytes(input: &[u8]) -> Vec<u8> {
let mut output = Vec::new();
let mut idx = 0;
while idx < input.len() {
match input[idx] {
0x08 | 0x7f => output.extend_from_slice(b"\x08 \x08"),
b'\r' | b'\n' => output.extend_from_slice(b"\r\n"),
0x1b => {
if input.get(idx + 1) == Some(&b'[') {
idx += 2;
while idx < input.len() && !(0x40..=0x7e).contains(&input[idx]) {
idx += 1;
}
} else {
idx += 1;
}
}
byte if byte.is_ascii_control() => {}
byte => output.push(byte),
}
idx += 1;
}
output
}
struct RawModeGuard {
original: Termios,
}
impl RawModeGuard {
fn enable() -> Result<Self, CliError> {
let stdin = io::stdin();
let original = tcgetattr(stdin.as_fd())?;
let mut raw = original.clone();
cfmakeraw(&mut raw);
tcsetattr(stdin.as_fd(), SetArg::TCSANOW, &raw)?;
Ok(Self { original })
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
let stdin = io::stdin();
let _ = tcsetattr(stdin.as_fd(), SetArg::TCSANOW, &self.original);
}
}
fn current_pty_size() -> PtySize {
let stdout = io::stdout();
if let Some(size) = pty_size_from_fd(stdout.as_fd()) {
return size;
}
let stdin = io::stdin();
pty_size_from_fd(stdin.as_fd()).unwrap_or_default()
}
fn pty_size_from_fd(fd: BorrowedFd<'_>) -> Option<PtySize> {
let mut winsize: libc::winsize = unsafe { mem::zeroed() };
let result = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCGWINSZ, &mut winsize) };
if result != 0 || winsize.ws_row == 0 || winsize.ws_col == 0 {
return None;
}
Some(PtySize {
rows: winsize.ws_row,
cols: winsize.ws_col,
pixel_width: winsize.ws_xpixel,
pixel_height: winsize.ws_ypixel,
})
}
fn poll_pty_size(master: Box<dyn portable_pty::MasterPty + Send>, stop: Arc<AtomicBool>) {
let mut last_size = current_pty_size();
while !stop.load(Ordering::Relaxed) {
thread::sleep(Duration::from_millis(250));
let next_size = current_pty_size();
if next_size != last_size {
let _ = master.resize(next_size);
last_size = next_size;
}
}
}
#[cfg(test)]
mod tests {
use nix::sys::termios::{InputFlags, LocalFlags, OutputFlags};
#[test]
fn child_pty_flags_enable_echo_and_canonical_input() {
let local = LocalFlags::empty();
let input = InputFlags::empty();
let output = OutputFlags::empty();
let (local, input, output) = super::normalize_child_pty_flags(local, input, output);
assert!(local.contains(LocalFlags::ECHO));
assert!(local.contains(LocalFlags::ECHOE));
assert!(local.contains(LocalFlags::ECHOK));
assert!(local.contains(LocalFlags::ICANON));
assert!(local.contains(LocalFlags::ISIG));
assert!(local.contains(LocalFlags::IEXTEN));
assert!(input.contains(InputFlags::ICRNL));
assert!(output.contains(OutputFlags::OPOST));
}
#[test]
fn local_echo_bytes_echo_printable_enter_and_backspace() {
assert_eq!(
super::local_echo_bytes(b"show\x7f route\r\x1b[A"),
b"show\x08 \x08 route\r\n"
);
}
#[test]
fn iterm_shell_integration_guard_removes_nested_iterm_environment() {
let mut builder = portable_pty::CommandBuilder::new("/bin/zsh");
builder.env("TERM_PROGRAM", "iTerm.app");
builder.env("TERM_PROGRAM_VERSION", "3.6.0");
builder.env("LC_TERMINAL", "iTerm.app");
builder.env("LC_TERMINAL_VERSION", "3.6.0");
builder.env("ITERM_SESSION_ID", "w0t0p0");
builder.env("ITERM_PROFILE", "Default");
super::apply_iterm_shell_integration_guard(&mut builder, true, true);
for key in [
"TERM_PROGRAM",
"TERM_PROGRAM_VERSION",
"LC_TERMINAL",
"LC_TERMINAL_VERSION",
"ITERM_SESSION_ID",
"ITERM_PROFILE",
] {
assert!(builder.get_env(key).is_none(), "{key} should be removed");
}
for (key, value) in [
("TERM_PROGRAM", "iTerm.app"),
("TERM_PROGRAM_VERSION", "3.6.0"),
("LC_TERMINAL", "iTerm.app"),
("LC_TERMINAL_VERSION", "3.6.0"),
("ITERM_SESSION_ID", "w0t0p0"),
("ITERM_PROFILE", "Default"),
] {
let parent_key = format!("PRISMTTY_PARENT_{key}");
assert_eq!(
builder.get_env(&parent_key),
Some(std::ffi::OsStr::new(value)),
"{parent_key} should preserve {key}"
);
}
assert_eq!(
builder.get_env("ITERM2_SQUELCH_MARK"),
Some(std::ffi::OsStr::new("1"))
);
assert_eq!(
builder.get_env("ITERM_SHELL_INTEGRATION_INSTALLED"),
Some(std::ffi::OsStr::new("prismtty"))
);
assert_eq!(
builder.get_env("PRISMTTY_NESTED_ITERM"),
Some(std::ffi::OsStr::new("1"))
);
}
#[test]
fn iterm_shell_integration_guard_keeps_environment_for_non_iterm_or_noninteractive() {
let mut builder = portable_pty::CommandBuilder::new("/bin/zsh");
builder.env("TERM_PROGRAM", "iTerm.app");
builder.env("ITERM_SESSION_ID", "w0t0p0");
super::apply_iterm_shell_integration_guard(&mut builder, true, false);
assert_eq!(
builder.get_env("TERM_PROGRAM"),
Some(std::ffi::OsStr::new("iTerm.app"))
);
assert!(builder.get_env("PRISMTTY_PARENT_TERM_PROGRAM").is_none());
assert!(
builder
.get_env("PRISMTTY_PARENT_ITERM_SESSION_ID")
.is_none()
);
super::apply_iterm_shell_integration_guard(&mut builder, false, true);
assert_eq!(
builder.get_env("TERM_PROGRAM"),
Some(std::ffi::OsStr::new("iTerm.app"))
);
assert!(builder.get_env("PRISMTTY_PARENT_TERM_PROGRAM").is_none());
assert!(
builder
.get_env("PRISMTTY_PARENT_ITERM_SESSION_ID")
.is_none()
);
}
}