Skip to main content

sqry_cli/output/
stream.rs

1//! Output stream abstraction for stdout/stderr separation
2
3use super::pager::{BufferedOutput, PagerConfig, PagerExitStatus};
4use std::io::{self, Write};
5
6/// Internal stdout backend for `OutputStreams`
7enum StdoutBackend {
8    /// Direct stdout (no paging)
9    Direct(Box<dyn Write + Send>),
10    /// Pager-backed output (may auto-page)
11    Pager(BufferedOutput),
12}
13
14/// Manages stdout and stderr streams
15///
16/// Supports optional pager integration for large output. When created with
17/// `with_pager()`, stdout writes are buffered and may be piped through
18/// a pager (like `less`) if output exceeds terminal height.
19pub struct OutputStreams {
20    stdout: StdoutBackend,
21    stderr: Box<dyn Write + Send>,
22}
23
24impl OutputStreams {
25    /// Create streams using actual stdout/stderr (no paging)
26    #[must_use]
27    pub fn new() -> Self {
28        Self {
29            stdout: StdoutBackend::Direct(Box::new(io::stdout())),
30            stderr: Box::new(io::stderr()),
31        }
32    }
33
34    /// Create streams with pager support
35    ///
36    /// When pager is enabled, stdout output is buffered and may be
37    /// piped through a pager (like `less`) if output exceeds terminal height.
38    ///
39    /// Call `finish()` at the end to properly handle pager lifecycle.
40    #[must_use]
41    pub fn with_pager(config: PagerConfig) -> Self {
42        Self {
43            stdout: StdoutBackend::Pager(BufferedOutput::new(config)),
44            stderr: Box::new(io::stderr()),
45        }
46    }
47
48    /// Create streams with custom writers (for testing)
49    #[cfg(test)]
50    #[allow(dead_code)] // API for future tests
51    pub fn with_writers<W1, W2>(stdout: W1, stderr: W2) -> Self
52    where
53        W1: Write + Send + 'static,
54        W2: Write + Send + 'static,
55    {
56        Self {
57            stdout: StdoutBackend::Direct(Box::new(stdout)),
58            stderr: Box::new(stderr),
59        }
60    }
61
62    /// Write results to stdout (data stream)
63    ///
64    /// # Errors
65    /// Returns an error if writing to stdout fails.
66    pub fn write_result(&mut self, content: &str) -> io::Result<()> {
67        match &mut self.stdout {
68            StdoutBackend::Direct(writer) => writeln!(writer, "{content}"),
69            StdoutBackend::Pager(buffer) => {
70                buffer.write(content)?;
71                buffer.write("\n")
72            }
73        }
74    }
75
76    /// Write diagnostic to stderr (diagnostic stream)
77    ///
78    /// # Errors
79    /// Returns an error if writing to stderr fails.
80    pub fn write_diagnostic(&mut self, content: &str) -> io::Result<()> {
81        writeln!(self.stderr, "{content}")
82    }
83
84    /// Flush stderr (for --explain to avoid interleaving)
85    #[allow(dead_code)]
86    ///
87    /// # Errors
88    /// Returns an error if flushing stderr fails.
89    pub fn flush_stderr(&mut self) -> io::Result<()> {
90        self.stderr.flush()
91    }
92
93    /// Finalize output, flushing buffer and waiting for pager if applicable
94    ///
95    /// Returns the pager exit status. For non-pager streams, returns `Success`.
96    /// Call this at the end of command execution to properly handle pager lifecycle.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if flushing or waiting for pager fails.
101    pub fn finish(self) -> io::Result<PagerExitStatus> {
102        match self.stdout {
103            StdoutBackend::Direct(_) => Ok(PagerExitStatus::Success),
104            StdoutBackend::Pager(buffer) => buffer.finish(),
105        }
106    }
107
108    /// Finalize output and check for pager exit status
109    ///
110    /// This is a convenience method that combines `finish()` with pager exit code
111    /// checking. If the pager exited with a non-zero code, returns a `CliError::PagerExit`.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if:
116    /// - Flushing or waiting for pager fails (IO error)
117    /// - Pager exited with non-zero code (`CliError::PagerExit`)
118    pub fn finish_checked(self) -> anyhow::Result<()> {
119        let status = self.finish()?;
120        if let Some(code) = status.exit_code() {
121            return Err(crate::error::CliError::pager_exit(code).into());
122        }
123        Ok(())
124    }
125}
126
127impl Default for OutputStreams {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133/// Test-friendly streams that capture output to strings
134#[cfg(test)]
135pub struct TestOutputStreams {
136    pub stdout: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
137    pub stderr: std::sync::Arc<std::sync::Mutex<Vec<u8>>>,
138}
139
140#[cfg(test)]
141impl TestOutputStreams {
142    pub fn new() -> (Self, OutputStreams) {
143        let stdout = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
144        let stderr = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
145
146        let test = Self {
147            stdout: std::sync::Arc::clone(&stdout),
148            stderr: std::sync::Arc::clone(&stderr),
149        };
150
151        let streams = OutputStreams {
152            stdout: StdoutBackend::Direct(Box::new(SharedWriter(stdout))),
153            stderr: Box::new(SharedWriter(stderr)),
154        };
155
156        (test, streams)
157    }
158
159    pub fn stdout_string(&self) -> String {
160        let guard = self.stdout.lock().unwrap();
161        String::from_utf8_lossy(&guard).to_string()
162    }
163
164    pub fn stderr_string(&self) -> String {
165        let guard = self.stderr.lock().unwrap();
166        String::from_utf8_lossy(&guard).to_string()
167    }
168}
169
170#[cfg(test)]
171struct SharedWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
172
173#[cfg(test)]
174impl Write for SharedWriter {
175    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
176        let mut guard = self.0.lock().unwrap();
177        guard.extend_from_slice(buf);
178        Ok(buf.len())
179    }
180
181    fn flush(&mut self) -> io::Result<()> {
182        Ok(())
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_output_streams_creation() {
192        let _streams = OutputStreams::new();
193        // Just verify it can be created
194    }
195
196    #[test]
197    fn test_default() {
198        let _streams = OutputStreams::default();
199    }
200
201    #[test]
202    fn test_output_streams_capture() {
203        let (test, mut streams) = TestOutputStreams::new();
204
205        streams.write_result("hello").unwrap();
206        streams.write_diagnostic("world").unwrap();
207
208        assert_eq!(test.stdout_string(), "hello\n");
209        assert_eq!(test.stderr_string(), "world\n");
210    }
211
212    #[test]
213    fn test_finish_non_pager_returns_success() {
214        let streams = OutputStreams::new();
215        let status = streams.finish().unwrap();
216        assert!(status.is_success());
217    }
218}