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 += 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 + 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
389impl MacroPlayback {
390    /// Create a new playback scheduler for the given macro.
391    pub fn new(input_macro: InputMacro) -> Self {
392        let next_due = input_macro
393            .events()
394            .first()
395            .map(|e| e.delay)
396            .unwrap_or(Duration::ZERO);
397        Self {
398            input_macro,
399            position: 0,
400            elapsed: Duration::ZERO,
401            next_due,
402            speed: 1.0,
403            looping: false,
404            start_logged: false,
405            stop_logged: false,
406            error_logged: false,
407        }
408    }
409
410    /// Set playback speed (must be finite and positive).
411    pub fn set_speed(&mut self, speed: f64) {
412        self.speed = normalize_speed(speed);
413    }
414
415    /// Fluent speed setter.
416    #[must_use]
417    pub fn with_speed(mut self, speed: f64) -> Self {
418        self.set_speed(speed);
419        self
420    }
421
422    /// Enable or disable looping.
423    pub fn set_looping(&mut self, looping: bool) {
424        self.looping = looping;
425    }
426
427    /// Fluent looping setter.
428    #[must_use]
429    pub fn with_looping(mut self, looping: bool) -> Self {
430        self.set_looping(looping);
431        self
432    }
433
434    /// Get the current playback speed.
435    pub fn speed(&self) -> f64 {
436        self.speed
437    }
438
439    /// Get current playback position (event index).
440    pub fn position(&self) -> usize {
441        self.position
442    }
443
444    /// Get elapsed virtual time.
445    pub fn elapsed(&self) -> Duration {
446        self.elapsed
447    }
448
449    /// Check if playback is complete (non-looping).
450    pub fn is_done(&self) -> bool {
451        if self.input_macro.is_empty() {
452            return true;
453        }
454        if self.looping && self.input_macro.total_duration() > Duration::ZERO {
455            return false;
456        }
457        self.position >= self.input_macro.len()
458    }
459
460    /// Reset playback to the beginning.
461    pub fn reset(&mut self) {
462        self.position = 0;
463        self.elapsed = Duration::ZERO;
464        self.next_due = self
465            .input_macro
466            .events()
467            .first()
468            .map(|e| e.delay)
469            .unwrap_or(Duration::ZERO);
470        self.start_logged = false;
471        self.stop_logged = false;
472        self.error_logged = false;
473    }
474
475    /// Advance playback time and return any events now due.
476    pub fn advance(&mut self, delta: Duration) -> Vec<Event> {
477        if self.input_macro.is_empty() {
478            #[cfg(feature = "tracing")]
479            if !self.error_logged {
480                let meta = self.input_macro.metadata();
481                tracing::warn!(
482                    macro_event = "playback_error",
483                    reason = "macro_empty",
484                    name = %meta.name,
485                    events = 0usize,
486                    duration_ms = self.input_macro.total_duration().as_millis() as u64,
487                );
488                self.error_logged = true;
489            }
490            return Vec::new();
491        }
492        if self.is_done() {
493            return Vec::new();
494        }
495
496        #[cfg(feature = "tracing")]
497        if !self.start_logged {
498            let meta = self.input_macro.metadata();
499            tracing::info!(
500                macro_event = "playback_start",
501                name = %meta.name,
502                events = self.input_macro.len(),
503                duration_ms = self.input_macro.total_duration().as_millis() as u64,
504                speed = self.speed,
505                looping = self.looping,
506            );
507            self.start_logged = true;
508        }
509
510        self.elapsed += scale_duration(delta, self.speed);
511        let events = self.drain_due_events();
512
513        #[cfg(feature = "tracing")]
514        if self.is_done() && !self.stop_logged {
515            let meta = self.input_macro.metadata();
516            tracing::info!(
517                macro_event = "playback_stop",
518                reason = "completed",
519                name = %meta.name,
520                events = self.input_macro.len(),
521                elapsed_ms = self.elapsed.as_millis() as u64,
522                looping = self.looping,
523            );
524            self.stop_logged = true;
525        }
526
527        events
528    }
529
530    fn drain_due_events(&mut self) -> Vec<Event> {
531        let mut out = Vec::new();
532        let total_duration = self.input_macro.total_duration();
533        let can_loop = self.looping && total_duration > Duration::ZERO;
534        if can_loop && self.position >= self.input_macro.len() {
535            let total_secs = total_duration.as_secs_f64();
536            if total_secs > 0.0 {
537                let elapsed_secs = self.elapsed.as_secs_f64() % total_secs;
538                self.elapsed = Duration::from_secs_f64(elapsed_secs);
539            } else {
540                self.elapsed = Duration::ZERO;
541            }
542            self.position = 0;
543            self.next_due = self
544                .input_macro
545                .events()
546                .first()
547                .map(|e| e.delay)
548                .unwrap_or(Duration::ZERO);
549        }
550
551        while self.position < self.input_macro.len() && self.elapsed >= self.next_due {
552            let timed = &self.input_macro.events[self.position];
553            #[cfg(feature = "tracing")]
554            tracing::debug!(event = ?timed.event, delay = ?timed.delay, "macro playback event");
555            out.push(timed.event.clone());
556            self.position += 1;
557            if self.position < self.input_macro.len() {
558                self.next_due += self.input_macro.events[self.position].delay;
559            } else if can_loop {
560                // Carry any overflow elapsed time into the next loop.
561                self.elapsed = self.elapsed.saturating_sub(total_duration);
562                self.position = 0;
563                self.next_due = self
564                    .input_macro
565                    .events()
566                    .first()
567                    .map(|e| e.delay)
568                    .unwrap_or(Duration::ZERO);
569            }
570        }
571
572        out
573    }
574}
575
576fn normalize_speed(speed: f64) -> f64 {
577    if !speed.is_finite() {
578        return 1.0;
579    }
580    if speed <= 0.0 {
581        return 0.0;
582    }
583    speed
584}
585
586fn scale_duration(delta: Duration, speed: f64) -> Duration {
587    if delta == Duration::ZERO {
588        return Duration::ZERO;
589    }
590    let speed = normalize_speed(speed);
591    if speed == 1.0 {
592        return delta;
593    }
594    Duration::from_secs_f64(delta.as_secs_f64() * speed)
595}
596
597// ---------------------------------------------------------------------------
598// EventRecorder – live event stream recording with start/stop/pause
599// ---------------------------------------------------------------------------
600
601/// State of an [`EventRecorder`].
602#[derive(Debug, Clone, Copy, PartialEq, Eq)]
603pub enum RecordingState {
604    /// Not yet started or has been stopped.
605    Idle,
606    /// Actively recording events.
607    Recording,
608    /// Temporarily paused (events are ignored).
609    Paused,
610}
611
612/// Records events from a live event stream with start/stop/pause control.
613///
614/// This is a higher-level wrapper around [`MacroRecorder`] designed for
615/// integration with the [`Program`](crate::program::Program) event loop.
616///
617/// # Usage
618///
619/// ```ignore
620/// let mut recorder = EventRecorder::new("my_session");
621/// recorder.start();
622///
623/// // In event loop:
624/// for event in events {
625///     recorder.record(&event);  // No-op if not recording
626///     // ... process event normally ...
627/// }
628///
629/// recorder.pause();
630/// // ... events here are not recorded ...
631/// recorder.resume();
632///
633/// let macro_recording = recorder.finish();
634/// ```
635pub struct EventRecorder {
636    inner: MacroRecorder,
637    state: RecordingState,
638    pause_start: Option<Instant>,
639    total_paused: Duration,
640    event_count: usize,
641}
642
643impl EventRecorder {
644    /// Create a new recorder with the given name.
645    ///
646    /// Starts in [`RecordingState::Idle`]. Call [`start`](Self::start)
647    /// to begin recording.
648    pub fn new(name: impl Into<String>) -> Self {
649        Self {
650            inner: MacroRecorder::new(name),
651            state: RecordingState::Idle,
652            pause_start: None,
653            total_paused: Duration::ZERO,
654            event_count: 0,
655        }
656    }
657
658    /// Set the terminal size metadata.
659    #[must_use]
660    pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
661        self.inner = self.inner.with_terminal_size(width, height);
662        self
663    }
664
665    /// Get the current recording state.
666    pub fn state(&self) -> RecordingState {
667        self.state
668    }
669
670    /// Check if actively recording (not idle or paused).
671    pub fn is_recording(&self) -> bool {
672        self.state == RecordingState::Recording
673    }
674
675    /// Start recording. No-op if already recording.
676    pub fn start(&mut self) {
677        match self.state {
678            RecordingState::Idle => {
679                self.state = RecordingState::Recording;
680                #[cfg(feature = "tracing")]
681                tracing::info!(
682                    macro_event = "recorder_start",
683                    name = %self.inner.name,
684                    term_cols = self.inner.terminal_size.0,
685                    term_rows = self.inner.terminal_size.1,
686                );
687            }
688            RecordingState::Paused => {
689                self.resume();
690            }
691            RecordingState::Recording => {} // Already recording
692        }
693    }
694
695    /// Pause recording. Events received while paused are ignored.
696    ///
697    /// No-op if not recording.
698    pub fn pause(&mut self) {
699        if self.state == RecordingState::Recording {
700            self.state = RecordingState::Paused;
701            self.pause_start = Some(Instant::now());
702        }
703    }
704
705    /// Resume recording after a pause.
706    ///
707    /// No-op if not paused.
708    pub fn resume(&mut self) {
709        if self.state == RecordingState::Paused {
710            if let Some(pause_start) = self.pause_start.take() {
711                self.total_paused += pause_start.elapsed();
712            }
713            // Reset the inner recorder's timestamp so the next event's
714            // delay is measured from the resume instant, not from the
715            // last event before the pause.
716            self.inner.last_event_time = Instant::now();
717            self.state = RecordingState::Recording;
718        }
719    }
720
721    /// Record an event. Only records if state is [`RecordingState::Recording`].
722    ///
723    /// Returns `true` if the event was recorded.
724    pub fn record(&mut self, event: &Event) -> bool {
725        if self.state != RecordingState::Recording {
726            return false;
727        }
728        self.inner.record_event(event.clone());
729        self.event_count += 1;
730        true
731    }
732
733    /// Record an event with an explicit delay override.
734    ///
735    /// Returns `true` if the event was recorded.
736    pub fn record_with_delay(&mut self, event: &Event, delay: Duration) -> bool {
737        if self.state != RecordingState::Recording {
738            return false;
739        }
740        self.inner.record_event_with_delay(event.clone(), delay);
741        self.event_count += 1;
742        true
743    }
744
745    /// Get the number of events recorded so far.
746    pub fn event_count(&self) -> usize {
747        self.event_count
748    }
749
750    /// Get the total time spent paused.
751    pub fn total_paused(&self) -> Duration {
752        let mut total = self.total_paused;
753        if let Some(pause_start) = self.pause_start {
754            total += pause_start.elapsed();
755        }
756        total
757    }
758
759    /// Stop recording and produce the final [`InputMacro`].
760    ///
761    /// Consumes the recorder.
762    pub fn finish(self) -> InputMacro {
763        self.finish_internal(true)
764    }
765
766    #[allow(unused_variables)]
767    fn finish_internal(self, log: bool) -> InputMacro {
768        let paused = self.total_paused();
769        let macro_data = self.inner.finish();
770        #[cfg(feature = "tracing")]
771        if log {
772            let meta = macro_data.metadata();
773            tracing::info!(
774                macro_event = "recorder_stop",
775                name = %meta.name,
776                events = macro_data.len(),
777                duration_ms = macro_data.total_duration().as_millis() as u64,
778                paused_ms = paused.as_millis() as u64,
779                term_cols = meta.terminal_size.0,
780                term_rows = meta.terminal_size.1,
781            );
782        }
783        macro_data
784    }
785
786    /// Stop recording and discard all events.
787    ///
788    /// Returns the number of events that were discarded.
789    pub fn discard(self) -> usize {
790        self.event_count
791    }
792}
793
794/// Filter specification for recording.
795///
796/// Controls which events are recorded. Useful for excluding noise
797/// events (like resize storms or mouse moves) from recordings.
798#[derive(Debug, Clone)]
799pub struct RecordingFilter {
800    /// Record keyboard events.
801    pub keys: bool,
802    /// Record mouse events.
803    pub mouse: bool,
804    /// Record resize events.
805    pub resize: bool,
806    /// Record paste events.
807    pub paste: bool,
808    /// Record focus events.
809    pub focus: bool,
810}
811
812impl Default for RecordingFilter {
813    fn default() -> Self {
814        Self {
815            keys: true,
816            mouse: true,
817            resize: true,
818            paste: true,
819            focus: true,
820        }
821    }
822}
823
824impl RecordingFilter {
825    /// Record only keyboard events.
826    pub fn keys_only() -> Self {
827        Self {
828            keys: true,
829            mouse: false,
830            resize: false,
831            paste: false,
832            focus: false,
833        }
834    }
835
836    /// Check if an event should be recorded.
837    pub fn accepts(&self, event: &Event) -> bool {
838        match event {
839            Event::Key(_) => self.keys,
840            Event::Mouse(_) => self.mouse,
841            Event::Resize { .. } => self.resize,
842            Event::Paste(_) => self.paste,
843            Event::Focus(_) => self.focus,
844            Event::Clipboard(_) => true, // Always record clipboard responses
845            Event::Tick => false,        // Internal timing, not recorded
846        }
847    }
848}
849
850/// A filtered event recorder that only records events matching a filter.
851pub struct FilteredEventRecorder {
852    recorder: EventRecorder,
853    filter: RecordingFilter,
854    filtered_count: usize,
855}
856
857impl FilteredEventRecorder {
858    /// Create a filtered recorder.
859    pub fn new(name: impl Into<String>, filter: RecordingFilter) -> Self {
860        Self {
861            recorder: EventRecorder::new(name),
862            filter,
863            filtered_count: 0,
864        }
865    }
866
867    /// Set terminal size metadata.
868    #[must_use]
869    pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
870        self.recorder = self.recorder.with_terminal_size(width, height);
871        self
872    }
873
874    /// Start recording.
875    pub fn start(&mut self) {
876        self.recorder.start();
877    }
878
879    /// Pause recording.
880    pub fn pause(&mut self) {
881        self.recorder.pause();
882    }
883
884    /// Resume recording.
885    pub fn resume(&mut self) {
886        self.recorder.resume();
887    }
888
889    /// Get current state.
890    pub fn state(&self) -> RecordingState {
891        self.recorder.state()
892    }
893
894    /// Check if actively recording.
895    pub fn is_recording(&self) -> bool {
896        self.recorder.is_recording()
897    }
898
899    /// Record an event if it passes the filter.
900    ///
901    /// Returns `true` if the event was recorded (passed filter and recorder is active).
902    pub fn record(&mut self, event: &Event) -> bool {
903        if !self.filter.accepts(event) {
904            self.filtered_count += 1;
905            return false;
906        }
907        self.recorder.record(event)
908    }
909
910    /// Get the number of events that were filtered out.
911    pub fn filtered_count(&self) -> usize {
912        self.filtered_count
913    }
914
915    /// Get the number of events actually recorded.
916    pub fn event_count(&self) -> usize {
917        self.recorder.event_count()
918    }
919
920    /// Stop recording and produce the final macro.
921    #[allow(unused_variables)]
922    pub fn finish(self) -> InputMacro {
923        let filtered = self.filtered_count;
924        let paused = self.recorder.total_paused();
925        let macro_data = self.recorder.finish_internal(false);
926        #[cfg(feature = "tracing")]
927        {
928            let meta = macro_data.metadata();
929            tracing::info!(
930                macro_event = "recorder_stop",
931                name = %meta.name,
932                events = macro_data.len(),
933                filtered,
934                duration_ms = macro_data.total_duration().as_millis() as u64,
935                paused_ms = paused.as_millis() as u64,
936                term_cols = meta.terminal_size.0,
937                term_rows = meta.terminal_size.1,
938            );
939        }
940        macro_data
941    }
942}
943
944#[cfg(test)]
945mod tests {
946    use super::*;
947    use crate::program::{Cmd, Model};
948    use crate::simulator::ProgramSimulator;
949    use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
950    use ftui_render::frame::Frame;
951    use proptest::prelude::*;
952
953    // ---------- Test model ----------
954
955    struct Counter {
956        value: i32,
957    }
958
959    #[derive(Debug)]
960    enum CounterMsg {
961        Increment,
962        Decrement,
963        Quit,
964    }
965
966    impl From<Event> for CounterMsg {
967        fn from(event: Event) -> Self {
968            match event {
969                Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
970                Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
971                Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
972                _ => CounterMsg::Increment,
973            }
974        }
975    }
976
977    impl Model for Counter {
978        type Message = CounterMsg;
979
980        fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
981            match msg {
982                CounterMsg::Increment => {
983                    self.value += 1;
984                    Cmd::none()
985                }
986                CounterMsg::Decrement => {
987                    self.value -= 1;
988                    Cmd::none()
989                }
990                CounterMsg::Quit => Cmd::quit(),
991            }
992        }
993
994        fn view(&self, _frame: &mut Frame) {}
995    }
996
997    fn key_event(c: char) -> Event {
998        Event::Key(KeyEvent {
999            code: KeyCode::Char(c),
1000            modifiers: Modifiers::empty(),
1001            kind: KeyEventKind::Press,
1002        })
1003    }
1004
1005    // ---------- TimedEvent tests ----------
1006
1007    #[test]
1008    fn timed_event_immediate_has_zero_delay() {
1009        let te = TimedEvent::immediate(key_event('a'));
1010        assert_eq!(te.delay, Duration::ZERO);
1011    }
1012
1013    #[test]
1014    fn timed_event_new_preserves_delay() {
1015        let delay = Duration::from_millis(100);
1016        let te = TimedEvent::new(key_event('x'), delay);
1017        assert_eq!(te.delay, delay);
1018    }
1019
1020    // ---------- InputMacro tests ----------
1021
1022    #[test]
1023    fn macro_from_events_has_zero_delays() {
1024        let m = InputMacro::from_events("test", vec![key_event('+'), key_event('-')]);
1025        assert_eq!(m.len(), 2);
1026        assert!(!m.is_empty());
1027        assert_eq!(m.total_duration(), Duration::ZERO);
1028        for te in m.events() {
1029            assert_eq!(te.delay, Duration::ZERO);
1030        }
1031    }
1032
1033    #[test]
1034    fn macro_metadata() {
1035        let m = InputMacro::from_events("my_macro", vec![key_event('a')]);
1036        assert_eq!(m.metadata().name, "my_macro");
1037        assert_eq!(m.metadata().terminal_size, (80, 24));
1038    }
1039
1040    #[test]
1041    fn empty_macro() {
1042        let m = InputMacro::from_events("empty", vec![]);
1043        assert!(m.is_empty());
1044        assert_eq!(m.len(), 0);
1045    }
1046
1047    #[test]
1048    fn bare_events_extracts_events() {
1049        let events = vec![key_event('+'), key_event('-'), key_event('q')];
1050        let m = InputMacro::from_events("test", events.clone());
1051        let bare = m.bare_events();
1052        assert_eq!(bare.len(), 3);
1053        assert_eq!(bare, events);
1054    }
1055
1056    // ---------- MacroRecorder tests ----------
1057
1058    #[test]
1059    fn recorder_captures_events() {
1060        let mut rec = MacroRecorder::new("rec_test");
1061        rec.record_event(key_event('+'));
1062        rec.record_event(key_event('+'));
1063        rec.record_event(key_event('-'));
1064        assert_eq!(rec.event_count(), 3);
1065
1066        let m = rec.finish();
1067        assert_eq!(m.len(), 3);
1068        assert_eq!(m.metadata().name, "rec_test");
1069    }
1070
1071    #[test]
1072    fn recorder_with_terminal_size() {
1073        let rec = MacroRecorder::new("sized").with_terminal_size(120, 40);
1074        let m = rec.finish();
1075        assert_eq!(m.metadata().terminal_size, (120, 40));
1076    }
1077
1078    #[test]
1079    fn recorder_explicit_delays() {
1080        let mut rec = MacroRecorder::new("delayed");
1081        rec.record_event_with_delay(key_event('+'), Duration::from_millis(0));
1082        rec.record_event_with_delay(key_event('-'), Duration::from_millis(50));
1083        rec.record_event_with_delay(key_event('q'), Duration::from_millis(100));
1084
1085        let m = rec.finish();
1086        assert_eq!(m.events()[0].delay, Duration::from_millis(0));
1087        assert_eq!(m.events()[1].delay, Duration::from_millis(50));
1088        assert_eq!(m.events()[2].delay, Duration::from_millis(100));
1089    }
1090
1091    // ---------- MacroPlayer tests ----------
1092
1093    #[test]
1094    fn player_replays_all_events() {
1095        let m = InputMacro::from_events(
1096            "replay",
1097            vec![key_event('+'), key_event('+'), key_event('+')],
1098        );
1099
1100        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1101        sim.init();
1102
1103        let mut player = MacroPlayer::new(&m);
1104        assert_eq!(player.remaining(), 3);
1105        assert!(!player.is_done());
1106
1107        player.replay_all(&mut sim);
1108
1109        assert!(player.is_done());
1110        assert_eq!(player.remaining(), 0);
1111        assert_eq!(sim.model().value, 3);
1112    }
1113
1114    #[test]
1115    fn player_step_advances_position() {
1116        let m = InputMacro::from_events("step", vec![key_event('+'), key_event('+')]);
1117
1118        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1119        sim.init();
1120
1121        let mut player = MacroPlayer::new(&m);
1122        assert_eq!(player.position(), 0);
1123
1124        assert!(player.step(&mut sim));
1125        assert_eq!(player.position(), 1);
1126        assert_eq!(sim.model().value, 1);
1127
1128        assert!(player.step(&mut sim));
1129        assert_eq!(player.position(), 2);
1130        assert_eq!(sim.model().value, 2);
1131
1132        assert!(!player.step(&mut sim));
1133    }
1134
1135    #[test]
1136    fn player_stops_on_quit() {
1137        let m = InputMacro::from_events(
1138            "quit_test",
1139            vec![key_event('+'), key_event('q'), key_event('+')],
1140        );
1141
1142        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1143        sim.init();
1144
1145        let mut player = MacroPlayer::new(&m);
1146        player.replay_all(&mut sim);
1147
1148        // Only increment and quit processed; third event skipped
1149        assert_eq!(sim.model().value, 1);
1150        assert!(!sim.is_running());
1151    }
1152
1153    #[test]
1154    fn player_replay_until_respects_time() {
1155        let events = vec![
1156            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1157            TimedEvent::new(key_event('+'), Duration::from_millis(20)),
1158            TimedEvent::new(key_event('+'), Duration::from_millis(100)),
1159        ];
1160        let m = InputMacro::new(
1161            events,
1162            MacroMetadata {
1163                name: "timed".to_string(),
1164                terminal_size: (80, 24),
1165                total_duration: Duration::from_millis(130),
1166            },
1167        );
1168
1169        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1170        sim.init();
1171
1172        let mut player = MacroPlayer::new(&m);
1173
1174        // Play events up to 50ms: first two events (10ms + 20ms = 30ms)
1175        player.replay_until(&mut sim, Duration::from_millis(50));
1176        assert_eq!(sim.model().value, 2);
1177        assert_eq!(player.position(), 2);
1178
1179        // Third event at 130ms, play until 200ms
1180        player.replay_until(&mut sim, Duration::from_millis(200));
1181        assert_eq!(sim.model().value, 3);
1182        assert!(player.is_done());
1183    }
1184
1185    #[test]
1186    fn player_elapsed_tracks_virtual_time() {
1187        let events = vec![
1188            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1189            TimedEvent::new(key_event('+'), Duration::from_millis(20)),
1190        ];
1191        let m = InputMacro::new(
1192            events,
1193            MacroMetadata {
1194                name: "elapsed".to_string(),
1195                terminal_size: (80, 24),
1196                total_duration: Duration::from_millis(30),
1197            },
1198        );
1199
1200        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1201        sim.init();
1202
1203        let mut player = MacroPlayer::new(&m);
1204        assert_eq!(player.elapsed(), Duration::ZERO);
1205
1206        player.step(&mut sim);
1207        assert_eq!(player.elapsed(), Duration::from_millis(10));
1208
1209        player.step(&mut sim);
1210        assert_eq!(player.elapsed(), Duration::from_millis(30));
1211    }
1212
1213    #[test]
1214    fn player_reset_restarts_playback() {
1215        let m = InputMacro::from_events("reset", vec![key_event('+'), key_event('+')]);
1216
1217        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1218        sim.init();
1219
1220        let mut player = MacroPlayer::new(&m);
1221        player.replay_all(&mut sim);
1222        assert_eq!(sim.model().value, 2);
1223        assert!(player.is_done());
1224
1225        // Reset player and replay into fresh simulator
1226        player.reset();
1227        assert_eq!(player.position(), 0);
1228        assert!(!player.is_done());
1229
1230        let mut sim2 = ProgramSimulator::new(Counter { value: 10 });
1231        sim2.init();
1232        player.replay_all(&mut sim2);
1233        assert_eq!(sim2.model().value, 12);
1234    }
1235
1236    #[test]
1237    fn player_replay_with_sleeper_respects_delays() {
1238        let events = vec![
1239            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1240            TimedEvent::new(key_event('+'), Duration::from_millis(0)),
1241            TimedEvent::new(key_event('+'), Duration::from_millis(25)),
1242        ];
1243        let m = InputMacro::new(
1244            events,
1245            MacroMetadata {
1246                name: "timed_sleep".to_string(),
1247                terminal_size: (80, 24),
1248                total_duration: Duration::from_millis(35),
1249            },
1250        );
1251
1252        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1253        sim.init();
1254
1255        let mut player = MacroPlayer::new(&m);
1256        let mut sleeps = Vec::new();
1257        player.replay_with_sleeper(&mut sim, |d| sleeps.push(d));
1258
1259        assert_eq!(
1260            sleeps,
1261            vec![Duration::from_millis(10), Duration::from_millis(25)]
1262        );
1263        assert_eq!(sim.model().value, 3);
1264    }
1265
1266    // ---------- MacroPlayback tests ----------
1267
1268    #[test]
1269    fn playback_emits_due_events_in_order() {
1270        let events = vec![
1271            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1272            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1273        ];
1274        let m = InputMacro::new(
1275            events,
1276            MacroMetadata {
1277                name: "playback".to_string(),
1278                terminal_size: (80, 24),
1279                total_duration: Duration::from_millis(20),
1280            },
1281        );
1282
1283        let mut playback = MacroPlayback::new(m.clone());
1284        assert!(playback.advance(Duration::from_millis(5)).is_empty());
1285        let first = playback.advance(Duration::from_millis(5));
1286        assert_eq!(first.len(), 1);
1287        let second = playback.advance(Duration::from_millis(10));
1288        assert_eq!(second.len(), 1);
1289        assert!(playback.advance(Duration::from_millis(10)).is_empty());
1290    }
1291
1292    #[test]
1293    fn playback_speed_scales_time() {
1294        let events = vec![TimedEvent::new(key_event('+'), Duration::from_millis(10))];
1295        let m = InputMacro::new(
1296            events,
1297            MacroMetadata {
1298                name: "speed".to_string(),
1299                terminal_size: (80, 24),
1300                total_duration: Duration::from_millis(10),
1301            },
1302        );
1303
1304        let mut playback = MacroPlayback::new(m.clone()).with_speed(2.0);
1305        let events = playback.advance(Duration::from_millis(5));
1306        assert_eq!(events.len(), 1);
1307    }
1308
1309    #[test]
1310    fn playback_looping_handles_large_delta() {
1311        let events = vec![
1312            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1313            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1314        ];
1315        let m = InputMacro::new(
1316            events,
1317            MacroMetadata {
1318                name: "loop".to_string(),
1319                terminal_size: (80, 24),
1320                total_duration: Duration::from_millis(20),
1321            },
1322        );
1323
1324        let mut playback = MacroPlayback::new(m.clone()).with_looping(true);
1325        let events = playback.advance(Duration::from_millis(50));
1326        assert_eq!(events.len(), 5);
1327    }
1328
1329    #[test]
1330    fn playback_zero_duration_does_not_loop_forever() {
1331        let m = InputMacro::from_events("zero", vec![key_event('+'), key_event('+')]);
1332        let mut playback = MacroPlayback::new(m.clone()).with_looping(true);
1333
1334        let events = playback.advance(Duration::ZERO);
1335        assert_eq!(events.len(), 2);
1336        assert!(playback.advance(Duration::from_millis(10)).is_empty());
1337    }
1338
1339    #[test]
1340    fn macro_replay_with_sleeper_wrapper() {
1341        let events = vec![
1342            TimedEvent::new(key_event('+'), Duration::from_millis(5)),
1343            TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1344        ];
1345        let m = InputMacro::new(
1346            events,
1347            MacroMetadata {
1348                name: "wrapper".to_string(),
1349                terminal_size: (80, 24),
1350                total_duration: Duration::from_millis(15),
1351            },
1352        );
1353
1354        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1355        sim.init();
1356
1357        let mut slept = Vec::new();
1358        m.replay_with_sleeper(&mut sim, |d| slept.push(d));
1359
1360        assert_eq!(
1361            slept,
1362            vec![Duration::from_millis(5), Duration::from_millis(10)]
1363        );
1364        assert_eq!(sim.model().value, 2);
1365    }
1366
1367    #[test]
1368    fn empty_macro_replay() {
1369        let m = InputMacro::from_events("empty", vec![]);
1370
1371        let mut sim = ProgramSimulator::new(Counter { value: 5 });
1372        sim.init();
1373
1374        let mut player = MacroPlayer::new(&m);
1375        assert!(player.is_done());
1376        player.replay_all(&mut sim);
1377        assert_eq!(sim.model().value, 5);
1378    }
1379
1380    #[test]
1381    fn macro_with_mixed_events() {
1382        let events = vec![
1383            key_event('+'),
1384            Event::Resize {
1385                width: 100,
1386                height: 50,
1387            },
1388            key_event('-'),
1389            Event::Focus(true),
1390            key_event('+'),
1391        ];
1392        let m = InputMacro::from_events("mixed", events);
1393
1394        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1395        sim.init();
1396
1397        let mut player = MacroPlayer::new(&m);
1398        player.replay_all(&mut sim);
1399
1400        // +1, resize->increment, -1, focus->increment, +1 = 3
1401        // (Counter converts all non-matching events to Increment)
1402        assert_eq!(sim.model().value, 3);
1403    }
1404
1405    #[test]
1406    fn deterministic_replay() {
1407        let m = InputMacro::from_events(
1408            "determinism",
1409            vec![
1410                key_event('+'),
1411                key_event('+'),
1412                key_event('-'),
1413                key_event('+'),
1414                key_event('+'),
1415            ],
1416        );
1417
1418        // Replay twice and verify identical results
1419        let result1 = {
1420            let mut sim = ProgramSimulator::new(Counter { value: 0 });
1421            sim.init();
1422            MacroPlayer::new(&m).replay_all(&mut sim);
1423            sim.model().value
1424        };
1425
1426        let result2 = {
1427            let mut sim = ProgramSimulator::new(Counter { value: 0 });
1428            sim.init();
1429            MacroPlayer::new(&m).replay_all(&mut sim);
1430            sim.model().value
1431        };
1432
1433        assert_eq!(result1, result2);
1434        assert_eq!(result1, 3);
1435    }
1436
1437    // ---------- EventRecorder tests ----------
1438
1439    #[test]
1440    fn event_recorder_starts_idle() {
1441        let rec = EventRecorder::new("test");
1442        assert_eq!(rec.state(), RecordingState::Idle);
1443        assert!(!rec.is_recording());
1444        assert_eq!(rec.event_count(), 0);
1445    }
1446
1447    #[test]
1448    fn event_recorder_start_activates() {
1449        let mut rec = EventRecorder::new("test");
1450        rec.start();
1451        assert_eq!(rec.state(), RecordingState::Recording);
1452        assert!(rec.is_recording());
1453    }
1454
1455    #[test]
1456    fn event_recorder_ignores_events_when_idle() {
1457        let mut rec = EventRecorder::new("test");
1458        assert!(!rec.record(&key_event('a')));
1459        assert_eq!(rec.event_count(), 0);
1460    }
1461
1462    #[test]
1463    fn event_recorder_records_when_active() {
1464        let mut rec = EventRecorder::new("test");
1465        rec.start();
1466        assert!(rec.record(&key_event('a')));
1467        assert!(rec.record(&key_event('b')));
1468        assert_eq!(rec.event_count(), 2);
1469
1470        let m = rec.finish();
1471        assert_eq!(m.len(), 2);
1472    }
1473
1474    #[test]
1475    fn event_recorder_pause_ignores_events() {
1476        let mut rec = EventRecorder::new("test");
1477        rec.start();
1478        rec.record(&key_event('a'));
1479        rec.pause();
1480        assert_eq!(rec.state(), RecordingState::Paused);
1481        assert!(!rec.is_recording());
1482
1483        // Events during pause are ignored
1484        assert!(!rec.record(&key_event('b')));
1485        assert_eq!(rec.event_count(), 1);
1486    }
1487
1488    #[test]
1489    fn event_recorder_resume_after_pause() {
1490        let mut rec = EventRecorder::new("test");
1491        rec.start();
1492        rec.record(&key_event('a'));
1493        rec.pause();
1494        rec.record(&key_event('b')); // ignored
1495        rec.resume();
1496        assert!(rec.is_recording());
1497        rec.record(&key_event('c'));
1498        assert_eq!(rec.event_count(), 2);
1499
1500        let m = rec.finish();
1501        assert_eq!(m.len(), 2);
1502        assert_eq!(m.bare_events()[0], key_event('a'));
1503        assert_eq!(m.bare_events()[1], key_event('c'));
1504    }
1505
1506    #[test]
1507    fn event_recorder_start_resumes_when_paused() {
1508        let mut rec = EventRecorder::new("test");
1509        rec.start();
1510        rec.pause();
1511        assert_eq!(rec.state(), RecordingState::Paused);
1512
1513        rec.start(); // Should resume
1514        assert_eq!(rec.state(), RecordingState::Recording);
1515    }
1516
1517    #[test]
1518    fn event_recorder_pause_noop_when_idle() {
1519        let mut rec = EventRecorder::new("test");
1520        rec.pause();
1521        assert_eq!(rec.state(), RecordingState::Idle);
1522    }
1523
1524    #[test]
1525    fn event_recorder_resume_noop_when_idle() {
1526        let mut rec = EventRecorder::new("test");
1527        rec.resume();
1528        assert_eq!(rec.state(), RecordingState::Idle);
1529    }
1530
1531    #[test]
1532    fn event_recorder_discard() {
1533        let mut rec = EventRecorder::new("test");
1534        rec.start();
1535        rec.record(&key_event('a'));
1536        rec.record(&key_event('b'));
1537        let count = rec.discard();
1538        assert_eq!(count, 2);
1539    }
1540
1541    #[test]
1542    fn event_recorder_with_terminal_size() {
1543        let mut rec = EventRecorder::new("sized").with_terminal_size(120, 40);
1544        rec.start();
1545        rec.record(&key_event('x'));
1546        let m = rec.finish();
1547        assert_eq!(m.metadata().terminal_size, (120, 40));
1548    }
1549
1550    #[test]
1551    fn event_recorder_finish_produces_valid_macro() {
1552        let mut rec = EventRecorder::new("full_test");
1553        rec.start();
1554        rec.record(&key_event('+'));
1555        rec.record(&key_event('+'));
1556        rec.record(&key_event('-'));
1557
1558        let m = rec.finish();
1559        assert_eq!(m.len(), 3);
1560        assert_eq!(m.metadata().name, "full_test");
1561
1562        // Replay and verify
1563        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1564        sim.init();
1565        MacroPlayer::new(&m).replay_all(&mut sim);
1566        assert_eq!(sim.model().value, 1); // +1 +1 -1 = 1
1567    }
1568
1569    #[test]
1570    fn event_recorder_record_with_delay() {
1571        let mut rec = EventRecorder::new("delayed");
1572        rec.start();
1573        assert!(rec.record_with_delay(&key_event('a'), Duration::from_millis(50)));
1574        assert!(rec.record_with_delay(&key_event('b'), Duration::from_millis(100)));
1575        assert_eq!(rec.event_count(), 2);
1576
1577        let m = rec.finish();
1578        assert_eq!(m.events()[0].delay, Duration::from_millis(50));
1579        assert_eq!(m.events()[1].delay, Duration::from_millis(100));
1580    }
1581
1582    #[test]
1583    fn event_recorder_record_with_delay_ignores_when_idle() {
1584        let mut rec = EventRecorder::new("test");
1585        assert!(!rec.record_with_delay(&key_event('a'), Duration::from_millis(50)));
1586        assert_eq!(rec.event_count(), 0);
1587    }
1588
1589    // ---------- RecordingFilter tests ----------
1590
1591    #[test]
1592    fn filter_default_accepts_all() {
1593        let filter = RecordingFilter::default();
1594        assert!(filter.accepts(&key_event('a')));
1595        assert!(filter.accepts(&Event::Resize {
1596            width: 80,
1597            height: 24
1598        }));
1599        assert!(filter.accepts(&Event::Focus(true)));
1600    }
1601
1602    #[test]
1603    fn filter_keys_only() {
1604        let filter = RecordingFilter::keys_only();
1605        assert!(filter.accepts(&key_event('a')));
1606        assert!(!filter.accepts(&Event::Resize {
1607            width: 80,
1608            height: 24
1609        }));
1610        assert!(!filter.accepts(&Event::Focus(true)));
1611    }
1612
1613    #[test]
1614    fn filter_custom() {
1615        let filter = RecordingFilter {
1616            keys: true,
1617            mouse: false,
1618            resize: false,
1619            paste: true,
1620            focus: false,
1621        };
1622        assert!(filter.accepts(&key_event('a')));
1623        assert!(!filter.accepts(&Event::Resize {
1624            width: 80,
1625            height: 24
1626        }));
1627        assert!(!filter.accepts(&Event::Focus(false)));
1628    }
1629
1630    // ---------- FilteredEventRecorder tests ----------
1631
1632    #[test]
1633    fn filtered_recorder_records_matching_events() {
1634        let mut rec = FilteredEventRecorder::new("filtered", RecordingFilter::default());
1635        rec.start();
1636        assert!(rec.record(&key_event('a')));
1637        assert_eq!(rec.event_count(), 1);
1638        assert_eq!(rec.filtered_count(), 0);
1639    }
1640
1641    #[test]
1642    fn filtered_recorder_skips_filtered_events() {
1643        let mut rec = FilteredEventRecorder::new("keys_only", RecordingFilter::keys_only());
1644        rec.start();
1645        assert!(rec.record(&key_event('a')));
1646        assert!(!rec.record(&Event::Focus(true)));
1647        assert!(!rec.record(&Event::Resize {
1648            width: 100,
1649            height: 50
1650        }));
1651        assert!(rec.record(&key_event('b')));
1652
1653        assert_eq!(rec.event_count(), 2);
1654        assert_eq!(rec.filtered_count(), 2);
1655    }
1656
1657    #[test]
1658    fn filtered_recorder_finish_produces_macro() {
1659        let mut rec = FilteredEventRecorder::new("test", RecordingFilter::keys_only());
1660        rec.start();
1661        rec.record(&key_event('+'));
1662        rec.record(&Event::Focus(true)); // filtered
1663        rec.record(&key_event('+'));
1664
1665        let m = rec.finish();
1666        assert_eq!(m.len(), 2);
1667
1668        let mut sim = ProgramSimulator::new(Counter { value: 0 });
1669        sim.init();
1670        MacroPlayer::new(&m).replay_all(&mut sim);
1671        assert_eq!(sim.model().value, 2);
1672    }
1673
1674    #[test]
1675    fn filtered_recorder_pause_resume() {
1676        let mut rec = FilteredEventRecorder::new("test", RecordingFilter::default());
1677        rec.start();
1678        rec.record(&key_event('a'));
1679        rec.pause();
1680        assert!(!rec.record(&key_event('b'))); // paused
1681        rec.resume();
1682        rec.record(&key_event('c'));
1683        assert_eq!(rec.event_count(), 2);
1684    }
1685
1686    #[test]
1687    fn filtered_recorder_with_terminal_size() {
1688        let mut rec = FilteredEventRecorder::new("sized", RecordingFilter::default())
1689            .with_terminal_size(200, 60);
1690        rec.start();
1691        rec.record(&key_event('x'));
1692        let m = rec.finish();
1693        assert_eq!(m.metadata().terminal_size, (200, 60));
1694    }
1695
1696    // ---------- Property tests ----------
1697
1698    #[derive(Default)]
1699    struct EventSink {
1700        events: Vec<Event>,
1701    }
1702
1703    #[derive(Debug, Clone)]
1704    struct EventMsg(Event);
1705
1706    impl From<Event> for EventMsg {
1707        fn from(event: Event) -> Self {
1708            Self(event)
1709        }
1710    }
1711
1712    impl Model for EventSink {
1713        type Message = EventMsg;
1714
1715        fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1716            self.events.push(msg.0);
1717            Cmd::none()
1718        }
1719
1720        fn view(&self, _frame: &mut Frame) {}
1721    }
1722
1723    proptest! {
1724        #[test]
1725        fn recorder_with_explicit_delays_roundtrips(pairs in proptest::collection::vec((0u8..=25, 0u16..=2000), 0..32)) {
1726            let mut recorder = MacroRecorder::new("prop").with_terminal_size(80, 24);
1727            let mut expected_total = Duration::ZERO;
1728            let mut expected_events = Vec::with_capacity(pairs.len());
1729
1730            for (ch_idx, delay_ms) in &pairs {
1731                let ch = char::from(b'a' + *ch_idx);
1732                let delay = Duration::from_millis(*delay_ms as u64);
1733                expected_total += delay;
1734                let ev = key_event(ch);
1735                expected_events.push(ev.clone());
1736                recorder.record_event_with_delay(ev, delay);
1737            }
1738
1739            let m = recorder.finish();
1740            prop_assert_eq!(m.len(), pairs.len());
1741            prop_assert_eq!(m.metadata().terminal_size, (80, 24));
1742            prop_assert_eq!(m.total_duration(), expected_total);
1743            prop_assert_eq!(m.bare_events(), expected_events);
1744        }
1745
1746        #[test]
1747        fn player_replays_events_in_order(pairs in proptest::collection::vec((0u8..=25, 0u16..=2000), 0..32)) {
1748            let mut timed = Vec::with_capacity(pairs.len());
1749            let mut total = Duration::ZERO;
1750            let mut expected_events = Vec::with_capacity(pairs.len());
1751
1752            for (ch_idx, delay_ms) in &pairs {
1753                let ch = char::from(b'a' + *ch_idx);
1754                let delay = Duration::from_millis(*delay_ms as u64);
1755                total += delay;
1756                let ev = key_event(ch);
1757                expected_events.push(ev.clone());
1758                timed.push(TimedEvent::new(ev, delay));
1759            }
1760
1761            let m = InputMacro::new(timed, MacroMetadata {
1762                name: "prop".to_string(),
1763                terminal_size: (80, 24),
1764                total_duration: total,
1765            });
1766
1767            let mut sim = ProgramSimulator::new(EventSink::default());
1768            sim.init();
1769            let mut player = MacroPlayer::new(&m);
1770            player.replay_all(&mut sim);
1771
1772            prop_assert_eq!(sim.model().events.clone(), expected_events);
1773            prop_assert_eq!(player.elapsed(), total);
1774        }
1775    }
1776}