1use std::io::{self, Stdout, Write};
10
11use crate::ansi::AnsiDecoder;
12use crate::text::Text;
13use crate::{Console, ConsoleOptions};
14
15pub struct FileProxy<C: Write, W: Write> {
40 console: Console<C>,
42 inner: W,
44 buffer: String,
46 decoder: AnsiDecoder,
48}
49
50impl<W: Write> FileProxy<Stdout, W> {
51 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 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 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 pub fn inner(&self) -> &W {
95 &self.inner
96 }
97
98 pub fn inner_mut(&mut self) -> &mut W {
100 &mut self.inner
101 }
102
103 pub fn console(&self) -> &Console<C> {
105 &self.console
106 }
107
108 pub fn console_mut(&mut self) -> &mut Console<C> {
110 &mut self.console
111 }
112
113 pub fn into_inner(self) -> W {
115 self.inner
116 }
117
118 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 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 self.buffer.push_str(remaining);
139 break;
140 }
141 }
142
143 if !lines.is_empty() {
145 let decoded_texts: Vec<Text> = lines
147 .iter()
148 .map(|line| self.decoder.decode_line(line))
149 .collect();
150
151 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 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 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 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!(proxy, "Hello, ").unwrap();
218 assert!(proxy.console().get_captured().is_empty());
219
220 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 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!(proxy, "Partial").unwrap();
265 assert!(proxy.console().get_captured().is_empty());
266
267 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 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}