Skip to main content

ftui_runtime/
input_macro.rs

1#![forbid(unsafe_code)]
2
3//! Input macro recording and playback.
4//!
5//! Record terminal input events with timing information for deterministic
6//! replay through the [`ProgramSimulator`](crate::simulator::ProgramSimulator).
7//!
8//! # Example
9//!
10//! ```ignore
11//! use ftui_runtime::input_macro::{InputMacro, MacroRecorder, MacroPlayer};
12//! use ftui_runtime::simulator::ProgramSimulator;
13//! use ftui_core::event::Event;
14//! use std::time::Duration;
15//!
16//! // Record events
17//! let mut recorder = MacroRecorder::new("test_flow");
18//! recorder.record_event(some_event.clone());
19//! // ... time passes ...
20//! recorder.record_event(another_event.clone());
21//! let macro_recording = recorder.finish();
22//!
23//! // Replay through simulator
24//! let mut sim = ProgramSimulator::new(my_model);
25//! sim.init();
26//! let mut player = MacroPlayer::new(&macro_recording);
27//! player.replay_all(&mut sim);
28//! ```
29
30use ftui_core::event::Event;
31use web_time::{Duration, Instant};
32
33/// A recorded input event with timing relative to recording start.
34#[derive(Debug, Clone)]
35pub struct TimedEvent {
36    /// The recorded event.
37    pub event: Event,
38    /// Delay from the previous event (or from recording start for the first event).
39    pub delay: Duration,
40}
41
42impl TimedEvent {
43    /// Create a new timed event with the given delay.
44    pub fn new(event: Event, delay: Duration) -> Self {
45        Self { event, delay }
46    }
47
48    /// Create a timed event with zero delay.
49    pub fn immediate(event: Event) -> Self {
50        Self {
51            event,
52            delay: Duration::ZERO,
53        }
54    }
55}
56
57/// Metadata about a recorded macro.
58#[derive(Debug, Clone)]
59pub struct MacroMetadata {
60    /// Human-readable name for this macro.
61    pub name: String,
62    /// Terminal size at recording time.
63    pub terminal_size: (u16, u16),
64    /// Total duration of the recording.
65    pub total_duration: Duration,
66}
67
68/// A recorded sequence of input events with timing.
69///
70/// An `InputMacro` captures events and their relative timing so they can
71/// be replayed deterministically through a [`ProgramSimulator`](crate::simulator::ProgramSimulator).
72#[derive(Debug, Clone)]
73pub struct InputMacro {
74    /// The recorded events with timing.
75    events: Vec<TimedEvent>,
76    /// Recording metadata.
77    metadata: MacroMetadata,
78}
79
80impl InputMacro {
81    /// Create a new macro from events and metadata.
82    pub fn new(events: Vec<TimedEvent>, metadata: MacroMetadata) -> Self {
83        Self { events, metadata }
84    }
85
86    /// Create a macro from events with no timing (all zero delay).
87    ///
88    /// Useful for building test macros programmatically.
89    pub fn from_events(name: impl Into<String>, events: Vec<Event>) -> Self {
90        let timed: Vec<TimedEvent> = events.into_iter().map(TimedEvent::immediate).collect();
91        Self {
92            metadata: MacroMetadata {
93                name: name.into(),
94                terminal_size: (80, 24),
95                total_duration: Duration::ZERO,
96            },
97            events: timed,
98        }
99    }
100
101    /// Get the recorded events.
102    pub fn events(&self) -> &[TimedEvent] {
103        &self.events
104    }
105
106    /// Get the metadata.
107    #[inline]
108    pub fn metadata(&self) -> &MacroMetadata {
109        &self.metadata
110    }
111
112    /// Get the number of recorded events.
113    #[inline]
114    pub fn len(&self) -> usize {
115        self.events.len()
116    }
117
118    /// Check if the macro has no events.
119    #[inline]
120    pub fn is_empty(&self) -> bool {
121        self.events.is_empty()
122    }
123
124    /// Get the total duration of the recording.
125    #[inline]
126    pub fn total_duration(&self) -> Duration {
127        self.metadata.total_duration
128    }
129
130    /// Extract just the events (without timing) in order.
131    pub fn bare_events(&self) -> Vec<Event> {
132        self.events.iter().map(|te| te.event.clone()).collect()
133    }
134
135    /// Replay this macro through a simulator, honoring recorded delays.
136    pub fn replay_with_timing<M: crate::program::Model>(
137        &self,
138        sim: &mut crate::simulator::ProgramSimulator<M>,
139    ) {
140        let mut player = MacroPlayer::new(self);
141        player.replay_with_timing(sim);
142    }
143
144    /// Replay this macro through a simulator with a custom sleep function.
145    ///
146    /// Useful for tests that want deterministic timing without wall-clock sleep.
147    pub fn replay_with_sleeper<M, F>(
148        &self,
149        sim: &mut crate::simulator::ProgramSimulator<M>,
150        sleep: F,
151    ) where
152        M: crate::program::Model,
153        F: FnMut(Duration),
154    {
155        let mut player = MacroPlayer::new(self);
156        player.replay_with_sleeper(sim, sleep);
157    }
158}
159
160/// Records input events with timing into an [`InputMacro`].
161///
162/// Call [`record_event`](Self::record_event) for each event, then
163/// [`finish`](Self::finish) to produce the final macro.
164pub struct MacroRecorder {
165    name: String,
166    terminal_size: (u16, u16),
167    events: Vec<TimedEvent>,
168    start_time: Instant,
169    last_event_time: Instant,
170}
171
172impl MacroRecorder {
173    /// Start a new recording session.
174    pub fn new(name: impl Into<String>) -> Self {
175        let now = Instant::now();
176        Self {
177            name: name.into(),
178            terminal_size: (80, 24),
179            events: Vec::new(),
180            start_time: now,
181            last_event_time: now,
182        }
183    }
184
185    /// Set the terminal size metadata.
186    #[must_use]
187    pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
188        self.terminal_size = (width, height);
189        self
190    }
191
192    /// Record an event at the current time.
193    ///
194    /// The delay is measured from the previous event (or recording start).
195    pub fn record_event(&mut self, event: Event) {
196        let now = Instant::now();
197        let delay = now.duration_since(self.last_event_time);
198        #[cfg(feature = "tracing")]
199        tracing::debug!(event = ?event, delay = ?delay, "macro record event");
200        self.events.push(TimedEvent::new(event, delay));
201        self.last_event_time = now;
202    }
203
204    /// Record an event with an explicit delay from the previous event.
205    pub fn record_event_with_delay(&mut self, event: Event, delay: Duration) {
206        #[cfg(feature = "tracing")]
207        tracing::debug!(event = ?event, delay = ?delay, "macro record event");
208        self.events.push(TimedEvent::new(event, delay));
209        // Advance the synthetic clock
210        self.last_event_time += delay;
211    }
212
213    /// Get the number of events recorded so far.
214    pub fn event_count(&self) -> usize {
215        self.events.len()
216    }
217
218    /// Finish recording and produce the macro.
219    pub fn finish(self) -> InputMacro {
220        let total_duration = self.last_event_time.duration_since(self.start_time);
221        InputMacro {
222            events: self.events,
223            metadata: MacroMetadata {
224                name: self.name,
225                terminal_size: self.terminal_size,
226                total_duration,
227            },
228        }
229    }
230}
231
232/// Replays an [`InputMacro`] through a [`ProgramSimulator`].
233///
234/// Events are injected in order. Timing information is available
235/// for inspection but does not cause real delays (the simulator
236/// is deterministic and instant).
237pub struct MacroPlayer<'a> {
238    input_macro: &'a InputMacro,
239    position: usize,
240    elapsed: Duration,
241}
242
243impl<'a> MacroPlayer<'a> {
244    /// Create a player for the given macro.
245    pub fn new(input_macro: &'a InputMacro) -> Self {
246        Self {
247            input_macro,
248            position: 0,
249            elapsed: Duration::ZERO,
250        }
251    }
252
253    /// Get current playback position (event index).
254    pub fn position(&self) -> usize {
255        self.position
256    }
257
258    /// Get elapsed virtual time.
259    pub fn elapsed(&self) -> Duration {
260        self.elapsed
261    }
262
263    /// Check if playback is complete.
264    pub fn is_done(&self) -> bool {
265        self.position >= self.input_macro.len()
266    }
267
268    /// Get the number of remaining events.
269    pub fn remaining(&self) -> usize {
270        self.input_macro.len().saturating_sub(self.position)
271    }
272
273    /// Step one event, injecting it into the simulator.
274    ///
275    /// Returns `true` if an event was played, `false` if playback is complete.
276    pub fn step<M: crate::program::Model>(
277        &mut self,
278        sim: &mut crate::simulator::ProgramSimulator<M>,
279    ) -> bool {
280        if self.is_done() {
281            return false;
282        }
283
284        let timed = &self.input_macro.events[self.position];
285        #[cfg(feature = "tracing")]
286        tracing::debug!(event = ?timed.event, delay = ?timed.delay, "macro playback event");
287        self.elapsed = self.elapsed.saturating_add(timed.delay);
288        sim.inject_events(std::slice::from_ref(&timed.event));
289        self.position += 1;
290        true
291    }
292
293    /// Replay all remaining events into the simulator.
294    ///
295    /// Stops early if the simulator quits.
296    pub fn replay_all<M: crate::program::Model>(
297        &mut self,
298        sim: &mut crate::simulator::ProgramSimulator<M>,
299    ) {
300        while !self.is_done() && sim.is_running() {
301            self.step(sim);
302        }
303    }
304
305    /// Replay all remaining events, honoring recorded delays.
306    ///
307    /// This uses real wall-clock sleeping for each recorded delay before
308    /// injecting the event. Stops early if the simulator quits.
309    pub fn replay_with_timing<M: crate::program::Model>(
310        &mut self,
311        sim: &mut crate::simulator::ProgramSimulator<M>,
312    ) {
313        self.replay_with_sleeper(sim, std::thread::sleep);
314    }
315
316    /// Replay all remaining events with a custom sleep function.
317    ///
318    /// Useful for tests that want to avoid real sleeping while still verifying
319    /// the delay schedule.
320    pub fn replay_with_sleeper<M, F>(
321        &mut self,
322        sim: &mut crate::simulator::ProgramSimulator<M>,
323        mut sleep: F,
324    ) where
325        M: crate::program::Model,
326        F: FnMut(Duration),
327    {
328        while !self.is_done() && sim.is_running() {
329            let timed = &self.input_macro.events[self.position];
330            if timed.delay > Duration::ZERO {
331                sleep(timed.delay);
332            }
333            self.step(sim);
334        }
335    }
336
337    /// Replay events up to the given virtual time.
338    ///
339    /// Only events whose cumulative delay is within `until` are played.
340    pub fn replay_until<M: crate::program::Model>(
341        &mut self,
342        sim: &mut crate::simulator::ProgramSimulator<M>,
343        until: Duration,
344    ) {
345        while !self.is_done() && sim.is_running() {
346            let timed = &self.input_macro.events[self.position];
347            let next_elapsed = self.elapsed.saturating_add(timed.delay);
348            if next_elapsed > until {
349                break;
350            }
351            self.step(sim);
352        }
353    }
354
355    /// Reset playback to the beginning.
356    pub fn reset(&mut self) {
357        self.position = 0;
358        self.elapsed = Duration::ZERO;
359    }
360}
361
362// ---------------------------------------------------------------------------
363// MacroPlayback – deterministic scheduler for live playback
364// ---------------------------------------------------------------------------
365
366/// Deterministic playback scheduler with speed and looping controls.
367///
368/// Invariants:
369/// - Event order is preserved.
370/// - `elapsed` is monotonic for a given `advance` sequence.
371/// - No events are emitted without their cumulative delay being satisfied.
372///
373/// Failure modes:
374/// - If total duration is zero and looping is enabled, looping is ignored to
375///   avoid infinite emission within a single `advance` call.
376#[derive(Debug, Clone)]
377pub struct MacroPlayback {
378    input_macro: InputMacro,
379    position: usize,
380    elapsed: Duration,
381    next_due: Duration,
382    speed: f64,
383    looping: bool,
384    start_logged: bool,
385    stop_logged: bool,
386    error_logged: bool,
387}
388
389/// Safety cap to prevent pathological looping replays from monopolizing a
390/// frame when elapsed time spikes (e.g. host clock jumps / extreme speed).
391const MAX_DUE_EVENTS_PER_ADVANCE: usize = 4096;
392
393impl MacroPlayback {
394    /// Create a new playback scheduler for the given macro.
395    pub fn new(input_macro: InputMacro) -> Self {
396        let next_due = input_macro
397            .events()
398            .first()
399            .map(|e| e.delay)
400            .unwrap_or(Duration::ZERO);
401        Self {
402            input_macro,
403            position: 0,
404            elapsed: Duration::ZERO,
405            next_due,
406            speed: 1.0,
407            looping: false,
408            start_logged: false,
409            stop_logged: false,
410            error_logged: false,
411        }
412    }
413
414    /// Set playback speed (must be finite and positive).
415    pub fn set_speed(&mut self, speed: f64) {
416        self.speed = normalize_speed(speed);
417    }
418
419    /// Fluent speed setter.
420    #[must_use]
421    pub fn with_speed(mut self, speed: f64) -> Self {
422        self.set_speed(speed);
423        self
424    }
425
426    /// Enable or disable looping.
427    pub fn set_looping(&mut self, looping: bool) {
428        self.looping = looping;
429    }
430
431    /// Fluent looping setter.
432    #[must_use]
433    pub fn with_looping(mut self, looping: bool) -> Self {
434        self.set_looping(looping);
435        self
436    }
437
438    /// Get the current playback speed.
439    pub fn speed(&self) -> f64 {
440        self.speed
441    }
442
443    /// Get current playback position (event index).
444    pub fn position(&self) -> usize {
445        self.position
446    }
447
448    /// Get elapsed virtual time.
449    pub fn elapsed(&self) -> Duration {
450        self.elapsed
451    }
452
453    /// Check if playback is complete (non-looping).
454    pub fn is_done(&self) -> bool {
455        if self.input_macro.is_empty() {
456            return true;
457        }
458        if self.looping && self.input_macro.total_duration() > Duration::ZERO {
459            return false;
460        }
461        self.position >= self.input_macro.len()
462    }
463
464    /// Reset playback to the beginning.
465    pub fn reset(&mut self) {
466        self.position = 0;
467        self.elapsed = Duration::ZERO;
468        self.next_due = self
469            .input_macro
470            .events()
471            .first()
472            .map(|e| e.delay)
473            .unwrap_or(Duration::ZERO);
474        self.start_logged = false;
475        self.stop_logged = false;
476        self.error_logged = false;
477    }
478
479    /// Advance playback time and return any events now due.
480    pub fn advance(&mut self, delta: Duration) -> Vec<Event> {
481        if self.input_macro.is_empty() {
482            #[cfg(feature = "tracing")]
483            if !self.error_logged {
484                let meta = self.input_macro.metadata();
485                tracing::warn!(
486                    macro_event = "playback_error",
487                    reason = "macro_empty",
488                    name = %meta.name,
489                    events = 0usize,
490                    duration_ms = self.input_macro.total_duration().as_millis() as u64,
491                );
492                self.error_logged = true;
493            }
494            return Vec::new();
495        }
496        if self.is_done() {
497            return Vec::new();
498        }
499
500        #[cfg(feature = "tracing")]
501        if !self.start_logged {
502            let meta = self.input_macro.metadata();
503            tracing::info!(
504                macro_event = "playback_start",
505                name = %meta.name,
506                events = self.input_macro.len(),
507                duration_ms = self.input_macro.total_duration().as_millis() as u64,
508                speed = self.speed,
509                looping = self.looping,
510            );
511            self.start_logged = true;
512        }
513
514        let scaled = scale_duration(delta, self.speed);
515        let total_duration = self.input_macro.total_duration();
516        if self.looping && total_duration > Duration::ZERO && scaled == Duration::MAX {
517            // Overflowed speed scaling can produce effectively infinite backlog.
518            // Collapse to a single bounded loop window for this advance tick.
519            self.elapsed =
520                loop_elapsed_remainder(self.elapsed, total_duration).saturating_add(total_duration);
521        } else {
522            self.elapsed = self.elapsed.saturating_add(scaled);
523        }
524        let events = self.drain_due_events();
525
526        #[cfg(feature = "tracing")]
527        if self.is_done() && !self.stop_logged {
528            let meta = self.input_macro.metadata();
529            tracing::info!(
530                macro_event = "playback_stop",
531                reason = "completed",
532                name = %meta.name,
533                events = self.input_macro.len(),
534                elapsed_ms = self.elapsed.as_millis() as u64,
535                looping = self.looping,
536            );
537            self.stop_logged = true;
538        }
539
540        events
541    }
542
543    fn drain_due_events(&mut self) -> Vec<Event> {
544        let mut out = Vec::new();
545        let total_duration = self.input_macro.total_duration();
546        let can_loop = self.looping && total_duration > Duration::ZERO;
547        if can_loop && self.position >= self.input_macro.len() {
548            self.elapsed = loop_elapsed_remainder(self.elapsed, total_duration);
549            self.position = 0;
550            self.next_due = self
551                .input_macro
552                .events()
553                .first()
554                .map(|e| e.delay)
555                .unwrap_or(Duration::ZERO);
556        }
557
558        while out.len() < MAX_DUE_EVENTS_PER_ADVANCE
559            && self.position < self.input_macro.len()
560            && self.elapsed >= self.next_due
561        {
562            let timed = &self.input_macro.events[self.position];
563            #[cfg(feature = "tracing")]
564            tracing::debug!(event = ?timed.event, delay = ?timed.delay, "macro playback event");
565            out.push(timed.event.clone());
566            self.position += 1;
567            if self.position < self.input_macro.len() {
568                self.next_due = self
569                    .next_due
570                    .saturating_add(self.input_macro.events[self.position].delay);
571            } else if can_loop {
572                // Carry any overflow elapsed time into the next loop.
573                self.elapsed = self.elapsed.saturating_sub(total_duration);
574                self.position = 0;
575                self.next_due = self
576                    .input_macro
577                    .events()
578                    .first()
579                    .map(|e| e.delay)
580                    .unwrap_or(Duration::ZERO);
581            }
582        }
583
584        if can_loop && out.len() == MAX_DUE_EVENTS_PER_ADVANCE {
585            // Collapse extreme backlog so a single advance cannot spin for
586            // unbounded time under huge elapsed/speed spikes.
587            self.elapsed = loop_elapsed_remainder(self.elapsed, total_duration);
588            if self.position >= self.input_macro.len() {
589                self.position = 0;
590                self.next_due = self
591                    .input_macro
592                    .events()
593                    .first()
594                    .map(|e| e.delay)
595                    .unwrap_or(Duration::ZERO);
596            }
597        }
598
599        out
600    }
601}
602
603fn normalize_speed(speed: f64) -> f64 {
604    if !speed.is_finite() {
605        return 1.0;
606    }
607    if speed <= 0.0 {
608        return 0.0;
609    }
610    speed
611}
612
613fn scale_duration(delta: Duration, speed: f64) -> Duration {
614    if delta == Duration::ZERO {
615        return Duration::ZERO;
616    }
617    let speed = normalize_speed(speed);
618    if speed == 0.0 {
619        return Duration::ZERO;
620    }
621    if speed == 1.0 {
622        return delta;
623    }
624    duration_from_secs_f64_saturating(delta.as_secs_f64() * speed)
625}
626
627fn duration_from_secs_f64_saturating(secs: f64) -> Duration {
628    if secs.is_nan() || secs <= 0.0 {
629        return Duration::ZERO;
630    }
631    Duration::try_from_secs_f64(secs).unwrap_or(Duration::MAX)
632}
633
634fn loop_elapsed_remainder(elapsed: Duration, total_duration: Duration) -> Duration {
635    let total_secs = total_duration.as_secs_f64();
636    if total_secs <= 0.0 {
637        return Duration::ZERO;
638    }
639    let elapsed_secs = elapsed.as_secs_f64() % total_secs;
640    duration_from_secs_f64_saturating(elapsed_secs)
641}
642
643// ---------------------------------------------------------------------------
644// EventRecorder – live event stream recording with start/stop/pause
645// ---------------------------------------------------------------------------
646
647/// State of an [`EventRecorder`].
648#[derive(Debug, Clone, Copy, PartialEq, Eq)]
649pub enum RecordingState {
650    /// Not yet started or has been stopped.
651    Idle,
652    /// Actively recording events.
653    Recording,
654    /// Temporarily paused (events are ignored).
655    Paused,
656}
657
658/// Records events from a live event stream with start/stop/pause control.
659///
660/// This is a higher-level wrapper around [`MacroRecorder`] designed for
661/// integration with the [`Program`](crate::program::Program) event loop.
662///
663/// # Usage
664///
665/// ```ignore
666/// let mut recorder = EventRecorder::new("my_session");
667/// recorder.start();
668///
669/// // In event loop:
670/// for event in events {
671///     recorder.record(&event);  // No-op if not recording
672///     // ... process event normally ...
673/// }
674///
675/// recorder.pause();
676/// // ... events here are not recorded ...
677/// recorder.resume();
678///
679/// let macro_recording = recorder.finish();
680/// ```
681pub struct EventRecorder {
682    inner: MacroRecorder,
683    state: RecordingState,
684    pause_start: Option<Instant>,
685    total_paused: Duration,
686    event_count: usize,
687}
688
689impl EventRecorder {
690    /// Create a new recorder with the given name.
691    ///
692    /// Starts in [`RecordingState::Idle`]. Call [`start`](Self::start)
693    /// to begin recording.
694    pub fn new(name: impl Into<String>) -> Self {
695        Self {
696            inner: MacroRecorder::new(name),
697            state: RecordingState::Idle,
698            pause_start: None,
699            total_paused: Duration::ZERO,
700            event_count: 0,
701        }
702    }
703
704    /// Set the terminal size metadata.
705    #[must_use]
706    pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
707        self.inner = self.inner.with_terminal_size(width, height);
708        self
709    }
710
711    /// Get the current recording state.
712    pub fn state(&self) -> RecordingState {
713        self.state
714    }
715
716    /// Check if actively recording (not idle or paused).
717    pub fn is_recording(&self) -> bool {
718        self.state == RecordingState::Recording
719    }
720
721    /// Start recording. No-op if already recording.
722    pub fn start(&mut self) {
723        match self.state {
724            RecordingState::Idle => {
725                self.state = RecordingState::Recording;
726                #[cfg(feature = "tracing")]
727                tracing::info!(
728                    macro_event = "recorder_start",
729                    name = %self.inner.name,
730                    term_cols = self.inner.terminal_size.0,
731                    term_rows = self.inner.terminal_size.1,
732                );
733            }
734            RecordingState::Paused => {
735                self.resume();
736            }
737            RecordingState::Recording => {} // Already recording
738        }
739    }
740
741    /// Pause recording. Events received while paused are ignored.
742    ///
743    /// No-op if not recording.
744    pub fn pause(&mut self) {
745        if self.state == RecordingState::Recording {
746            self.state = RecordingState::Paused;
747            self.pause_start = Some(Instant::now());
748        }
749    }
750
751    /// Resume recording after a pause.
752    ///
753    /// No-op if not paused.
754    pub fn resume(&mut self) {
755        if self.state == RecordingState::Paused {
756            if let Some(pause_start) = self.pause_start.take() {
757                self.total_paused += pause_start.elapsed();
758            }
759            // Reset the inner recorder's timestamp so the next event's
760            // delay is measured from the resume instant, not from the
761            // last event before the pause.
762            self.inner.last_event_time = Instant::now();
763            self.state = RecordingState::Recording;
764        }
765    }
766
767    /// Record an event. Only records if state is [`RecordingState::Recording`].
768    ///
769    /// Returns `true` if the event was recorded.
770    pub fn record(&mut self, event: &Event) -> bool {
771        if self.state != RecordingState::Recording {
772            return false;
773        }
774        self.inner.record_event(event.clone());
775        self.event_count += 1;
776        true
777    }
778
779    /// Record an event with an explicit delay override.
780    ///
781    /// Returns `true` if the event was recorded.
782    pub fn record_with_delay(&mut self, event: &Event, delay: Duration) -> bool {
783        if self.state != RecordingState::Recording {
784            return false;
785        }
786        self.inner.record_event_with_delay(event.clone(), delay);
787        self.event_count += 1;
788        true
789    }
790
791    /// Get the number of events recorded so far.
792    pub fn event_count(&self) -> usize {
793        self.event_count
794    }
795
796    /// Get the total time spent paused.
797    pub fn total_paused(&self) -> Duration {
798        let mut total = self.total_paused;
799        if let Some(pause_start) = self.pause_start {
800            total += pause_start.elapsed();
801        }
802        total
803    }
804
805    /// Stop recording and produce the final [`InputMacro`].
806    ///
807    /// Consumes the recorder.
808    pub fn finish(self) -> InputMacro {
809        self.finish_internal(true)
810    }
811
812    #[allow(unused_variables)]
813    fn finish_internal(self, log: bool) -> InputMacro {
814        let paused = self.total_paused();
815        let macro_data = self.inner.finish();
816        #[cfg(feature = "tracing")]
817        if log {
818            let meta = macro_data.metadata();
819            tracing::info!(
820                macro_event = "recorder_stop",
821                name = %meta.name,
822                events = macro_data.len(),
823                duration_ms = macro_data.total_duration().as_millis() as u64,
824                paused_ms = paused.as_millis() as u64,
825                term_cols = meta.terminal_size.0,
826                term_rows = meta.terminal_size.1,
827            );
828        }
829        macro_data
830    }
831
832    /// Stop recording and discard all events.
833    ///
834    /// Returns the number of events that were discarded.
835    pub fn discard(self) -> usize {
836        self.event_count
837    }
838}
839
840/// Filter specification for recording.
841///
842/// Controls which events are recorded. Useful for excluding noise
843/// events (like resize storms or mouse moves) from recordings.
844#[derive(Debug, Clone)]
845pub struct RecordingFilter {
846    /// Record keyboard events.
847    pub keys: bool,
848    /// Record mouse events.
849    pub mouse: bool,
850    /// Record resize events.
851    pub resize: bool,
852    /// Record paste events.
853    pub paste: bool,
854    /// Record focus events.
855    pub focus: bool,
856}
857
858impl Default for RecordingFilter {
859    fn default() -> Self {
860        Self {
861            keys: true,
862            mouse: true,
863            resize: true,
864            paste: true,
865            focus: true,
866        }
867    }
868}
869
870impl RecordingFilter {
871    /// Record only keyboard events.
872    pub fn keys_only() -> Self {
873        Self {
874            keys: true,
875            mouse: false,
876            resize: false,
877            paste: false,
878            focus: false,
879        }
880    }
881
882    /// Check if an event should be recorded.
883    pub fn accepts(&self, event: &Event) -> bool {
884        match event {
885            Event::Key(_) => self.keys,
886            Event::Mouse(_) => self.mouse,
887            Event::Resize { .. } => self.resize,
888            Event::Paste(_) => self.paste,
889            Event::Focus(_) => self.focus,
890            Event::Clipboard(_) => true, // Always record clipboard responses
891            Event::Tick => false,        // Internal timing, not recorded
892        }
893    }
894}
895
896/// A filtered event recorder that only records events matching a filter.
897pub struct FilteredEventRecorder {
898    recorder: EventRecorder,
899    filter: RecordingFilter,
900    filtered_count: usize,
901}
902
903impl FilteredEventRecorder {
904    /// Create a filtered recorder.
905    pub fn new(name: impl Into<String>, filter: RecordingFilter) -> Self {
906        Self {
907            recorder: EventRecorder::new(name),
908            filter,
909            filtered_count: 0,
910        }
911    }
912
913    /// Set terminal size metadata.
914    #[must_use]
915    pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
916        self.recorder = self.recorder.with_terminal_size(width, height);
917        self
918    }
919
920    /// Start recording.
921    pub fn start(&mut self) {
922        self.recorder.start();
923    }
924
925    /// Pause recording.
926    pub fn pause(&mut self) {
927        self.recorder.pause();
928    }
929
930    /// Resume recording.
931    pub fn resume(&mut self) {
932        self.recorder.resume();
933    }
934
935    /// Get current state.
936    pub fn state(&self) -> RecordingState {
937        self.recorder.state()
938    }
939
940    /// Check if actively recording.
941    pub fn is_recording(&self) -> bool {
942        self.recorder.is_recording()
943    }
944
945    /// Record an event if it passes the filter.
946    ///
947    /// Returns `true` if the event was recorded (passed filter and recorder is active).
948    pub fn record(&mut self, event: &Event) -> bool {
949        if !self.filter.accepts(event) {
950            self.filtered_count += 1;
951            return false;
952        }
953        self.recorder.record(event)
954    }
955
956    /// Get the number of events that were filtered out.
957    pub fn filtered_count(&self) -> usize {
958        self.filtered_count
959    }
960
961    /// Get the number of events actually recorded.
962    pub fn event_count(&self) -> usize {
963        self.recorder.event_count()
964    }
965
966    /// Stop recording and produce the final macro.
967    #[allow(unused_variables)]
968    pub fn finish(self) -> InputMacro {
969        let filtered = self.filtered_count;
970        let paused = self.recorder.total_paused();
971        let macro_data = self.recorder.finish_internal(false);
972        #[cfg(feature = "tracing")]
973        {
974            let meta = macro_data.metadata();
975            tracing::info!(
976                macro_event = "recorder_stop",
977                name = %meta.name,
978                events = macro_data.len(),
979                filtered,
980                duration_ms = macro_data.total_duration().as_millis() as u64,
981                paused_ms = paused.as_millis() as u64,
982                term_cols = meta.terminal_size.0,
983                term_rows = meta.terminal_size.1,
984            );
985        }
986        macro_data
987    }
988}
989
990#[cfg(test)]
991mod tests {
992    use super::*;
993    use crate::program::{Cmd, Model};
994    use crate::simulator::ProgramSimulator;
995    use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
996    use ftui_render::frame::Frame;
997    use proptest::prelude::*;
998
999    // ---------- Test model ----------
1000
1001    struct Counter {
1002        value: i32,
1003    }
1004
1005    #[derive(Debug)]
1006    enum CounterMsg {
1007        Increment,
1008        Decrement,
1009        Quit,
1010    }
1011
1012    impl From<Event> for CounterMsg {
1013        fn from(event: Event) -> Self {
1014            match event {
1015                Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
1016                Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
1017                Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
1018                _ => CounterMsg::Increment,
1019            }
1020        }
1021    }
1022
1023    impl Model for Counter {
1024        type Message = CounterMsg;
1025
1026        fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1027            match msg {
1028                CounterMsg::Increment => {
1029                    self.value += 1;
1030                    Cmd::none()
1031                }
1032                CounterMsg::Decrement => {
1033                    self.value -= 1;
1034                    Cmd::none()
1035                }
1036                CounterMsg::Quit => Cmd::quit(),
1037            }
1038        }
1039
1040        fn view(&self, _frame: &mut Frame) {}
1041    }
1042
1043    fn key_event(c: char) -> Event {
1044        Event::Key(KeyEvent {
1045            code: KeyCode::Char(c),
1046            modifiers: Modifiers::empty(),
1047            kind: KeyEventKind::Press,
1048        })
1049    }
1050
1051    // ---------- TimedEvent tests ----------
1052
1053    #[test]
1054    fn timed_event_immediate_has_zero_delay() {
1055        let te = TimedEvent::immediate(key_event('a'));
1056        assert_eq!(te.delay, Duration::ZERO);
1057    }
1058
1059    #[test]
1060    fn timed_event_new_preserves_delay() {
1061        let delay = Duration::from_millis(100);
1062        let te = TimedEvent::new(key_event('x'), delay);
1063        assert_eq!(te.delay, delay);
1064    }
1065
1066    // ---------- InputMacro tests ----------
1067
1068    #[test]
1069    fn macro_from_events_has_zero_delays() {
1070        let m = InputMacro::from_events("test", vec![key_event('+'), key_event('-')]);
1071        assert_eq!(m.len(), 2);
1072        assert!(!m.is_empty());
1073        assert_eq!(m.total_duration(), Duration::ZERO);
1074        for te in m.events() {
1075            assert_eq!(te.delay, Duration::ZERO);
1076        }
1077    }
1078
1079    #[test]
1080    fn macro_metadata() {
1081        let m = InputMacro::from_events("my_macro", vec![key_event('a')]);
1082        assert_eq!(m.metadata().name, "my_macro");
1083        assert_eq!(m.metadata().terminal_size, (80, 24));
1084    }
1085
1086    #[test]
1087    fn empty_macro() {
1088        let m = InputMacro::from_events("empty", vec![]);
1089        assert!(m.is_empty());
1090        assert_eq!(m.len(), 0);
1091    }
1092
1093    #[test]
1094    fn bare_events_extracts_events() {
1095        let events = vec![key_event('+'), key_event('-'), key_event('q')];
1096        let m = InputMacro::from_events("test", events.clone());
1097        let bare = m.bare_events();
1098        assert_eq!(bare.len(), 3);
1099        assert_eq!(bare, events);
1100    }
1101
1102    // ---------- MacroRecorder tests ----------
1103
1104    #[test]
1105    fn recorder_captures_events() {
1106        let mut rec = MacroRecorder::new("rec_test");
1107        rec.record_event(key_event('+'));
1108        rec.record_event(key_event('+'));
1109        rec.record_event(key_event('-'));
1110        assert_eq!(rec.event_count(), 3);
1111
1112        let m = rec.finish();
1113        assert_eq!(m.len(), 3);
1114        assert_eq!(m.metadata().name, "rec_test");
1115    }
1116
1117    #[test]
1118    fn recorder_with_terminal_size() {
1119        let rec = MacroRecorder::new("sized").with_terminal_size(120, 40);
1120        let m = rec.finish();
1121        assert_eq!(m.metadata().terminal_size, (120, 40));
1122    }
1123
1124    #[test]
1125    fn recorder_explicit_delays() {
1126        let mut rec = MacroRecorder::new("delayed");
1127        rec.record_event_with_delay(key_event('+'), Duration::from_millis(0));
1128        rec.record_event_with_delay(key_event('-'), Duration::from_millis(50));
1129        rec.record_event_with_delay(key_event('q'), Duration::from_millis(100));
1130
1131        let m = rec.finish();
1132        assert_eq!(m.events()[0].delay, Duration::from_millis(0));
1133        assert_eq!(m.events()[1].delay, Duration::from_millis(50));
1134        assert_eq!(m.events()[2].delay, Duration::from_millis(100));
1135    }
1136
1137    // ---------- MacroPlayer tests ----------
1138
1139    #[test]
1140    fn player_replays_all_events() {
1141        let m = InputMacro::from_events(
1142            "replay",
1143            vec![key_event('+'), key_event('+'), key_event('+')],
1144        );
1145
1146        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1147        sim.init();
1148
1149        let mut player = MacroPlayer::new(&m);
1150        assert_eq!(player.remaining(), 3);
1151        assert!(!player.is_done());
1152
1153        player.replay_all(&mut sim);
1154
1155        assert!(player.is_done());
1156        assert_eq!(player.remaining(), 0);
1157        assert_eq!(sim.model().value, 3);
1158    }
1159
1160    #[test]
1161    fn player_step_advances_position() {
1162        let m = InputMacro::from_events("step", vec![key_event('+'), key_event('+')]);
1163
1164        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1165        sim.init();
1166
1167        let mut player = MacroPlayer::new(&m);
1168        assert_eq!(player.position(), 0);
1169
1170        assert!(player.step(&mut sim));
1171        assert_eq!(player.position(), 1);
1172        assert_eq!(sim.model().value, 1);
1173
1174        assert!(player.step(&mut sim));
1175        assert_eq!(player.position(), 2);
1176        assert_eq!(sim.model().value, 2);
1177
1178        assert!(!player.step(&mut sim));
1179    }
1180
1181    #[test]
1182    fn player_stops_on_quit() {
1183        let m = InputMacro::from_events(
1184            "quit_test",
1185            vec![key_event('+'), key_event('q'), key_event('+')],
1186        );
1187
1188        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1189        sim.init();
1190
1191        let mut player = MacroPlayer::new(&m);
1192        player.replay_all(&mut sim);
1193
1194        // Only increment and quit processed; third event skipped
1195        assert_eq!(sim.model().value, 1);
1196        assert!(!sim.is_running());
1197    }
1198
1199    #[test]
1200    fn player_replay_until_respects_time() {
1201        let events = vec![
1202            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1203            TimedEvent::new(key_event('+'), Duration::from_millis(20)),
1204            TimedEvent::new(key_event('+'), Duration::from_millis(100)),
1205        ];
1206        let m = InputMacro::new(
1207            events,
1208            MacroMetadata {
1209                name: "timed".to_string(),
1210                terminal_size: (80, 24),
1211                total_duration: Duration::from_millis(130),
1212            },
1213        );
1214
1215        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1216        sim.init();
1217
1218        let mut player = MacroPlayer::new(&m);
1219
1220        // Play events up to 50ms: first two events (10ms + 20ms = 30ms)
1221        player.replay_until(&mut sim, Duration::from_millis(50));
1222        assert_eq!(sim.model().value, 2);
1223        assert_eq!(player.position(), 2);
1224
1225        // Third event at 130ms, play until 200ms
1226        player.replay_until(&mut sim, Duration::from_millis(200));
1227        assert_eq!(sim.model().value, 3);
1228        assert!(player.is_done());
1229    }
1230
1231    #[test]
1232    fn player_elapsed_tracks_virtual_time() {
1233        let events = vec![
1234            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1235            TimedEvent::new(key_event('+'), Duration::from_millis(20)),
1236        ];
1237        let m = InputMacro::new(
1238            events,
1239            MacroMetadata {
1240                name: "elapsed".to_string(),
1241                terminal_size: (80, 24),
1242                total_duration: Duration::from_millis(30),
1243            },
1244        );
1245
1246        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1247        sim.init();
1248
1249        let mut player = MacroPlayer::new(&m);
1250        assert_eq!(player.elapsed(), Duration::ZERO);
1251
1252        player.step(&mut sim);
1253        assert_eq!(player.elapsed(), Duration::from_millis(10));
1254
1255        player.step(&mut sim);
1256        assert_eq!(player.elapsed(), Duration::from_millis(30));
1257    }
1258
1259    #[test]
1260    fn player_reset_restarts_playback() {
1261        let m = InputMacro::from_events("reset", vec![key_event('+'), key_event('+')]);
1262
1263        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1264        sim.init();
1265
1266        let mut player = MacroPlayer::new(&m);
1267        player.replay_all(&mut sim);
1268        assert_eq!(sim.model().value, 2);
1269        assert!(player.is_done());
1270
1271        // Reset player and replay into fresh simulator
1272        player.reset();
1273        assert_eq!(player.position(), 0);
1274        assert!(!player.is_done());
1275
1276        let mut sim2 = ProgramSimulator::new(Counter { value: 10 });
1277        sim2.init();
1278        player.replay_all(&mut sim2);
1279        assert_eq!(sim2.model().value, 12);
1280    }
1281
1282    #[test]
1283    fn player_replay_with_sleeper_respects_delays() {
1284        let events = vec![
1285            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1286            TimedEvent::new(key_event('+'), Duration::from_millis(0)),
1287            TimedEvent::new(key_event('+'), Duration::from_millis(25)),
1288        ];
1289        let m = InputMacro::new(
1290            events,
1291            MacroMetadata {
1292                name: "timed_sleep".to_string(),
1293                terminal_size: (80, 24),
1294                total_duration: Duration::from_millis(35),
1295            },
1296        );
1297
1298        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1299        sim.init();
1300
1301        let mut player = MacroPlayer::new(&m);
1302        let mut sleeps = Vec::new();
1303        player.replay_with_sleeper(&mut sim, |d| sleeps.push(d));
1304
1305        assert_eq!(
1306            sleeps,
1307            vec![Duration::from_millis(10), Duration::from_millis(25)]
1308        );
1309        assert_eq!(sim.model().value, 3);
1310    }
1311
1312    // ---------- MacroPlayback tests ----------
1313
1314    #[test]
1315    fn playback_emits_due_events_in_order() {
1316        let events = vec![
1317            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1318            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1319        ];
1320        let m = InputMacro::new(
1321            events,
1322            MacroMetadata {
1323                name: "playback".to_string(),
1324                terminal_size: (80, 24),
1325                total_duration: Duration::from_millis(20),
1326            },
1327        );
1328
1329        let mut playback = MacroPlayback::new(m.clone());
1330        assert!(playback.advance(Duration::from_millis(5)).is_empty());
1331        let first = playback.advance(Duration::from_millis(5));
1332        assert_eq!(first.len(), 1);
1333        let second = playback.advance(Duration::from_millis(10));
1334        assert_eq!(second.len(), 1);
1335        assert!(playback.advance(Duration::from_millis(10)).is_empty());
1336    }
1337
1338    #[test]
1339    fn playback_speed_scales_time() {
1340        let events = vec![TimedEvent::new(key_event('+'), Duration::from_millis(10))];
1341        let m = InputMacro::new(
1342            events,
1343            MacroMetadata {
1344                name: "speed".to_string(),
1345                terminal_size: (80, 24),
1346                total_duration: Duration::from_millis(10),
1347            },
1348        );
1349
1350        let mut playback = MacroPlayback::new(m.clone()).with_speed(2.0);
1351        let events = playback.advance(Duration::from_millis(5));
1352        assert_eq!(events.len(), 1);
1353    }
1354
1355    #[test]
1356    fn playback_speed_huge_value_does_not_panic() {
1357        let events = vec![TimedEvent::new(key_event('+'), Duration::from_millis(10))];
1358        let m = InputMacro::new(
1359            events,
1360            MacroMetadata {
1361                name: "huge-speed".to_string(),
1362                terminal_size: (80, 24),
1363                total_duration: Duration::from_millis(10),
1364            },
1365        );
1366
1367        let mut playback = MacroPlayback::new(m).with_speed(f64::MAX);
1368        let events = playback.advance(Duration::from_millis(1));
1369        assert_eq!(events.len(), 1);
1370    }
1371
1372    #[test]
1373    fn playback_speed_huge_looping_multiple_advances_do_not_panic() {
1374        let events = vec![TimedEvent::new(key_event('+'), Duration::from_millis(10))];
1375        let m = InputMacro::new(
1376            events,
1377            MacroMetadata {
1378                name: "huge-speed-looping".to_string(),
1379                terminal_size: (80, 24),
1380                total_duration: Duration::from_millis(10),
1381            },
1382        );
1383
1384        let mut playback = MacroPlayback::new(m)
1385            .with_speed(f64::MAX)
1386            .with_looping(true);
1387        let first = playback.advance(Duration::from_millis(1));
1388        assert_eq!(first.len(), 1);
1389        let second = playback.advance(Duration::from_millis(1));
1390        assert_eq!(second.len(), 1);
1391    }
1392
1393    #[test]
1394    fn playback_looping_handles_large_delta() {
1395        let events = vec![
1396            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1397            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1398        ];
1399        let m = InputMacro::new(
1400            events,
1401            MacroMetadata {
1402                name: "loop".to_string(),
1403                terminal_size: (80, 24),
1404                total_duration: Duration::from_millis(20),
1405            },
1406        );
1407
1408        let mut playback = MacroPlayback::new(m.clone()).with_looping(true);
1409        let events = playback.advance(Duration::from_millis(50));
1410        assert_eq!(events.len(), 5);
1411    }
1412
1413    #[test]
1414    fn playback_zero_duration_does_not_loop_forever() {
1415        let m = InputMacro::from_events("zero", vec![key_event('+'), key_event('+')]);
1416        let mut playback = MacroPlayback::new(m.clone()).with_looping(true);
1417
1418        let events = playback.advance(Duration::ZERO);
1419        assert_eq!(events.len(), 2);
1420        assert!(playback.advance(Duration::from_millis(10)).is_empty());
1421    }
1422
1423    #[test]
1424    fn macro_replay_with_sleeper_wrapper() {
1425        let events = vec![
1426            TimedEvent::new(key_event('+'), Duration::from_millis(5)),
1427            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1428        ];
1429        let m = InputMacro::new(
1430            events,
1431            MacroMetadata {
1432                name: "wrapper".to_string(),
1433                terminal_size: (80, 24),
1434                total_duration: Duration::from_millis(15),
1435            },
1436        );
1437
1438        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1439        sim.init();
1440
1441        let mut slept = Vec::new();
1442        m.replay_with_sleeper(&mut sim, |d| slept.push(d));
1443
1444        assert_eq!(
1445            slept,
1446            vec![Duration::from_millis(5), Duration::from_millis(10)]
1447        );
1448        assert_eq!(sim.model().value, 2);
1449    }
1450
1451    #[test]
1452    fn empty_macro_replay() {
1453        let m = InputMacro::from_events("empty", vec![]);
1454
1455        let mut sim = ProgramSimulator::new(Counter { value: 5 });
1456        sim.init();
1457
1458        let mut player = MacroPlayer::new(&m);
1459        assert!(player.is_done());
1460        player.replay_all(&mut sim);
1461        assert_eq!(sim.model().value, 5);
1462    }
1463
1464    #[test]
1465    fn macro_with_mixed_events() {
1466        let events = vec![
1467            key_event('+'),
1468            Event::Resize {
1469                width: 100,
1470                height: 50,
1471            },
1472            key_event('-'),
1473            Event::Focus(true),
1474            key_event('+'),
1475        ];
1476        let m = InputMacro::from_events("mixed", events);
1477
1478        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1479        sim.init();
1480
1481        let mut player = MacroPlayer::new(&m);
1482        player.replay_all(&mut sim);
1483
1484        // +1, resize->increment, -1, focus->increment, +1 = 3
1485        // (Counter converts all non-matching events to Increment)
1486        assert_eq!(sim.model().value, 3);
1487    }
1488
1489    #[test]
1490    fn deterministic_replay() {
1491        let m = InputMacro::from_events(
1492            "determinism",
1493            vec![
1494                key_event('+'),
1495                key_event('+'),
1496                key_event('-'),
1497                key_event('+'),
1498                key_event('+'),
1499            ],
1500        );
1501
1502        // Replay twice and verify identical results
1503        let result1 = {
1504            let mut sim = ProgramSimulator::new(Counter { value: 0 });
1505            sim.init();
1506            MacroPlayer::new(&m).replay_all(&mut sim);
1507            sim.model().value
1508        };
1509
1510        let result2 = {
1511            let mut sim = ProgramSimulator::new(Counter { value: 0 });
1512            sim.init();
1513            MacroPlayer::new(&m).replay_all(&mut sim);
1514            sim.model().value
1515        };
1516
1517        assert_eq!(result1, result2);
1518        assert_eq!(result1, 3);
1519    }
1520
1521    // ---------- EventRecorder tests ----------
1522
1523    #[test]
1524    fn event_recorder_starts_idle() {
1525        let rec = EventRecorder::new("test");
1526        assert_eq!(rec.state(), RecordingState::Idle);
1527        assert!(!rec.is_recording());
1528        assert_eq!(rec.event_count(), 0);
1529    }
1530
1531    #[test]
1532    fn event_recorder_start_activates() {
1533        let mut rec = EventRecorder::new("test");
1534        rec.start();
1535        assert_eq!(rec.state(), RecordingState::Recording);
1536        assert!(rec.is_recording());
1537    }
1538
1539    #[test]
1540    fn event_recorder_ignores_events_when_idle() {
1541        let mut rec = EventRecorder::new("test");
1542        assert!(!rec.record(&key_event('a')));
1543        assert_eq!(rec.event_count(), 0);
1544    }
1545
1546    #[test]
1547    fn event_recorder_records_when_active() {
1548        let mut rec = EventRecorder::new("test");
1549        rec.start();
1550        assert!(rec.record(&key_event('a')));
1551        assert!(rec.record(&key_event('b')));
1552        assert_eq!(rec.event_count(), 2);
1553
1554        let m = rec.finish();
1555        assert_eq!(m.len(), 2);
1556    }
1557
1558    #[test]
1559    fn event_recorder_pause_ignores_events() {
1560        let mut rec = EventRecorder::new("test");
1561        rec.start();
1562        rec.record(&key_event('a'));
1563        rec.pause();
1564        assert_eq!(rec.state(), RecordingState::Paused);
1565        assert!(!rec.is_recording());
1566
1567        // Events during pause are ignored
1568        assert!(!rec.record(&key_event('b')));
1569        assert_eq!(rec.event_count(), 1);
1570    }
1571
1572    #[test]
1573    fn event_recorder_resume_after_pause() {
1574        let mut rec = EventRecorder::new("test");
1575        rec.start();
1576        rec.record(&key_event('a'));
1577        rec.pause();
1578        rec.record(&key_event('b')); // ignored
1579        rec.resume();
1580        assert!(rec.is_recording());
1581        rec.record(&key_event('c'));
1582        assert_eq!(rec.event_count(), 2);
1583
1584        let m = rec.finish();
1585        assert_eq!(m.len(), 2);
1586        assert_eq!(m.bare_events()[0], key_event('a'));
1587        assert_eq!(m.bare_events()[1], key_event('c'));
1588    }
1589
1590    #[test]
1591    fn event_recorder_start_resumes_when_paused() {
1592        let mut rec = EventRecorder::new("test");
1593        rec.start();
1594        rec.pause();
1595        assert_eq!(rec.state(), RecordingState::Paused);
1596
1597        rec.start(); // Should resume
1598        assert_eq!(rec.state(), RecordingState::Recording);
1599    }
1600
1601    #[test]
1602    fn event_recorder_pause_noop_when_idle() {
1603        let mut rec = EventRecorder::new("test");
1604        rec.pause();
1605        assert_eq!(rec.state(), RecordingState::Idle);
1606    }
1607
1608    #[test]
1609    fn event_recorder_resume_noop_when_idle() {
1610        let mut rec = EventRecorder::new("test");
1611        rec.resume();
1612        assert_eq!(rec.state(), RecordingState::Idle);
1613    }
1614
1615    #[test]
1616    fn event_recorder_discard() {
1617        let mut rec = EventRecorder::new("test");
1618        rec.start();
1619        rec.record(&key_event('a'));
1620        rec.record(&key_event('b'));
1621        let count = rec.discard();
1622        assert_eq!(count, 2);
1623    }
1624
1625    #[test]
1626    fn event_recorder_with_terminal_size() {
1627        let mut rec = EventRecorder::new("sized").with_terminal_size(120, 40);
1628        rec.start();
1629        rec.record(&key_event('x'));
1630        let m = rec.finish();
1631        assert_eq!(m.metadata().terminal_size, (120, 40));
1632    }
1633
1634    #[test]
1635    fn event_recorder_finish_produces_valid_macro() {
1636        let mut rec = EventRecorder::new("full_test");
1637        rec.start();
1638        rec.record(&key_event('+'));
1639        rec.record(&key_event('+'));
1640        rec.record(&key_event('-'));
1641
1642        let m = rec.finish();
1643        assert_eq!(m.len(), 3);
1644        assert_eq!(m.metadata().name, "full_test");
1645
1646        // Replay and verify
1647        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1648        sim.init();
1649        MacroPlayer::new(&m).replay_all(&mut sim);
1650        assert_eq!(sim.model().value, 1); // +1 +1 -1 = 1
1651    }
1652
1653    #[test]
1654    fn event_recorder_record_with_delay() {
1655        let mut rec = EventRecorder::new("delayed");
1656        rec.start();
1657        assert!(rec.record_with_delay(&key_event('a'), Duration::from_millis(50)));
1658        assert!(rec.record_with_delay(&key_event('b'), Duration::from_millis(100)));
1659        assert_eq!(rec.event_count(), 2);
1660
1661        let m = rec.finish();
1662        assert_eq!(m.events()[0].delay, Duration::from_millis(50));
1663        assert_eq!(m.events()[1].delay, Duration::from_millis(100));
1664    }
1665
1666    #[test]
1667    fn event_recorder_record_with_delay_ignores_when_idle() {
1668        let mut rec = EventRecorder::new("test");
1669        assert!(!rec.record_with_delay(&key_event('a'), Duration::from_millis(50)));
1670        assert_eq!(rec.event_count(), 0);
1671    }
1672
1673    // ---------- RecordingFilter tests ----------
1674
1675    #[test]
1676    fn filter_default_accepts_all() {
1677        let filter = RecordingFilter::default();
1678        assert!(filter.accepts(&key_event('a')));
1679        assert!(filter.accepts(&Event::Resize {
1680            width: 80,
1681            height: 24
1682        }));
1683        assert!(filter.accepts(&Event::Focus(true)));
1684    }
1685
1686    #[test]
1687    fn filter_keys_only() {
1688        let filter = RecordingFilter::keys_only();
1689        assert!(filter.accepts(&key_event('a')));
1690        assert!(!filter.accepts(&Event::Resize {
1691            width: 80,
1692            height: 24
1693        }));
1694        assert!(!filter.accepts(&Event::Focus(true)));
1695    }
1696
1697    #[test]
1698    fn filter_custom() {
1699        let filter = RecordingFilter {
1700            keys: true,
1701            mouse: false,
1702            resize: false,
1703            paste: true,
1704            focus: false,
1705        };
1706        assert!(filter.accepts(&key_event('a')));
1707        assert!(!filter.accepts(&Event::Resize {
1708            width: 80,
1709            height: 24
1710        }));
1711        assert!(!filter.accepts(&Event::Focus(false)));
1712    }
1713
1714    // ---------- FilteredEventRecorder tests ----------
1715
1716    #[test]
1717    fn filtered_recorder_records_matching_events() {
1718        let mut rec = FilteredEventRecorder::new("filtered", RecordingFilter::default());
1719        rec.start();
1720        assert!(rec.record(&key_event('a')));
1721        assert_eq!(rec.event_count(), 1);
1722        assert_eq!(rec.filtered_count(), 0);
1723    }
1724
1725    #[test]
1726    fn filtered_recorder_skips_filtered_events() {
1727        let mut rec = FilteredEventRecorder::new("keys_only", RecordingFilter::keys_only());
1728        rec.start();
1729        assert!(rec.record(&key_event('a')));
1730        assert!(!rec.record(&Event::Focus(true)));
1731        assert!(!rec.record(&Event::Resize {
1732            width: 100,
1733            height: 50
1734        }));
1735        assert!(rec.record(&key_event('b')));
1736
1737        assert_eq!(rec.event_count(), 2);
1738        assert_eq!(rec.filtered_count(), 2);
1739    }
1740
1741    #[test]
1742    fn filtered_recorder_finish_produces_macro() {
1743        let mut rec = FilteredEventRecorder::new("test", RecordingFilter::keys_only());
1744        rec.start();
1745        rec.record(&key_event('+'));
1746        rec.record(&Event::Focus(true)); // filtered
1747        rec.record(&key_event('+'));
1748
1749        let m = rec.finish();
1750        assert_eq!(m.len(), 2);
1751
1752        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1753        sim.init();
1754        MacroPlayer::new(&m).replay_all(&mut sim);
1755        assert_eq!(sim.model().value, 2);
1756    }
1757
1758    #[test]
1759    fn filtered_recorder_pause_resume() {
1760        let mut rec = FilteredEventRecorder::new("test", RecordingFilter::default());
1761        rec.start();
1762        rec.record(&key_event('a'));
1763        rec.pause();
1764        assert!(!rec.record(&key_event('b'))); // paused
1765        rec.resume();
1766        rec.record(&key_event('c'));
1767        assert_eq!(rec.event_count(), 2);
1768    }
1769
1770    #[test]
1771    fn filtered_recorder_with_terminal_size() {
1772        let mut rec = FilteredEventRecorder::new("sized", RecordingFilter::default())
1773            .with_terminal_size(200, 60);
1774        rec.start();
1775        rec.record(&key_event('x'));
1776        let m = rec.finish();
1777        assert_eq!(m.metadata().terminal_size, (200, 60));
1778    }
1779
1780    // ---------- Property tests ----------
1781
1782    #[derive(Default)]
1783    struct EventSink {
1784        events: Vec<Event>,
1785    }
1786
1787    #[derive(Debug, Clone)]
1788    struct EventMsg(Event);
1789
1790    impl From<Event> for EventMsg {
1791        fn from(event: Event) -> Self {
1792            Self(event)
1793        }
1794    }
1795
1796    impl Model for EventSink {
1797        type Message = EventMsg;
1798
1799        fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1800            self.events.push(msg.0);
1801            Cmd::none()
1802        }
1803
1804        fn view(&self, _frame: &mut Frame) {}
1805    }
1806
1807    proptest! {
1808        #[test]
1809        fn recorder_with_explicit_delays_roundtrips(pairs in proptest::collection::vec((0u8..=25, 0u16..=2000), 0..32)) {
1810            let mut recorder = MacroRecorder::new("prop").with_terminal_size(80, 24);
1811            let mut expected_total = Duration::ZERO;
1812            let mut expected_events = Vec::with_capacity(pairs.len());
1813
1814            for (ch_idx, delay_ms) in &pairs {
1815                let ch = char::from(b'a' + *ch_idx);
1816                let delay = Duration::from_millis(*delay_ms as u64);
1817                expected_total += delay;
1818                let ev = key_event(ch);
1819                expected_events.push(ev.clone());
1820                recorder.record_event_with_delay(ev, delay);
1821            }
1822
1823            let m = recorder.finish();
1824            prop_assert_eq!(m.len(), pairs.len());
1825            prop_assert_eq!(m.metadata().terminal_size, (80, 24));
1826            prop_assert_eq!(m.total_duration(), expected_total);
1827            prop_assert_eq!(m.bare_events(), expected_events);
1828        }
1829
1830        #[test]
1831        fn player_replays_events_in_order(pairs in proptest::collection::vec((0u8..=25, 0u16..=2000), 0..32)) {
1832            let mut timed = Vec::with_capacity(pairs.len());
1833            let mut total = Duration::ZERO;
1834            let mut expected_events = Vec::with_capacity(pairs.len());
1835
1836            for (ch_idx, delay_ms) in &pairs {
1837                let ch = char::from(b'a' + *ch_idx);
1838                let delay = Duration::from_millis(*delay_ms as u64);
1839                total += delay;
1840                let ev = key_event(ch);
1841                expected_events.push(ev.clone());
1842                timed.push(TimedEvent::new(ev, delay));
1843            }
1844
1845            let m = InputMacro::new(timed, MacroMetadata {
1846                name: "prop".to_string(),
1847                terminal_size: (80, 24),
1848                total_duration: total,
1849            });
1850
1851            let mut sim = ProgramSimulator::new(EventSink::default());
1852            sim.init();
1853            let mut player = MacroPlayer::new(&m);
1854            player.replay_all(&mut sim);
1855
1856            prop_assert_eq!(sim.model().events.clone(), expected_events);
1857            prop_assert_eq!(player.elapsed(), total);
1858        }
1859    }
1860}