rust_expect/transcript/
player.rs

1//! Transcript playback.
2
3use std::io::Write;
4use std::time::{Duration, Instant};
5
6use super::format::{EventType, Transcript, TranscriptEvent};
7
8/// Playback speed.
9#[derive(Debug, Clone, Copy, PartialEq, Default)]
10pub enum PlaybackSpeed {
11    /// Real-time playback.
12    #[default]
13    Realtime,
14    /// Fixed speed multiplier.
15    Speed(f64),
16    /// Maximum speed (instant).
17    Instant,
18}
19
20/// Playback options.
21#[derive(Debug, Clone)]
22pub struct PlaybackOptions {
23    /// Playback speed.
24    pub speed: PlaybackSpeed,
25    /// Maximum idle time between events.
26    pub max_idle: Duration,
27    /// Whether to show input events.
28    pub show_input: bool,
29    /// Whether to pause at markers.
30    pub pause_at_markers: bool,
31}
32
33impl Default for PlaybackOptions {
34    fn default() -> Self {
35        Self {
36            speed: PlaybackSpeed::Realtime,
37            max_idle: Duration::from_secs(5),
38            show_input: false,
39            pause_at_markers: false,
40        }
41    }
42}
43
44impl PlaybackOptions {
45    /// Create new options.
46    #[must_use]
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Set playback speed.
52    #[must_use]
53    pub const fn with_speed(mut self, speed: PlaybackSpeed) -> Self {
54        self.speed = speed;
55        self
56    }
57
58    /// Set maximum idle time.
59    #[must_use]
60    pub const fn with_max_idle(mut self, max: Duration) -> Self {
61        self.max_idle = max;
62        self
63    }
64
65    /// Show input events.
66    #[must_use]
67    pub const fn with_show_input(mut self, show: bool) -> Self {
68        self.show_input = show;
69        self
70    }
71}
72
73/// Transcript player state.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum PlayerState {
76    /// Not started.
77    Stopped,
78    /// Playing.
79    Playing,
80    /// Paused.
81    Paused,
82    /// Finished.
83    Finished,
84}
85
86/// A transcript player.
87pub struct Player<'a> {
88    /// The transcript to play.
89    transcript: &'a Transcript,
90    /// Current event index.
91    index: usize,
92    /// Playback options.
93    options: PlaybackOptions,
94    /// Player state.
95    state: PlayerState,
96    /// Playback start time.
97    start_time: Option<Instant>,
98    /// Last event time.
99    last_event_time: Duration,
100}
101
102impl<'a> Player<'a> {
103    /// Create a new player.
104    #[must_use]
105    pub fn new(transcript: &'a Transcript) -> Self {
106        Self {
107            transcript,
108            index: 0,
109            options: PlaybackOptions::default(),
110            state: PlayerState::Stopped,
111            start_time: None,
112            last_event_time: Duration::ZERO,
113        }
114    }
115
116    /// Set playback options.
117    #[must_use]
118    pub const fn with_options(mut self, options: PlaybackOptions) -> Self {
119        self.options = options;
120        self
121    }
122
123    /// Get current state.
124    #[must_use]
125    pub const fn state(&self) -> PlayerState {
126        self.state
127    }
128
129    /// Get current position (event index).
130    #[must_use]
131    pub const fn position(&self) -> usize {
132        self.index
133    }
134
135    /// Get total events.
136    #[must_use]
137    pub const fn total_events(&self) -> usize {
138        self.transcript.events.len()
139    }
140
141    /// Get current timestamp.
142    #[must_use]
143    pub fn current_time(&self) -> Duration {
144        if self.index < self.transcript.events.len() {
145            self.transcript.events[self.index].timestamp
146        } else {
147            self.transcript.duration()
148        }
149    }
150
151    /// Start playback.
152    pub fn play(&mut self) {
153        self.state = PlayerState::Playing;
154        self.start_time = Some(Instant::now());
155    }
156
157    /// Pause playback.
158    pub const fn pause(&mut self) {
159        self.state = PlayerState::Paused;
160    }
161
162    /// Stop playback.
163    pub const fn stop(&mut self) {
164        self.state = PlayerState::Stopped;
165        self.index = 0;
166        self.start_time = None;
167        self.last_event_time = Duration::ZERO;
168    }
169
170    /// Seek to a specific event.
171    pub fn seek(&mut self, index: usize) {
172        self.index = index.min(self.transcript.events.len());
173        if self.index < self.transcript.events.len() {
174            self.last_event_time = self.transcript.events[self.index].timestamp;
175        }
176    }
177
178    /// Get next event to play.
179    pub fn next_event(&mut self) -> Option<&TranscriptEvent> {
180        if self.state != PlayerState::Playing || self.index >= self.transcript.events.len() {
181            if self.index >= self.transcript.events.len() {
182                self.state = PlayerState::Finished;
183            }
184            return None;
185        }
186
187        let event = &self.transcript.events[self.index];
188        self.index += 1;
189        self.last_event_time = event.timestamp;
190        Some(event)
191    }
192
193    /// Calculate delay before next event.
194    #[must_use]
195    pub fn delay_to_next(&self) -> Duration {
196        if self.index >= self.transcript.events.len() {
197            return Duration::ZERO;
198        }
199
200        let next_time = self.transcript.events[self.index].timestamp;
201        let delay = next_time.saturating_sub(self.last_event_time);
202
203        // Apply speed
204        let delay = match self.options.speed {
205            PlaybackSpeed::Instant => Duration::ZERO,
206            PlaybackSpeed::Realtime => delay,
207            PlaybackSpeed::Speed(mult) => Duration::from_secs_f64(delay.as_secs_f64() / mult),
208        };
209
210        // Apply max idle
211        delay.min(self.options.max_idle)
212    }
213
214    /// Play to a writer (blocking).
215    pub fn play_to<W: Write>(&mut self, writer: &mut W) -> std::io::Result<()> {
216        self.play();
217
218        while let Some(event) = self.next_event() {
219            // Clone what we need from event to release the borrow
220            let event_type = event.event_type;
221            let event_data = event.data.clone();
222
223            // Now we can use self again
224            let delay = self.delay_to_next();
225            if delay > Duration::ZERO {
226                std::thread::sleep(delay);
227            }
228
229            // Handle event using cloned data
230            match event_type {
231                EventType::Output => {
232                    writer.write_all(&event_data)?;
233                    writer.flush()?;
234                }
235                EventType::Input if self.options.show_input => {
236                    // Could highlight input differently
237                    writer.write_all(&event_data)?;
238                    writer.flush()?;
239                }
240                EventType::Marker if self.options.pause_at_markers => {
241                    self.pause();
242                    // User would need to call play() to resume
243                    break;
244                }
245                _ => {}
246            }
247        }
248
249        Ok(())
250    }
251}
252
253/// Play a transcript to stdout.
254pub fn play_to_stdout(transcript: &Transcript, options: PlaybackOptions) -> std::io::Result<()> {
255    let mut player = Player::new(transcript).with_options(options);
256    let mut stdout = std::io::stdout();
257    player.play_to(&mut stdout)
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::transcript::format::TranscriptMetadata;
264
265    #[test]
266    fn player_basic() {
267        let mut transcript = Transcript::new(TranscriptMetadata::new(80, 24));
268        transcript.push(TranscriptEvent::output(Duration::ZERO, b"hello"));
269        transcript.push(TranscriptEvent::output(
270            Duration::from_millis(100),
271            b" world",
272        ));
273
274        let mut player = Player::new(&transcript);
275        assert_eq!(player.total_events(), 2);
276        assert_eq!(player.state(), PlayerState::Stopped);
277
278        player.play();
279        assert_eq!(player.state(), PlayerState::Playing);
280
281        let event = player.next_event().unwrap();
282        assert_eq!(event.data, b"hello");
283
284        let event = player.next_event().unwrap();
285        assert_eq!(event.data, b" world");
286
287        assert!(player.next_event().is_none());
288        assert_eq!(player.state(), PlayerState::Finished);
289    }
290
291    #[test]
292    fn player_instant_speed() {
293        let mut transcript = Transcript::new(TranscriptMetadata::new(80, 24));
294        transcript.push(TranscriptEvent::output(Duration::ZERO, b"a"));
295        transcript.push(TranscriptEvent::output(Duration::from_secs(10), b"b"));
296
297        let player = Player::new(&transcript)
298            .with_options(PlaybackOptions::new().with_speed(PlaybackSpeed::Instant));
299
300        assert_eq!(player.delay_to_next(), Duration::ZERO);
301    }
302}