Skip to main content

rskit_process/pty/
config.rs

1//! PTY-backed I/O mode configuration.
2
3use crate::command::{InputPolicy, OutputPolicy};
4use crate::runner::OutputObserver;
5
6use super::size::PtySize;
7
8/// Pseudoterminal-backed I/O mode.
9///
10/// The child runs attached to a real controlling terminal, so it renders output
11/// exactly as it would in an interactive shell — colors, progress bars, and
12/// tty-gated formatting are preserved. Unlike the pipe-backed modes, a PTY has a
13/// single stream: the child's stdout and stderr are merged in emission order.
14///
15/// The merged stream is delivered through the observer's **stdout** callbacks
16/// and, when the output policy captures either stdout or stderr, retained as the
17/// result's stdout (the two streams are merged, so either capture flag opts into
18/// keeping the combined output). The child never writes to the result's stderr
19/// in this mode; the only bytes that can appear there are synthetic termination
20/// diagnostics injected by the lifecycle layer (for example a note that the
21/// child was killed after a timeout or cancellation).
22#[derive(Clone, Default)]
23pub struct PtyIo {
24    /// Standard input policy. Only [`InputPolicy::Closed`] and
25    /// [`InputPolicy::Bytes`] are supported; inherited stdin is rejected.
26    ///
27    /// Terminal stdin cannot be half-closed, so [`InputPolicy::Closed`] does not
28    /// deliver a pipe-style EOF here: it simply means no bytes are ever written
29    /// to the child's terminal (the child keeps reading from a live tty that
30    /// stays open until the PTY is torn down), not that the child sees stdin
31    /// closed. Likewise [`InputPolicy::Bytes`] are delivered as terminal input
32    /// (as if typed) through the PTY's line discipline, so a reader returns on a
33    /// newline rather than on the writer closing.
34    pub input: InputPolicy,
35    /// Capture policy for the merged output stream. Either `capture_stdout` or
36    /// `capture_stderr` retains the combined stream (surfaced as the result's
37    /// stdout); `max_output_bytes` bounds it.
38    pub output: OutputPolicy,
39    /// Observer callbacks for the merged output stream (delivered via stdout).
40    pub observer: OutputObserver,
41    /// Window size advertised to the child through the PTY.
42    pub size: PtySize,
43}
44
45impl std::fmt::Debug for PtyIo {
46    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        formatter
48            .debug_struct("PtyIo")
49            .field("input", &self.input)
50            .field("output", &self.output)
51            .field("observer", &self.observer)
52            .field("size", &self.size)
53            .finish()
54    }
55}
56
57impl PtyIo {
58    /// Create a PTY mode that observes the merged stream through `observer`.
59    #[must_use]
60    pub fn new(observer: OutputObserver) -> Self {
61        Self {
62            observer,
63            ..Self::default()
64        }
65    }
66
67    /// Set the stdin policy.
68    #[must_use]
69    pub fn with_input(mut self, input: InputPolicy) -> Self {
70        self.input = input;
71        self
72    }
73
74    /// Set the capture policy for the merged output stream.
75    #[must_use]
76    pub fn with_output(mut self, output: OutputPolicy) -> Self {
77        self.output = output;
78        self
79    }
80
81    /// Set the window size advertised to the child.
82    #[must_use]
83    pub fn with_size(mut self, size: PtySize) -> Self {
84        self.size = size;
85        self
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn builders_update_fields() {
95        let io = PtyIo::new(OutputObserver::new())
96            .with_input(InputPolicy::Bytes(b"y\n".to_vec()))
97            .with_output(OutputPolicy::observe_only())
98            .with_size(PtySize::new(50, 200));
99        assert_eq!(io.input, InputPolicy::Bytes(b"y\n".to_vec()));
100        assert!(!io.output.capture_stdout);
101        assert_eq!(io.size, PtySize::new(50, 200));
102    }
103
104    #[test]
105    fn debug_reports_observer_presence() {
106        let io = PtyIo::new(OutputObserver::new().with_stdout_bytes(|_| {}));
107        let rendered = format!("{io:?}");
108        assert!(rendered.contains("PtyIo"));
109        assert!(rendered.contains("stdout_bytes: true"));
110    }
111}