Skip to main content

ftui_runtime/
input_macro.rs

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