ralph_core/
cli_capture.rs1use ralph_proto::{FrameCapture, TerminalWrite, UxEvent};
8use std::io::{self, Write};
9use std::time::Instant;
10
11pub struct CliCapture<W> {
38 inner: W,
40
41 captures: Vec<UxEvent>,
43
44 start_time: Instant,
46
47 is_stdout: bool,
49}
50
51impl<W> CliCapture<W> {
52 pub fn new(inner: W, is_stdout: bool) -> Self {
59 Self {
60 inner,
61 captures: Vec::new(),
62 start_time: Instant::now(),
63 is_stdout,
64 }
65 }
66
67 pub fn with_start_time(inner: W, is_stdout: bool, start_time: Instant) -> Self {
72 Self {
73 inner,
74 captures: Vec::new(),
75 start_time,
76 is_stdout,
77 }
78 }
79
80 #[allow(clippy::cast_possible_truncation)]
82 fn offset_ms(&self) -> u64 {
83 self.start_time.elapsed().as_millis() as u64
85 }
86
87 pub fn inner(&self) -> &W {
89 &self.inner
90 }
91
92 pub fn inner_mut(&mut self) -> &mut W {
94 &mut self.inner
95 }
96
97 pub fn into_inner(self) -> W {
99 self.inner
100 }
101}
102
103impl<W: Write> Write for CliCapture<W> {
104 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
105 let n = self.inner.write(buf)?;
107
108 if n > 0 {
110 self.captures
111 .push(UxEvent::TerminalWrite(TerminalWrite::new(
112 &buf[..n],
113 self.is_stdout,
114 self.offset_ms(),
115 )));
116 }
117
118 Ok(n)
119 }
120
121 fn flush(&mut self) -> io::Result<()> {
122 self.inner.flush()
123 }
124}
125
126impl<W: Send + Sync> FrameCapture for CliCapture<W> {
127 fn take_captures(&mut self) -> Vec<UxEvent> {
128 std::mem::take(&mut self.captures)
129 }
130
131 fn has_captures(&self) -> bool {
132 !self.captures.is_empty()
133 }
134}
135
136pub struct CliCapturePair<Stdout, Stderr> {
141 pub stdout: CliCapture<Stdout>,
143
144 pub stderr: CliCapture<Stderr>,
146}
147
148impl<Stdout, Stderr> CliCapturePair<Stdout, Stderr> {
149 pub fn new(stdout: Stdout, stderr: Stderr) -> Self {
151 let start_time = Instant::now();
152 Self {
153 stdout: CliCapture::with_start_time(stdout, true, start_time),
154 stderr: CliCapture::with_start_time(stderr, false, start_time),
155 }
156 }
157}
158
159impl<Stdout: Send + Sync, Stderr: Send + Sync> CliCapturePair<Stdout, Stderr> {
160 pub fn take_all_captures(&mut self) -> Vec<UxEvent> {
162 let mut stdout_events = self.stdout.take_captures();
163 let mut stderr_events = self.stderr.take_captures();
164
165 stdout_events.append(&mut stderr_events);
167 stdout_events.sort_by_key(|event| match event {
168 UxEvent::TerminalWrite(tw) => tw.offset_ms,
169 UxEvent::TerminalResize(tr) => tr.offset_ms,
170 UxEvent::TerminalColorMode(cm) => cm.offset_ms,
171 UxEvent::TuiFrame(tf) => tf.offset_ms,
172 });
173
174 stdout_events
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_capture_write() {
184 let mut output = Vec::new();
185
186 let events = {
188 let mut capture = CliCapture::new(&mut output, true);
189 write!(capture, "Hello").unwrap();
190 capture.take_captures()
191 };
192
193 assert_eq!(output, b"Hello");
195
196 assert_eq!(events.len(), 1);
198
199 if let UxEvent::TerminalWrite(tw) = &events[0] {
200 assert!(tw.stdout);
201 let decoded = tw.decode_bytes().unwrap();
202 assert_eq!(decoded, b"Hello");
203 } else {
204 panic!("Expected TerminalWrite event");
205 }
206 }
207
208 #[test]
209 fn test_capture_multiple_writes() {
210 let mut output = Vec::new();
211 let mut capture = CliCapture::new(&mut output, true);
212
213 writeln!(capture, "Line 1").unwrap();
214 writeln!(capture, "Line 2").unwrap();
215
216 let events = capture.take_captures();
217 assert_eq!(events.len(), 2);
218
219 if let (UxEvent::TerminalWrite(tw1), UxEvent::TerminalWrite(tw2)) = (&events[0], &events[1])
221 {
222 assert!(tw2.offset_ms >= tw1.offset_ms);
223 }
224 }
225
226 #[test]
227 fn test_capture_stderr() {
228 let mut output = Vec::new();
229 let mut capture = CliCapture::new(&mut output, false);
230
231 write!(capture, "Error!").unwrap();
232
233 let events = capture.take_captures();
234 if let UxEvent::TerminalWrite(tw) = &events[0] {
235 assert!(!tw.stdout); }
237 }
238
239 #[test]
240 fn test_capture_pair() {
241 let stdout_buf = Vec::new();
242 let stderr_buf = Vec::new();
243 let mut pair = CliCapturePair::new(stdout_buf, stderr_buf);
244
245 write!(pair.stdout, "out").unwrap();
246 write!(pair.stderr, "err").unwrap();
247
248 let events = pair.take_all_captures();
249 assert_eq!(events.len(), 2);
250 }
251
252 #[test]
253 fn test_take_captures_clears_buffer() {
254 let mut output = Vec::new();
255 let mut capture = CliCapture::new(&mut output, true);
256
257 write!(capture, "test").unwrap();
258 assert!(capture.has_captures());
259
260 let events = capture.take_captures();
261 assert_eq!(events.len(), 1);
262
263 assert!(!capture.has_captures());
265 assert!(capture.take_captures().is_empty());
266 }
267
268 #[test]
269 fn test_ansi_preservation() {
270 let mut output = Vec::new();
271 let mut capture = CliCapture::new(&mut output, true);
272
273 let ansi_text = b"\x1b[32mGreen\x1b[0m";
275 capture.write_all(ansi_text).unwrap();
276
277 let events = capture.take_captures();
278 if let UxEvent::TerminalWrite(tw) = &events[0] {
279 let decoded = tw.decode_bytes().unwrap();
280 assert_eq!(decoded, ansi_text);
281 }
282 }
283}