rust_expect/transcript/
recorder.rs

1//! Session recording.
2
3use std::sync::{Arc, Mutex};
4use std::time::{Duration, Instant};
5
6use super::format::{Transcript, TranscriptEvent, TranscriptMetadata};
7
8/// A session recorder.
9#[derive(Debug)]
10pub struct Recorder {
11    /// Start time.
12    start: Instant,
13    /// Recorded transcript.
14    transcript: Arc<Mutex<Transcript>>,
15    /// Whether recording is active.
16    recording: bool,
17    /// Maximum recording duration.
18    max_duration: Option<Duration>,
19    /// Maximum events to record.
20    max_events: Option<usize>,
21}
22
23impl Recorder {
24    /// Create a new recorder.
25    #[must_use]
26    pub fn new(width: u16, height: u16) -> Self {
27        Self {
28            start: Instant::now(),
29            transcript: Arc::new(Mutex::new(Transcript::new(TranscriptMetadata::new(
30                width, height,
31            )))),
32            recording: true,
33            max_duration: None,
34            max_events: None,
35        }
36    }
37
38    /// Set maximum duration.
39    #[must_use]
40    pub const fn with_max_duration(mut self, duration: Duration) -> Self {
41        self.max_duration = Some(duration);
42        self
43    }
44
45    /// Set maximum events.
46    #[must_use]
47    pub const fn with_max_events(mut self, count: usize) -> Self {
48        self.max_events = Some(count);
49        self
50    }
51
52    /// Get elapsed time since start.
53    #[must_use]
54    pub fn elapsed(&self) -> Duration {
55        self.start.elapsed()
56    }
57
58    /// Check if recording is active.
59    #[must_use]
60    pub const fn is_recording(&self) -> bool {
61        self.recording
62    }
63
64    /// Stop recording.
65    pub fn stop(&mut self) {
66        self.recording = false;
67        if let Ok(mut t) = self.transcript.lock() {
68            t.metadata.duration = Some(self.elapsed());
69        }
70    }
71
72    /// Record an output event.
73    pub fn record_output(&self, data: &[u8]) {
74        if !self.should_record() {
75            return;
76        }
77        self.push_event(TranscriptEvent::output(self.elapsed(), data));
78    }
79
80    /// Record an input event.
81    pub fn record_input(&self, data: &[u8]) {
82        if !self.should_record() {
83            return;
84        }
85        self.push_event(TranscriptEvent::input(self.elapsed(), data));
86    }
87
88    /// Record a resize event.
89    pub fn record_resize(&self, cols: u16, rows: u16) {
90        if !self.should_record() {
91            return;
92        }
93        self.push_event(TranscriptEvent::resize(self.elapsed(), cols, rows));
94    }
95
96    /// Add a marker.
97    pub fn add_marker(&self, label: &str) {
98        if !self.should_record() {
99            return;
100        }
101        self.push_event(TranscriptEvent::marker(self.elapsed(), label));
102    }
103
104    /// Check if we should still record.
105    fn should_record(&self) -> bool {
106        if !self.recording {
107            return false;
108        }
109
110        if let Some(max_dur) = self.max_duration
111            && self.elapsed() > max_dur
112        {
113            return false;
114        }
115
116        if let Some(max_events) = self.max_events
117            && let Ok(t) = self.transcript.lock()
118            && t.events.len() >= max_events
119        {
120            return false;
121        }
122
123        true
124    }
125
126    /// Push an event to the transcript.
127    fn push_event(&self, event: TranscriptEvent) {
128        if let Ok(mut t) = self.transcript.lock() {
129            t.push(event);
130        }
131    }
132
133    /// Get the transcript.
134    #[must_use]
135    pub fn transcript(&self) -> Arc<Mutex<Transcript>> {
136        Arc::clone(&self.transcript)
137    }
138
139    /// Take the transcript, consuming the recorder.
140    #[must_use]
141    pub fn into_transcript(self) -> Transcript {
142        Arc::try_unwrap(self.transcript)
143            .ok()
144            .and_then(|m| m.into_inner().ok())
145            .unwrap_or_else(|| Transcript::new(TranscriptMetadata::new(80, 24)))
146    }
147
148    /// Get event count.
149    #[must_use]
150    pub fn event_count(&self) -> usize {
151        self.transcript.lock().map(|t| t.events.len()).unwrap_or(0)
152    }
153}
154
155/// Builder for creating recorders.
156#[derive(Debug, Default)]
157pub struct RecorderBuilder {
158    width: u16,
159    height: u16,
160    command: Option<String>,
161    title: Option<String>,
162    max_duration: Option<Duration>,
163    max_events: Option<usize>,
164}
165
166impl RecorderBuilder {
167    /// Create a new builder.
168    #[must_use]
169    pub fn new() -> Self {
170        Self {
171            width: 80,
172            height: 24,
173            ..Default::default()
174        }
175    }
176
177    /// Set dimensions.
178    #[must_use]
179    pub const fn size(mut self, width: u16, height: u16) -> Self {
180        self.width = width;
181        self.height = height;
182        self
183    }
184
185    /// Set command.
186    #[must_use]
187    pub fn command(mut self, cmd: impl Into<String>) -> Self {
188        self.command = Some(cmd.into());
189        self
190    }
191
192    /// Set title.
193    #[must_use]
194    pub fn title(mut self, title: impl Into<String>) -> Self {
195        self.title = Some(title.into());
196        self
197    }
198
199    /// Set maximum duration.
200    #[must_use]
201    pub const fn max_duration(mut self, duration: Duration) -> Self {
202        self.max_duration = Some(duration);
203        self
204    }
205
206    /// Set maximum events.
207    #[must_use]
208    pub const fn max_events(mut self, count: usize) -> Self {
209        self.max_events = Some(count);
210        self
211    }
212
213    /// Build the recorder.
214    #[must_use]
215    pub fn build(self) -> Recorder {
216        let mut recorder = Recorder::new(self.width, self.height);
217
218        if let Some(duration) = self.max_duration {
219            recorder.max_duration = Some(duration);
220        }
221        if let Some(events) = self.max_events {
222            recorder.max_events = Some(events);
223        }
224
225        if let Ok(mut t) = recorder.transcript.lock() {
226            t.metadata.command = self.command;
227            t.metadata.title = self.title;
228        }
229
230        recorder
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn recorder_basic() {
240        let recorder = Recorder::new(80, 24);
241        recorder.record_output(b"hello");
242        recorder.record_input(b"world");
243
244        assert_eq!(recorder.event_count(), 2);
245    }
246
247    #[test]
248    fn recorder_stop() {
249        let mut recorder = Recorder::new(80, 24);
250        recorder.record_output(b"before");
251        recorder.stop();
252        recorder.record_output(b"after");
253
254        assert_eq!(recorder.event_count(), 1);
255    }
256
257    #[test]
258    fn recorder_builder() {
259        let recorder = RecorderBuilder::new()
260            .size(120, 40)
261            .title("Test")
262            .max_events(10)
263            .build();
264
265        assert!(recorder.is_recording());
266    }
267}