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