use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
use std::io::Write;
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use thiserror::Error;
use crate::core::types::CliTool;
#[derive(Error, Debug)]
pub enum PtyError {
#[error("Failed to create PTY: {0}")]
CreateFailed(String),
#[error("Failed to spawn process: {0}")]
SpawnFailed(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("PTY error: {0}")]
Pty(String),
}
pub struct PtyWrapper {
master: Box<dyn MasterPty + Send>,
child: Box<dyn Child + Send + Sync>,
writer: Box<dyn Write + Send>,
output_rx: Receiver<Vec<u8>>,
tool: CliTool,
}
impl PtyWrapper {
pub fn new(
tool: CliTool,
working_dir: &std::path::Path,
rows: u16,
cols: u16,
) -> Result<Self, PtyError> {
Self::new_with_env(tool, working_dir, &[], rows, cols)
}
pub fn new_compact(tool: CliTool, working_dir: &std::path::Path) -> Result<Self, PtyError> {
Self::new(tool, working_dir, 24, 80)
}
pub fn new_with_env(
tool: CliTool,
working_dir: &std::path::Path,
env_vars: &[(String, String)],
rows: u16,
cols: u16,
) -> Result<Self, PtyError> {
let pty_system = native_pty_system();
let pair = pty_system
.openpty(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.map_err(|e| PtyError::CreateFailed(e.to_string()))?;
let cmd = Self::build_command(tool, working_dir, env_vars);
let child = pair
.slave
.spawn_command(cmd)
.map_err(|e| PtyError::SpawnFailed(e.to_string()))?;
let reader = pair
.master
.try_clone_reader()
.map_err(|e| PtyError::Pty(e.to_string()))?;
let writer = pair
.master
.take_writer()
.map_err(|e| PtyError::Pty(e.to_string()))?;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
Self::reader_thread(reader, tx);
});
Ok(Self {
master: pair.master,
child,
writer,
output_rx: rx,
tool,
})
}
fn build_command(
tool: CliTool,
working_dir: &std::path::Path,
env_vars: &[(String, String)],
) -> CommandBuilder {
let mut cmd = if cfg!(windows) {
let tool_name = match tool {
CliTool::ClaudeCode => "claude",
CliTool::Codex => "codex",
CliTool::Gemini => "gemini",
CliTool::Cursor => "cursor-agent",
CliTool::OpenCode => "opencode",
};
let mut c = CommandBuilder::new("cmd");
c.args(["/Q", "/K", tool_name]);
c
} else {
match tool {
CliTool::ClaudeCode => CommandBuilder::new("claude"),
CliTool::Codex => CommandBuilder::new("codex"),
CliTool::Gemini => CommandBuilder::new("gemini"),
CliTool::Cursor => CommandBuilder::new("cursor-agent"),
CliTool::OpenCode => CommandBuilder::new("opencode"),
}
};
cmd.cwd(working_dir);
for (key, value) in env_vars {
cmd.env(key, value);
}
cmd
}
fn reader_thread(reader: Box<dyn std::io::Read + Send>, tx: Sender<Vec<u8>>) {
use std::io::Read;
let mut reader = reader;
let mut buffer = [0u8; 4096];
loop {
match reader.read(&mut buffer) {
Ok(0) => break, Ok(n) => {
if tx.send(buffer[..n].to_vec()).is_err() {
break;
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(std::time::Duration::from_millis(10));
}
Err(_) => break,
}
}
}
pub fn write(&mut self, data: &str) -> Result<(), PtyError> {
self.writer.write_all(data.as_bytes())?;
self.writer.flush()?;
Ok(())
}
pub fn write_bytes(&mut self, data: &[u8]) -> Result<(), PtyError> {
self.writer.write_all(data)?;
self.writer.flush()?;
Ok(())
}
pub fn writeln(&mut self, data: &str) -> Result<(), PtyError> {
self.write(&format!("{}\n", data))
}
pub fn try_recv(&self) -> Option<Vec<u8>> {
self.output_rx.try_recv().ok()
}
pub fn resize(&self, rows: u16, cols: u16) -> Result<(), PtyError> {
self.master
.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.map_err(|e| PtyError::Pty(e.to_string()))
}
pub fn is_running(&mut self) -> bool {
self.child.try_wait().ok().flatten().is_none()
}
pub fn kill(&mut self) -> Result<(), PtyError> {
self.child.kill().map_err(|e| PtyError::Pty(e.to_string()))
}
pub fn wait(&mut self) -> Option<u32> {
self.child.wait().ok().map(|s| s.exit_code())
}
pub fn tool(&self) -> CliTool {
self.tool
}
}