Skip to main content

rich_rs/
file_proxy.rs

1//! FileProxy: Redirect writes to a Console.
2//!
3//! Port of Python Rich's `rich/file_proxy.py`.
4//!
5//! FileProxy wraps a writer (e.g., stdout) and redirects writes to a Console,
6//! using AnsiDecoder to parse ANSI sequences from input. It implements line
7//! buffering - accumulating input until a newline, then printing via Console.
8
9use std::io::{self, Stdout, Write};
10
11use crate::ansi::AnsiDecoder;
12use crate::text::Text;
13use crate::{Console, ConsoleOptions};
14
15/// Wraps a writer (e.g., stdout) and redirects writes to a Console.
16///
17/// FileProxy buffers input until a newline is encountered, then decodes
18/// ANSI escape sequences and prints the result via the Console.
19///
20/// # Type Parameters
21///
22/// * `C` - The writer type for the Console (e.g., `Stdout` or `Vec<u8>`).
23/// * `W` - The inner writer type to wrap.
24///
25/// # Example
26///
27/// ```no_run
28/// use rich_rs::{Console, ConsoleOptions};
29/// use rich_rs::file_proxy::FileProxy;
30/// use std::io::Write;
31///
32/// let console = Console::new();
33/// let mut proxy = FileProxy::new(console, std::io::stdout());
34///
35/// // Writes are buffered until newline
36/// write!(proxy, "Hello, ").unwrap();
37/// writeln!(proxy, "World!").unwrap();  // Prints "Hello, World!" via Console
38/// ```
39pub struct FileProxy<C: Write, W: Write> {
40    /// The Console to redirect output to.
41    console: Console<C>,
42    /// The inner writer (for passthrough operations like fileno).
43    inner: W,
44    /// Line buffer - accumulates text until newline.
45    buffer: String,
46    /// ANSI decoder for parsing escape sequences.
47    decoder: AnsiDecoder,
48}
49
50impl<W: Write> FileProxy<Stdout, W> {
51    /// Create a new FileProxy with a stdout Console.
52    ///
53    /// # Arguments
54    ///
55    /// * `console` - The Console to redirect output to.
56    /// * `inner` - The inner writer to wrap.
57    pub fn new(console: Console<Stdout>, inner: W) -> Self {
58        Self {
59            console,
60            inner,
61            buffer: String::new(),
62            decoder: AnsiDecoder::new(),
63        }
64    }
65
66    /// Create a new FileProxy with custom console options.
67    pub fn with_options(options: ConsoleOptions, inner: W) -> Self {
68        Self {
69            console: Console::with_options(options),
70            inner,
71            buffer: String::new(),
72            decoder: AnsiDecoder::new(),
73        }
74    }
75}
76
77impl<C: Write, W: Write> FileProxy<C, W> {
78    /// Create a new FileProxy with a generic Console.
79    ///
80    /// # Arguments
81    ///
82    /// * `console` - The Console to redirect output to.
83    /// * `inner` - The inner writer to wrap.
84    pub fn with_console(console: Console<C>, inner: W) -> Self {
85        Self {
86            console,
87            inner,
88            buffer: String::new(),
89            decoder: AnsiDecoder::new(),
90        }
91    }
92
93    /// Get a reference to the inner writer.
94    pub fn inner(&self) -> &W {
95        &self.inner
96    }
97
98    /// Get a mutable reference to the inner writer.
99    pub fn inner_mut(&mut self) -> &mut W {
100        &mut self.inner
101    }
102
103    /// Get a reference to the console.
104    pub fn console(&self) -> &Console<C> {
105        &self.console
106    }
107
108    /// Get a mutable reference to the console.
109    pub fn console_mut(&mut self) -> &mut Console<C> {
110        &mut self.console
111    }
112
113    /// Consume the FileProxy and return the inner writer.
114    pub fn into_inner(self) -> W {
115        self.inner
116    }
117
118    /// Process buffered content and print complete lines.
119    fn process_text(&mut self, text: &str) -> io::Result<()> {
120        let mut remaining = text;
121        let mut lines: Vec<String> = Vec::new();
122
123        while !remaining.is_empty() {
124            if let Some(newline_pos) = remaining.find('\n') {
125                // Found a newline - complete the current line
126                let line_part = &remaining[..newline_pos];
127                let complete_line = if self.buffer.is_empty() {
128                    line_part.to_string()
129                } else {
130                    let mut line = std::mem::take(&mut self.buffer);
131                    line.push_str(line_part);
132                    line
133                };
134                lines.push(complete_line);
135                remaining = &remaining[newline_pos + 1..];
136            } else {
137                // No newline - buffer the remaining text
138                self.buffer.push_str(remaining);
139                break;
140            }
141        }
142
143        // Print complete lines via Console
144        if !lines.is_empty() {
145            // Decode ANSI sequences and join with newlines
146            let decoded_texts: Vec<Text> = lines
147                .iter()
148                .map(|line| self.decoder.decode_line(line))
149                .collect();
150
151            // Join texts with newlines
152            let mut output = Text::new();
153            for (i, text) in decoded_texts.into_iter().enumerate() {
154                if i > 0 {
155                    output.append("\n".to_string(), None);
156                }
157                output.append_text(&text);
158            }
159
160            self.console
161                .print(&output, None, None, None, false, "\n")
162                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
163        }
164
165        Ok(())
166    }
167}
168
169impl<C: Write, W: Write> Write for FileProxy<C, W> {
170    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
171        // Convert bytes to string (lossy for non-UTF8)
172        let text = String::from_utf8_lossy(buf);
173        self.process_text(&text)?;
174        Ok(buf.len())
175    }
176
177    fn flush(&mut self) -> io::Result<()> {
178        // Flush any remaining buffered content
179        if !self.buffer.is_empty() {
180            let buffered = std::mem::take(&mut self.buffer);
181            let decoded = self.decoder.decode_line(&buffered);
182            self.console
183                .print(&decoded, None, None, None, false, "\n")
184                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
185        }
186        Ok(())
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::io::Write;
194
195    #[test]
196    fn test_file_proxy_basic_write() {
197        let console = Console::capture();
198        let inner = Vec::<u8>::new();
199        let mut proxy = FileProxy::with_console(console, inner);
200
201        writeln!(proxy, "Hello, World!").unwrap();
202        proxy.flush().unwrap();
203
204        // The output goes to console, not inner
205        let console_output = proxy.console().get_captured();
206        assert!(console_output.contains("Hello"));
207        assert!(console_output.contains("World"));
208    }
209
210    #[test]
211    fn test_file_proxy_line_buffering() {
212        let console = Console::capture();
213        let inner = Vec::<u8>::new();
214        let mut proxy = FileProxy::with_console(console, inner);
215
216        // Write without newline - should buffer
217        write!(proxy, "Hello, ").unwrap();
218        assert!(proxy.console().get_captured().is_empty());
219
220        // Write with newline - should flush buffer
221        writeln!(proxy, "World!").unwrap();
222        let output = proxy.console().get_captured();
223        assert!(output.contains("Hello"));
224        assert!(output.contains("World"));
225    }
226
227    #[test]
228    fn test_file_proxy_ansi_decoding() {
229        let console = Console::capture();
230        let inner = Vec::<u8>::new();
231        let mut proxy = FileProxy::with_console(console, inner);
232
233        // Write text with ANSI bold
234        writeln!(proxy, "\x1b[1mBold\x1b[0m Normal").unwrap();
235
236        let output = proxy.console().get_captured();
237        assert!(output.contains("Bold"));
238        assert!(output.contains("Normal"));
239    }
240
241    #[test]
242    fn test_file_proxy_multiple_lines() {
243        let console = Console::capture();
244        let inner = Vec::<u8>::new();
245        let mut proxy = FileProxy::with_console(console, inner);
246
247        writeln!(proxy, "Line 1").unwrap();
248        writeln!(proxy, "Line 2").unwrap();
249        writeln!(proxy, "Line 3").unwrap();
250
251        let output = proxy.console().get_captured();
252        assert!(output.contains("Line 1"));
253        assert!(output.contains("Line 2"));
254        assert!(output.contains("Line 3"));
255    }
256
257    #[test]
258    fn test_file_proxy_flush_partial_line() {
259        let console = Console::capture();
260        let inner = Vec::<u8>::new();
261        let mut proxy = FileProxy::with_console(console, inner);
262
263        // Write without newline
264        write!(proxy, "Partial").unwrap();
265        assert!(proxy.console().get_captured().is_empty());
266
267        // Explicit flush should print the partial line
268        proxy.flush().unwrap();
269        let output = proxy.console().get_captured();
270        assert!(output.contains("Partial"));
271    }
272
273    #[test]
274    fn test_file_proxy_inner_access() {
275        let console = Console::capture();
276        let inner = Vec::<u8>::new();
277        let mut proxy = FileProxy::with_console(console, inner);
278
279        // Inner should be accessible
280        assert!(proxy.inner().is_empty());
281        proxy.inner_mut().push(42);
282        assert_eq!(proxy.inner().len(), 1);
283
284        let inner = proxy.into_inner();
285        assert_eq!(inner, vec![42]);
286    }
287}