Skip to main content

tui_test/terminal/
pty.rs

1//! PTY spawning and control via `portable-pty`.
2
3use std::io::{Read, Write};
4
5use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
6
7use crate::shell::Launch;
8
9pub struct Pty {
10    master: Option<Box<dyn MasterPty + Send>>,
11    writer: Option<Box<dyn Write + Send>>,
12    child: Box<dyn Child + Send + Sync>,
13}
14
15pub struct SpawnOptions {
16    pub cols: u16,
17    pub rows: u16,
18    pub cwd: Option<String>,
19    pub env: Vec<(String, String)>,
20}
21
22impl Pty {
23    /// Spawn a program in a fresh PTY, returning the controller and a reader
24    /// for its output.
25    pub fn spawn(
26        target: &str,
27        args: &[String],
28        opts: &SpawnOptions,
29    ) -> anyhow::Result<(Pty, Box<dyn Read + Send>)> {
30        let pty_system = native_pty_system();
31        let pair = pty_system.openpty(PtySize {
32            rows: opts.rows,
33            cols: opts.cols,
34            pixel_width: 0,
35            pixel_height: 0,
36        })?;
37
38        let mut cmd = CommandBuilder::new(target);
39        for arg in args {
40            cmd.arg(arg);
41        }
42        for (k, v) in std::env::vars() {
43            cmd.env(k, v);
44        }
45        cmd.env("TERM", "xterm-256color");
46        for (k, v) in &opts.env {
47            cmd.env(k, v);
48        }
49        if let Some(cwd) = &opts.cwd {
50            cmd.cwd(cwd);
51        } else if let Ok(cwd) = std::env::current_dir() {
52            cmd.cwd(cwd);
53        }
54
55        let child = pair.slave.spawn_command(cmd)?;
56        drop(pair.slave);
57
58        let reader = pair.master.try_clone_reader()?;
59        let writer = pair.master.take_writer()?;
60
61        Ok((
62            Pty {
63                master: Some(pair.master),
64                writer: Some(writer),
65                child,
66            },
67            reader,
68        ))
69    }
70
71    /// Spawn a shell using its computed launch configuration.
72    pub fn spawn_launch(
73        launch: &Launch,
74        cols: u16,
75        rows: u16,
76        cwd: Option<String>,
77    ) -> anyhow::Result<(Pty, Box<dyn Read + Send>)> {
78        let opts = SpawnOptions {
79            cols,
80            rows,
81            cwd,
82            env: launch.env.clone(),
83        };
84        Pty::spawn(&launch.target, &launch.args, &opts)
85    }
86
87    pub fn write(&mut self, data: &[u8]) -> std::io::Result<()> {
88        let writer = self
89            .writer
90            .as_mut()
91            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "PTY is closed"))?;
92        writer.write_all(data)?;
93        writer.flush()
94    }
95
96    pub fn resize(&mut self, cols: u16, rows: u16) -> anyhow::Result<()> {
97        let master = self
98            .master
99            .as_ref()
100            .ok_or_else(|| anyhow::anyhow!("PTY is closed"))?;
101        master.resize(PtySize {
102            rows,
103            cols,
104            pixel_width: 0,
105            pixel_height: 0,
106        })?;
107        Ok(())
108    }
109
110    pub fn pid(&self) -> Option<u32> {
111        self.child.process_id()
112    }
113
114    pub fn kill(&mut self) {
115        let _ = self.child.kill();
116    }
117
118    pub fn close(&mut self) {
119        self.kill();
120        self.writer.take();
121        self.master.take();
122    }
123
124    /// Send a named signal. Cross-platform support is limited: INT delivers a
125    /// Ctrl-C to the foreground app; TERM/KILL terminate the child.
126    pub fn signal(&mut self, name: &str) -> anyhow::Result<()> {
127        let upper = name.trim_start_matches("SIG").to_uppercase();
128        match upper.as_str() {
129            "INT" => self.write(b"\x03")?,
130            "TERM" | "KILL" | "QUIT" => self.kill(),
131            other => anyhow::bail!("unsupported signal: {other}"),
132        }
133        Ok(())
134    }
135
136    /// Return the exit code if the child has exited.
137    pub fn try_wait(&mut self) -> std::io::Result<Option<i32>> {
138        self.child
139            .try_wait()
140            .map(|status| status.map(|status| status.exit_code() as i32))
141    }
142}