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}