retroglyph_recorder/frame_recorder.rs
1//! [`FrameRecorder`], its [`FrameRecorderHandle`], and [`CapturedFrame`]: the `TestHarness`/
2//! `Headless`-driven capture source.
3
4use crate::OwnedCell;
5use retroglyph_core::backend::{Compositing, Cursor, CursorStyle, DrawCell, Input, Output};
6use retroglyph_core::event::Event;
7use retroglyph_core::grid::{Pos, Size};
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10
11/// One changed frame: every cell [`Output::draw_layers`] reported changed on that call, with the
12/// wall-clock time since [`FrameRecorder::new`].
13#[derive(Debug, Clone)]
14pub struct CapturedFrame {
15 /// Time since the recorder was created that this frame's `draw_layers` call arrived.
16 pub at: Duration,
17 /// Every cell changed on this call, in the order [`Output::draw_layers`] delivered them.
18 pub cells: Vec<OwnedCell>,
19}
20
21/// State shared between a [`FrameRecorder`] and every [`FrameRecorderHandle`] cloned from it.
22struct State {
23 started: Instant,
24 frames: Vec<CapturedFrame>,
25}
26
27/// A cheap, cloneable handle onto a [`FrameRecorder`]'s captured frames.
28///
29/// Exists for the same reason [`InputRecorder`](crate::InputRecorder)'s
30/// [`RecorderHandle`](crate::RecorderHandle) does: a driver like
31/// [`retroglyph_core::app::run_on`] takes a `Terminal<B>` (and so the `FrameRecorder<B>` it
32/// wraps) by value and never hands it back, so the only way to read what was captured once the
33/// loop returns is a handle taken out beforehand. Backed by `Arc<Mutex<..>>`: cloning shares the
34/// same underlying frames, it does not fork them.
35#[derive(Clone)]
36pub struct FrameRecorderHandle {
37 state: Arc<Mutex<State>>,
38}
39
40impl FrameRecorderHandle {
41 fn new() -> Self {
42 Self {
43 state: Arc::new(Mutex::new(State {
44 started: Instant::now(),
45 frames: Vec::new(),
46 })),
47 }
48 }
49
50 fn record(&self, cells: Vec<OwnedCell>) {
51 let mut state = self.lock();
52 let at = state.started.elapsed();
53 state.frames.push(CapturedFrame { at, cells });
54 }
55
56 /// Locks the shared state, recovering it if a previous holder panicked while holding the
57 /// lock rather than poisoning every future access -- the same rationale
58 /// [`RecorderHandle`](crate::RecorderHandle)'s own lock helper uses.
59 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
60 self.state
61 .lock()
62 .unwrap_or_else(std::sync::PoisonError::into_inner)
63 }
64
65 /// A clone of every frame captured so far, in the order it was drawn.
66 #[must_use]
67 pub fn frames(&self) -> Vec<CapturedFrame> {
68 self.lock().frames.clone()
69 }
70
71 /// Takes every captured frame, leaving the shared buffer empty. Use this (rather than
72 /// [`frames`](Self::frames) plus a manual clear) to hand ownership to
73 /// [`write_cast`](crate::write_cast) without cloning it.
74 #[must_use]
75 pub fn take_frames(&self) -> Vec<CapturedFrame> {
76 std::mem::take(&mut self.lock().frames)
77 }
78}
79
80/// Wraps a backend and records every frame it draws.
81///
82/// Taps [`Output::draw_layers`] -- the same [`DrawCell`] diff stream
83/// [`Terminal::present`](retroglyph_core::terminal::Terminal::present) and every backend already
84/// consume -- and buffers each call's changed cells as an owned [`CapturedFrame`], timestamped
85/// against when recording started. Works over a [`Backend`](retroglyph_core::backend::Backend),
86/// or any subset of `Output`/[`Input`]/[`Cursor`]: only `Output` is tapped, everything else is
87/// delegated unchanged, the same shape as
88/// [`InputRecorder`](crate::InputRecorder).
89///
90/// `DrawCell` borrows from the source grid, so capture converts to [`OwnedCell`] immediately on
91/// each `draw_layers` call rather than deferring to export time, when the borrow would already be
92/// gone.
93///
94/// # Known limitation
95///
96/// Buffers the whole session in memory (every frame captured, and every cell in it, lives in the
97/// shared [`FrameRecorderHandle`] until [`write_cast`](crate::write_cast) exports and
98/// [`take_frames`](FrameRecorderHandle::take_frames) drops it) rather than streaming to disk as
99/// frames arrive. Fine for docs/demo-length captures -- the ones this crate exists for -- but not
100/// a fit for an unbounded, long-running recording.
101///
102/// # Examples
103///
104/// ```
105/// use retroglyph_core::backend::{Headless, Output};
106/// use retroglyph_core::color::Style;
107/// use retroglyph_core::grid::Pos;
108/// use retroglyph_core::tile::Tile;
109/// use retroglyph_recorder::FrameRecorder;
110///
111/// let mut recorder = FrameRecorder::new(Headless::new(10, 3));
112/// let tile = Tile::new('x', Style::default());
113/// let cell = retroglyph_core::backend::DrawCell::new(Pos::new(0, 0), &tile);
114/// recorder.draw_layers(std::iter::once(cell)).unwrap();
115///
116/// assert_eq!(recorder.frames().len(), 1);
117/// ```
118pub struct FrameRecorder<B> {
119 inner: B,
120 handle: FrameRecorderHandle,
121}
122
123impl<B: Output> FrameRecorder<B> {
124 /// Wraps `inner`. Recording starts immediately: the clock [`CapturedFrame::at`] is measured
125 /// against begins ticking here, not on the first `draw_layers` call.
126 pub fn new(inner: B) -> Self {
127 Self {
128 inner,
129 handle: FrameRecorderHandle::new(),
130 }
131 }
132}
133
134impl<B> FrameRecorder<B> {
135 /// A cloneable [`FrameRecorderHandle`] onto this recorder's shared state, for reading
136 /// captured frames after a driver (e.g. [`retroglyph_core::app::run_on`]) has taken this
137 /// recorder's `Terminal` by value and won't hand it back.
138 #[must_use]
139 pub fn handle(&self) -> FrameRecorderHandle {
140 self.handle.clone()
141 }
142
143 /// [`FrameRecorderHandle::frames`] on this recorder's own handle.
144 #[must_use]
145 pub fn frames(&self) -> Vec<CapturedFrame> {
146 self.handle.frames()
147 }
148
149 /// [`FrameRecorderHandle::take_frames`] on this recorder's own handle.
150 pub fn take_frames(&mut self) -> Vec<CapturedFrame> {
151 self.handle.take_frames()
152 }
153
154 /// The wrapped backend, for anything not exposed through the decorator itself.
155 pub const fn inner(&self) -> &B {
156 &self.inner
157 }
158
159 /// The wrapped backend, mutably.
160 pub const fn inner_mut(&mut self) -> &mut B {
161 &mut self.inner
162 }
163}
164
165impl<B: Output> Output for FrameRecorder<B> {
166 type Error = B::Error;
167
168 // `Output::draw`'s default body already forwards to `draw_layers` below, so every cell this
169 // recorder ever sees -- including ones arriving through the single-layer `draw` entry point
170 // -- is captured without needing an override here.
171
172 fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
173 where
174 I: Iterator<Item = DrawCell<'a>>,
175 {
176 let cells: Vec<OwnedCell> = content.map(OwnedCell::from).collect();
177 // An empty `draw_layers` call (nothing changed since the last present) is common --
178 // `Terminal::present` calls it every frame regardless -- and recording it would pad
179 // every capture with useless idle frames. Only cells that actually changed produce a
180 // `CapturedFrame`.
181 if cells.is_empty() {
182 return self.inner.draw_layers(std::iter::empty());
183 }
184 let forwarded = cells.iter().map(|cell| {
185 DrawCell::on_layer(cell.layer, cell.pos, &cell.tile)
186 .with_grapheme(cell.grapheme.as_deref())
187 .with_tint(cell.tint)
188 });
189 self.inner.draw_layers(forwarded)?;
190 self.handle.record(cells);
191 Ok(())
192 }
193
194 fn compositing(&self) -> Compositing {
195 self.inner.compositing()
196 }
197
198 fn flush(&mut self) -> Result<(), Self::Error> {
199 self.inner.flush()
200 }
201
202 fn size(&self) -> Size {
203 self.inner.size()
204 }
205
206 fn clear(&mut self) -> Result<(), Self::Error> {
207 self.inner.clear()
208 }
209
210 fn resize(&mut self, size: Size) {
211 self.inner.resize(size);
212 }
213}
214
215impl<B: Input> Input for FrameRecorder<B> {
216 fn poll_event(&mut self, timeout: Duration) -> Option<Event> {
217 self.inner.poll_event(timeout)
218 }
219
220 fn push_event(&mut self, event: Event) {
221 self.inner.push_event(event);
222 }
223}
224
225impl<B: Cursor> Cursor for FrameRecorder<B> {
226 fn set_cursor_visible(&mut self, visible: bool) {
227 self.inner.set_cursor_visible(visible);
228 }
229
230 fn set_cursor_position(&mut self, position: Pos) {
231 self.inner.set_cursor_position(position);
232 }
233
234 fn set_cursor_style(&mut self, style: CursorStyle) {
235 self.inner.set_cursor_style(style);
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use retroglyph_core::backend::Headless;
243 use retroglyph_core::color::Style;
244 use retroglyph_core::tile::Tile;
245
246 fn cell(x: u16, y: u16, glyph: char) -> (Pos, Tile) {
247 (Pos::new(x, y), Tile::new(glyph, Style::default()))
248 }
249
250 #[test]
251 fn buffers_owned_cells_per_draw_layers_call() {
252 let mut recorder = FrameRecorder::new(Headless::new(10, 3));
253 let (pos, tile) = cell(0, 0, 'a');
254 recorder
255 .draw_layers(std::iter::once(DrawCell::new(pos, &tile)))
256 .unwrap();
257
258 let frames = recorder.frames();
259 assert_eq!(frames.len(), 1);
260 assert_eq!(frames[0].cells.len(), 1);
261 assert_eq!(frames[0].cells[0].tile.glyph(), 'a');
262 }
263
264 #[test]
265 fn empty_draw_layers_calls_produce_no_frame() {
266 let mut recorder = FrameRecorder::new(Headless::new(10, 3));
267 recorder
268 .draw_layers(std::iter::empty::<DrawCell<'_>>())
269 .unwrap();
270
271 assert!(recorder.frames().is_empty());
272 }
273
274 #[test]
275 fn take_frames_empties_the_buffer() {
276 let mut recorder = FrameRecorder::new(Headless::new(10, 3));
277 let (pos, tile) = cell(0, 0, 'a');
278 recorder
279 .draw_layers(std::iter::once(DrawCell::new(pos, &tile)))
280 .unwrap();
281
282 let taken = recorder.take_frames();
283 assert_eq!(taken.len(), 1);
284 assert!(recorder.frames().is_empty());
285 }
286
287 #[test]
288 fn forwards_to_inner_backend() {
289 let mut recorder = FrameRecorder::new(Headless::new(10, 3));
290 let (pos, tile) = cell(1, 1, 'z');
291 recorder
292 .draw_layers(std::iter::once(DrawCell::new(pos, &tile)))
293 .unwrap();
294 recorder.flush().unwrap();
295
296 assert_eq!(recorder.inner().grid()[pos].glyph(), 'z');
297 }
298
299 #[test]
300 fn a_handle_taken_before_a_move_still_reads_frames_captured_after_it() {
301 let mut recorder = FrameRecorder::new(Headless::new(10, 3));
302 let handle = recorder.handle();
303
304 let (pos, tile) = cell(0, 0, 'a');
305 recorder
306 .draw_layers(std::iter::once(DrawCell::new(pos, &tile)))
307 .unwrap();
308 drop(recorder); // the value moves on (e.g. into `Terminal`/`run_on`); the handle survives.
309
310 assert_eq!(handle.frames().len(), 1);
311 }
312}