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