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