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}