1use crate::sync_utils::mutex_lock_or_recover;
7use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
8use std::io::{Read, Write};
9use std::os::fd::RawFd;
10use std::sync::{Arc, Mutex};
11use thiserror::Error;
12
13#[derive(Error, Debug)]
14pub enum PtyError {
15 #[error("Failed to open PTY: {0}")]
16 OpenFailed(String),
17 #[error("Failed to spawn process: {0}")]
18 SpawnFailed(String),
19 #[error("Failed to write to PTY: {0}")]
20 WriteFailed(String),
21 #[error("Failed to read from PTY: {0}")]
22 ReadFailed(String),
23 #[error("Failed to resize PTY: {0}")]
24 ResizeFailed(String),
25 #[error("Process not running")]
26 NotRunning,
27}
28
29pub struct PtyHandle {
30 master: Box<dyn MasterPty + Send>,
31 child: Box<dyn Child + Send + Sync>,
32 reader: Arc<Mutex<Box<dyn Read + Send>>>,
33 writer: Arc<Mutex<Box<dyn Write + Send>>>,
34 size: PtySize,
35 reader_fd: RawFd,
36}
37
38impl PtyHandle {
39 pub fn spawn(
41 command: &str,
42 args: &[String],
43 cwd: Option<&str>,
44 env: Option<&std::collections::HashMap<String, String>>,
45 cols: u16,
46 rows: u16,
47 ) -> Result<Self, PtyError> {
48 let pty_system = native_pty_system();
49
50 let size = PtySize {
51 rows,
52 cols,
53 pixel_width: 0,
54 pixel_height: 0,
55 };
56
57 let pair = pty_system
58 .openpty(size)
59 .map_err(|e| PtyError::OpenFailed(e.to_string()))?;
60
61 let mut cmd = CommandBuilder::new(command);
62 cmd.args(args);
63
64 if let Some(dir) = cwd {
65 cmd.cwd(dir);
66 }
67
68 if let Some(env_vars) = env {
69 for (key, value) in env_vars {
70 cmd.env(key, value);
71 }
72 }
73
74 cmd.env("TERM", "xterm-256color");
76
77 let child = pair
78 .slave
79 .spawn_command(cmd)
80 .map_err(|e| PtyError::SpawnFailed(e.to_string()))?;
81
82 let reader = pair
83 .master
84 .try_clone_reader()
85 .map_err(|e| PtyError::OpenFailed(e.to_string()))?;
86
87 let reader_fd = pair
89 .master
90 .as_raw_fd()
91 .ok_or_else(|| PtyError::OpenFailed("Failed to get master fd".to_string()))?;
92
93 let writer = pair
94 .master
95 .take_writer()
96 .map_err(|e| PtyError::OpenFailed(e.to_string()))?;
97
98 Ok(Self {
99 master: pair.master,
100 child,
101 reader: Arc::new(Mutex::new(reader)),
102 writer: Arc::new(Mutex::new(writer)),
103 size,
104 reader_fd,
105 })
106 }
107
108 pub fn pid(&self) -> Option<u32> {
110 self.child.process_id()
111 }
112
113 pub fn is_running(&mut self) -> bool {
115 self.child
116 .try_wait()
117 .map(|status| status.is_none())
118 .unwrap_or(false)
119 }
120
121 pub fn write(&self, data: &[u8]) -> Result<(), PtyError> {
123 let mut writer = mutex_lock_or_recover(&self.writer);
124 writer
125 .write_all(data)
126 .map_err(|e| PtyError::WriteFailed(e.to_string()))?;
127 writer
128 .flush()
129 .map_err(|e| PtyError::WriteFailed(e.to_string()))?;
130 Ok(())
131 }
132
133 pub fn write_str(&self, s: &str) -> Result<(), PtyError> {
135 self.write(s.as_bytes())
136 }
137
138 pub fn read(&self, buf: &mut [u8]) -> Result<usize, PtyError> {
140 let mut reader = mutex_lock_or_recover(&self.reader);
141 reader
142 .read(buf)
143 .map_err(|e| PtyError::ReadFailed(e.to_string()))
144 }
145
146 pub fn try_read(&self, buf: &mut [u8], timeout_ms: i32) -> Result<usize, PtyError> {
149 let mut pollfd = libc::pollfd {
151 fd: self.reader_fd,
152 events: libc::POLLIN,
153 revents: 0,
154 };
155
156 let result = unsafe { libc::poll(&mut pollfd, 1, timeout_ms) };
157
158 if result < 0 {
159 return Err(PtyError::ReadFailed("poll failed".to_string()));
160 }
161
162 if result == 0 {
163 return Ok(0);
165 }
166
167 let mut reader = mutex_lock_or_recover(&self.reader);
169 reader
170 .read(buf)
171 .map_err(|e| PtyError::ReadFailed(e.to_string()))
172 }
173
174 pub fn resize(&mut self, cols: u16, rows: u16) -> Result<(), PtyError> {
176 self.size = PtySize {
177 rows,
178 cols,
179 pixel_width: 0,
180 pixel_height: 0,
181 };
182 self.master
183 .resize(self.size)
184 .map_err(|e| PtyError::ResizeFailed(e.to_string()))
185 }
186
187 pub fn size(&self) -> (u16, u16) {
189 (self.size.cols, self.size.rows)
190 }
191
192 pub fn kill(&mut self) -> Result<(), PtyError> {
194 self.child
195 .kill()
196 .map_err(|e| PtyError::SpawnFailed(e.to_string()))
197 }
198
199 pub fn wait(&mut self) -> Result<Option<portable_pty::ExitStatus>, PtyError> {
201 self.child
202 .try_wait()
203 .map_err(|e| PtyError::SpawnFailed(e.to_string()))
204 }
205
206 pub fn reader(&self) -> Arc<Mutex<Box<dyn Read + Send>>> {
208 Arc::clone(&self.reader)
209 }
210
211 pub fn reader_fd(&self) -> RawFd {
213 self.reader_fd
214 }
215}
216
217pub fn key_to_escape_sequence(key: &str) -> Option<Vec<u8>> {
219 if key.contains('+') {
221 let parts: Vec<&str> = key.split('+').collect();
222 if parts.len() == 2 {
223 let modifier = parts[0];
224 let base_key = parts[1];
225
226 return match modifier.to_lowercase().as_str() {
227 "ctrl" | "control" => {
228 if base_key.len() == 1 {
230 let c = base_key.chars().next()?.to_ascii_uppercase();
231 if c.is_ascii_alphabetic() {
232 return Some(vec![(c as u8) - b'A' + 1]);
233 }
234 }
235 match base_key.to_lowercase().as_str() {
237 "c" => Some(vec![3]), "d" => Some(vec![4]), "z" => Some(vec![26]), "\\" => Some(vec![28]), "[" => Some(vec![27]), _ => None,
243 }
244 }
245 "alt" | "meta" => {
246 let base = key_to_escape_sequence(base_key)?;
248 let mut result = vec![0x1b]; result.extend(base);
250 Some(result)
251 }
252 "shift" => {
253 match base_key.to_lowercase().as_str() {
255 "tab" => Some(vec![0x1b, b'[', b'Z']), _ => {
257 if base_key.len() == 1 {
259 Some(base_key.to_uppercase().as_bytes().to_vec())
260 } else {
261 None
262 }
263 }
264 }
265 }
266 _ => None,
267 };
268 }
269 }
270
271 match key {
273 "Enter" | "Return" => Some(vec![b'\r']),
275 "Tab" => Some(vec![b'\t']),
276 "Escape" | "Esc" => Some(vec![0x1b]),
277 "Backspace" => Some(vec![0x7f]),
278 "Delete" => Some(vec![0x1b, b'[', b'3', b'~']),
279 "Space" => Some(vec![b' ']),
280
281 "ArrowUp" | "Up" => Some(vec![0x1b, b'[', b'A']),
283 "ArrowDown" | "Down" => Some(vec![0x1b, b'[', b'B']),
284 "ArrowRight" | "Right" => Some(vec![0x1b, b'[', b'C']),
285 "ArrowLeft" | "Left" => Some(vec![0x1b, b'[', b'D']),
286
287 "Home" => Some(vec![0x1b, b'[', b'H']),
289 "End" => Some(vec![0x1b, b'[', b'F']),
290 "PageUp" => Some(vec![0x1b, b'[', b'5', b'~']),
291 "PageDown" => Some(vec![0x1b, b'[', b'6', b'~']),
292 "Insert" => Some(vec![0x1b, b'[', b'2', b'~']),
293
294 "F1" => Some(vec![0x1b, b'O', b'P']),
296 "F2" => Some(vec![0x1b, b'O', b'Q']),
297 "F3" => Some(vec![0x1b, b'O', b'R']),
298 "F4" => Some(vec![0x1b, b'O', b'S']),
299 "F5" => Some(vec![0x1b, b'[', b'1', b'5', b'~']),
300 "F6" => Some(vec![0x1b, b'[', b'1', b'7', b'~']),
301 "F7" => Some(vec![0x1b, b'[', b'1', b'8', b'~']),
302 "F8" => Some(vec![0x1b, b'[', b'1', b'9', b'~']),
303 "F9" => Some(vec![0x1b, b'[', b'2', b'0', b'~']),
304 "F10" => Some(vec![0x1b, b'[', b'2', b'1', b'~']),
305 "F11" => Some(vec![0x1b, b'[', b'2', b'3', b'~']),
306 "F12" => Some(vec![0x1b, b'[', b'2', b'4', b'~']),
307
308 _ if key.len() == 1 => Some(key.as_bytes().to_vec()),
310
311 _ => None,
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_key_to_escape_sequence() {
321 assert_eq!(key_to_escape_sequence("Enter"), Some(vec![b'\r']));
322 assert_eq!(key_to_escape_sequence("Tab"), Some(vec![b'\t']));
323 assert_eq!(key_to_escape_sequence("Escape"), Some(vec![0x1b]));
324 assert_eq!(
325 key_to_escape_sequence("ArrowUp"),
326 Some(vec![0x1b, b'[', b'A'])
327 );
328 assert_eq!(key_to_escape_sequence("Ctrl+C"), Some(vec![3]));
329 assert_eq!(key_to_escape_sequence("a"), Some(vec![b'a']));
330 }
331}