1#![forbid(unsafe_code)]
2
3use ftui_core::event::Event;
31use std::time::{Duration, Instant};
32
33#[derive(Debug, Clone)]
35pub struct TimedEvent {
36 pub event: Event,
38 pub delay: Duration,
40}
41
42impl TimedEvent {
43 pub fn new(event: Event, delay: Duration) -> Self {
45 Self { event, delay }
46 }
47
48 pub fn immediate(event: Event) -> Self {
50 Self {
51 event,
52 delay: Duration::ZERO,
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct MacroMetadata {
60 pub name: String,
62 pub terminal_size: (u16, u16),
64 pub total_duration: Duration,
66}
67
68#[derive(Debug, Clone)]
73pub struct InputMacro {
74 events: Vec<TimedEvent>,
76 metadata: MacroMetadata,
78}
79
80impl InputMacro {
81 pub fn new(events: Vec<TimedEvent>, metadata: MacroMetadata) -> Self {
83 Self { events, metadata }
84 }
85
86 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 pub fn events(&self) -> &[TimedEvent] {
103 &self.events
104 }
105
106 pub fn metadata(&self) -> &MacroMetadata {
108 &self.metadata
109 }
110
111 pub fn len(&self) -> usize {
113 self.events.len()
114 }
115
116 pub fn is_empty(&self) -> bool {
118 self.events.is_empty()
119 }
120
121 pub fn total_duration(&self) -> Duration {
123 self.metadata.total_duration
124 }
125
126 pub fn bare_events(&self) -> Vec<Event> {
128 self.events.iter().map(|te| te.event.clone()).collect()
129 }
130
131 pub fn replay_with_timing<M: crate::program::Model>(
133 &self,
134 sim: &mut crate::simulator::ProgramSimulator<M>,
135 ) {
136 let mut player = MacroPlayer::new(self);
137 player.replay_with_timing(sim);
138 }
139
140 pub fn replay_with_sleeper<M, F>(
144 &self,
145 sim: &mut crate::simulator::ProgramSimulator<M>,
146 sleep: F,
147 ) where
148 M: crate::program::Model,
149 F: FnMut(Duration),
150 {
151 let mut player = MacroPlayer::new(self);
152 player.replay_with_sleeper(sim, sleep);
153 }
154}
155
156pub struct MacroRecorder {
161 name: String,
162 terminal_size: (u16, u16),
163 events: Vec<TimedEvent>,
164 start_time: Instant,
165 last_event_time: Instant,
166}
167
168impl MacroRecorder {
169 pub fn new(name: impl Into<String>) -> Self {
171 let now = Instant::now();
172 Self {
173 name: name.into(),
174 terminal_size: (80, 24),
175 events: Vec::new(),
176 start_time: now,
177 last_event_time: now,
178 }
179 }
180
181 pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
183 self.terminal_size = (width, height);
184 self
185 }
186
187 pub fn record_event(&mut self, event: Event) {
191 let now = Instant::now();
192 let delay = now.duration_since(self.last_event_time);
193 #[cfg(feature = "tracing")]
194 tracing::debug!(event = ?event, delay = ?delay, "macro record event");
195 self.events.push(TimedEvent::new(event, delay));
196 self.last_event_time = now;
197 }
198
199 pub fn record_event_with_delay(&mut self, event: Event, delay: Duration) {
201 #[cfg(feature = "tracing")]
202 tracing::debug!(event = ?event, delay = ?delay, "macro record event");
203 self.events.push(TimedEvent::new(event, delay));
204 self.last_event_time += delay;
206 }
207
208 pub fn event_count(&self) -> usize {
210 self.events.len()
211 }
212
213 pub fn finish(self) -> InputMacro {
215 let total_duration = self.last_event_time.duration_since(self.start_time);
216 InputMacro {
217 events: self.events,
218 metadata: MacroMetadata {
219 name: self.name,
220 terminal_size: self.terminal_size,
221 total_duration,
222 },
223 }
224 }
225}
226
227pub struct MacroPlayer<'a> {
233 input_macro: &'a InputMacro,
234 position: usize,
235 elapsed: Duration,
236}
237
238impl<'a> MacroPlayer<'a> {
239 pub fn new(input_macro: &'a InputMacro) -> Self {
241 Self {
242 input_macro,
243 position: 0,
244 elapsed: Duration::ZERO,
245 }
246 }
247
248 pub fn position(&self) -> usize {
250 self.position
251 }
252
253 pub fn elapsed(&self) -> Duration {
255 self.elapsed
256 }
257
258 pub fn is_done(&self) -> bool {
260 self.position >= self.input_macro.len()
261 }
262
263 pub fn remaining(&self) -> usize {
265 self.input_macro.len().saturating_sub(self.position)
266 }
267
268 pub fn step<M: crate::program::Model>(
272 &mut self,
273 sim: &mut crate::simulator::ProgramSimulator<M>,
274 ) -> bool {
275 if self.is_done() {
276 return false;
277 }
278
279 let timed = &self.input_macro.events[self.position];
280 #[cfg(feature = "tracing")]
281 tracing::debug!(event = ?timed.event, delay = ?timed.delay, "macro playback event");
282 self.elapsed += timed.delay;
283 sim.inject_events(std::slice::from_ref(&timed.event));
284 self.position += 1;
285 true
286 }
287
288 pub fn replay_all<M: crate::program::Model>(
292 &mut self,
293 sim: &mut crate::simulator::ProgramSimulator<M>,
294 ) {
295 while !self.is_done() && sim.is_running() {
296 self.step(sim);
297 }
298 }
299
300 pub fn replay_with_timing<M: crate::program::Model>(
305 &mut self,
306 sim: &mut crate::simulator::ProgramSimulator<M>,
307 ) {
308 self.replay_with_sleeper(sim, std::thread::sleep);
309 }
310
311 pub fn replay_with_sleeper<M, F>(
316 &mut self,
317 sim: &mut crate::simulator::ProgramSimulator<M>,
318 mut sleep: F,
319 ) where
320 M: crate::program::Model,
321 F: FnMut(Duration),
322 {
323 while !self.is_done() && sim.is_running() {
324 let timed = &self.input_macro.events[self.position];
325 if timed.delay > Duration::ZERO {
326 sleep(timed.delay);
327 }
328 self.step(sim);
329 }
330 }
331
332 pub fn replay_until<M: crate::program::Model>(
336 &mut self,
337 sim: &mut crate::simulator::ProgramSimulator<M>,
338 until: Duration,
339 ) {
340 while !self.is_done() && sim.is_running() {
341 let timed = &self.input_macro.events[self.position];
342 let next_elapsed = self.elapsed + timed.delay;
343 if next_elapsed > until {
344 break;
345 }
346 self.step(sim);
347 }
348 }
349
350 pub fn reset(&mut self) {
352 self.position = 0;
353 self.elapsed = Duration::ZERO;
354 }
355}
356
357#[derive(Debug, Clone)]
372pub struct MacroPlayback {
373 input_macro: InputMacro,
374 position: usize,
375 elapsed: Duration,
376 next_due: Duration,
377 speed: f64,
378 looping: bool,
379 start_logged: bool,
380 stop_logged: bool,
381 error_logged: bool,
382}
383
384impl MacroPlayback {
385 pub fn new(input_macro: InputMacro) -> Self {
387 let next_due = input_macro
388 .events()
389 .first()
390 .map(|e| e.delay)
391 .unwrap_or(Duration::ZERO);
392 Self {
393 input_macro,
394 position: 0,
395 elapsed: Duration::ZERO,
396 next_due,
397 speed: 1.0,
398 looping: false,
399 start_logged: false,
400 stop_logged: false,
401 error_logged: false,
402 }
403 }
404
405 pub fn set_speed(&mut self, speed: f64) {
407 self.speed = normalize_speed(speed);
408 }
409
410 pub fn with_speed(mut self, speed: f64) -> Self {
412 self.set_speed(speed);
413 self
414 }
415
416 pub fn set_looping(&mut self, looping: bool) {
418 self.looping = looping;
419 }
420
421 pub fn with_looping(mut self, looping: bool) -> Self {
423 self.set_looping(looping);
424 self
425 }
426
427 pub fn speed(&self) -> f64 {
429 self.speed
430 }
431
432 pub fn position(&self) -> usize {
434 self.position
435 }
436
437 pub fn elapsed(&self) -> Duration {
439 self.elapsed
440 }
441
442 pub fn is_done(&self) -> bool {
444 if self.input_macro.is_empty() {
445 return true;
446 }
447 if self.looping && self.input_macro.total_duration() > Duration::ZERO {
448 return false;
449 }
450 self.position >= self.input_macro.len()
451 }
452
453 pub fn reset(&mut self) {
455 self.position = 0;
456 self.elapsed = Duration::ZERO;
457 self.next_due = self
458 .input_macro
459 .events()
460 .first()
461 .map(|e| e.delay)
462 .unwrap_or(Duration::ZERO);
463 self.start_logged = false;
464 self.stop_logged = false;
465 self.error_logged = false;
466 }
467
468 pub fn advance(&mut self, delta: Duration) -> Vec<Event> {
470 if self.input_macro.is_empty() {
471 #[cfg(feature = "tracing")]
472 if !self.error_logged {
473 let meta = self.input_macro.metadata();
474 tracing::warn!(
475 macro_event = "playback_error",
476 reason = "macro_empty",
477 name = %meta.name,
478 events = 0usize,
479 duration_ms = self.input_macro.total_duration().as_millis() as u64,
480 );
481 self.error_logged = true;
482 }
483 return Vec::new();
484 }
485 if self.is_done() {
486 return Vec::new();
487 }
488
489 #[cfg(feature = "tracing")]
490 if !self.start_logged {
491 let meta = self.input_macro.metadata();
492 tracing::info!(
493 macro_event = "playback_start",
494 name = %meta.name,
495 events = self.input_macro.len(),
496 duration_ms = self.input_macro.total_duration().as_millis() as u64,
497 speed = self.speed,
498 looping = self.looping,
499 );
500 self.start_logged = true;
501 }
502
503 self.elapsed += scale_duration(delta, self.speed);
504 let events = self.drain_due_events();
505
506 #[cfg(feature = "tracing")]
507 if self.is_done() && !self.stop_logged {
508 let meta = self.input_macro.metadata();
509 tracing::info!(
510 macro_event = "playback_stop",
511 reason = "completed",
512 name = %meta.name,
513 events = self.input_macro.len(),
514 elapsed_ms = self.elapsed.as_millis() as u64,
515 looping = self.looping,
516 );
517 self.stop_logged = true;
518 }
519
520 events
521 }
522
523 fn drain_due_events(&mut self) -> Vec<Event> {
524 let mut out = Vec::new();
525 let total_duration = self.input_macro.total_duration();
526 let can_loop = self.looping && total_duration > Duration::ZERO;
527 if can_loop && self.position >= self.input_macro.len() {
528 let total_secs = total_duration.as_secs_f64();
529 if total_secs > 0.0 {
530 let elapsed_secs = self.elapsed.as_secs_f64() % total_secs;
531 self.elapsed = Duration::from_secs_f64(elapsed_secs);
532 } else {
533 self.elapsed = Duration::ZERO;
534 }
535 self.position = 0;
536 self.next_due = self
537 .input_macro
538 .events()
539 .first()
540 .map(|e| e.delay)
541 .unwrap_or(Duration::ZERO);
542 }
543
544 while self.position < self.input_macro.len() && self.elapsed >= self.next_due {
545 let timed = &self.input_macro.events[self.position];
546 #[cfg(feature = "tracing")]
547 tracing::debug!(event = ?timed.event, delay = ?timed.delay, "macro playback event");
548 out.push(timed.event.clone());
549 self.position += 1;
550 if self.position < self.input_macro.len() {
551 self.next_due += self.input_macro.events[self.position].delay;
552 } else if can_loop {
553 self.elapsed = self.elapsed.saturating_sub(total_duration);
555 self.position = 0;
556 self.next_due = self
557 .input_macro
558 .events()
559 .first()
560 .map(|e| e.delay)
561 .unwrap_or(Duration::ZERO);
562 }
563 }
564
565 out
566 }
567}
568
569fn normalize_speed(speed: f64) -> f64 {
570 if !speed.is_finite() {
571 return 1.0;
572 }
573 if speed <= 0.0 {
574 return 0.0;
575 }
576 speed
577}
578
579fn scale_duration(delta: Duration, speed: f64) -> Duration {
580 if delta == Duration::ZERO {
581 return Duration::ZERO;
582 }
583 let speed = normalize_speed(speed);
584 if speed == 1.0 {
585 return delta;
586 }
587 Duration::from_secs_f64(delta.as_secs_f64() * speed)
588}
589
590#[derive(Debug, Clone, Copy, PartialEq, Eq)]
596pub enum RecordingState {
597 Idle,
599 Recording,
601 Paused,
603}
604
605pub struct EventRecorder {
629 inner: MacroRecorder,
630 state: RecordingState,
631 pause_start: Option<Instant>,
632 total_paused: Duration,
633 event_count: usize,
634}
635
636impl EventRecorder {
637 pub fn new(name: impl Into<String>) -> Self {
642 Self {
643 inner: MacroRecorder::new(name),
644 state: RecordingState::Idle,
645 pause_start: None,
646 total_paused: Duration::ZERO,
647 event_count: 0,
648 }
649 }
650
651 pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
653 self.inner = self.inner.with_terminal_size(width, height);
654 self
655 }
656
657 pub fn state(&self) -> RecordingState {
659 self.state
660 }
661
662 pub fn is_recording(&self) -> bool {
664 self.state == RecordingState::Recording
665 }
666
667 pub fn start(&mut self) {
669 match self.state {
670 RecordingState::Idle => {
671 self.state = RecordingState::Recording;
672 #[cfg(feature = "tracing")]
673 tracing::info!(
674 macro_event = "recorder_start",
675 name = %self.inner.name,
676 term_cols = self.inner.terminal_size.0,
677 term_rows = self.inner.terminal_size.1,
678 );
679 }
680 RecordingState::Paused => {
681 self.resume();
682 }
683 RecordingState::Recording => {} }
685 }
686
687 pub fn pause(&mut self) {
691 if self.state == RecordingState::Recording {
692 self.state = RecordingState::Paused;
693 self.pause_start = Some(Instant::now());
694 }
695 }
696
697 pub fn resume(&mut self) {
701 if self.state == RecordingState::Paused {
702 if let Some(pause_start) = self.pause_start.take() {
703 self.total_paused += pause_start.elapsed();
704 }
705 self.inner.last_event_time = Instant::now();
709 self.state = RecordingState::Recording;
710 }
711 }
712
713 pub fn record(&mut self, event: &Event) -> bool {
717 if self.state != RecordingState::Recording {
718 return false;
719 }
720 self.inner.record_event(event.clone());
721 self.event_count += 1;
722 true
723 }
724
725 pub fn record_with_delay(&mut self, event: &Event, delay: Duration) -> bool {
729 if self.state != RecordingState::Recording {
730 return false;
731 }
732 self.inner.record_event_with_delay(event.clone(), delay);
733 self.event_count += 1;
734 true
735 }
736
737 pub fn event_count(&self) -> usize {
739 self.event_count
740 }
741
742 pub fn total_paused(&self) -> Duration {
744 let mut total = self.total_paused;
745 if let Some(pause_start) = self.pause_start {
746 total += pause_start.elapsed();
747 }
748 total
749 }
750
751 pub fn finish(self) -> InputMacro {
755 self.finish_internal(true)
756 }
757
758 #[allow(unused_variables)]
759 fn finish_internal(self, log: bool) -> InputMacro {
760 let paused = self.total_paused();
761 let macro_data = self.inner.finish();
762 #[cfg(feature = "tracing")]
763 if log {
764 let meta = macro_data.metadata();
765 tracing::info!(
766 macro_event = "recorder_stop",
767 name = %meta.name,
768 events = macro_data.len(),
769 duration_ms = macro_data.total_duration().as_millis() as u64,
770 paused_ms = paused.as_millis() as u64,
771 term_cols = meta.terminal_size.0,
772 term_rows = meta.terminal_size.1,
773 );
774 }
775 macro_data
776 }
777
778 pub fn discard(self) -> usize {
782 self.event_count
783 }
784}
785
786#[derive(Debug, Clone)]
791pub struct RecordingFilter {
792 pub keys: bool,
794 pub mouse: bool,
796 pub resize: bool,
798 pub paste: bool,
800 pub focus: bool,
802}
803
804impl Default for RecordingFilter {
805 fn default() -> Self {
806 Self {
807 keys: true,
808 mouse: true,
809 resize: true,
810 paste: true,
811 focus: true,
812 }
813 }
814}
815
816impl RecordingFilter {
817 pub fn keys_only() -> Self {
819 Self {
820 keys: true,
821 mouse: false,
822 resize: false,
823 paste: false,
824 focus: false,
825 }
826 }
827
828 pub fn accepts(&self, event: &Event) -> bool {
830 match event {
831 Event::Key(_) => self.keys,
832 Event::Mouse(_) => self.mouse,
833 Event::Resize { .. } => self.resize,
834 Event::Paste(_) => self.paste,
835 Event::Focus(_) => self.focus,
836 Event::Clipboard(_) => true, Event::Tick => false, }
839 }
840}
841
842pub struct FilteredEventRecorder {
844 recorder: EventRecorder,
845 filter: RecordingFilter,
846 filtered_count: usize,
847}
848
849impl FilteredEventRecorder {
850 pub fn new(name: impl Into<String>, filter: RecordingFilter) -> Self {
852 Self {
853 recorder: EventRecorder::new(name),
854 filter,
855 filtered_count: 0,
856 }
857 }
858
859 pub fn with_terminal_size(mut self, width: u16, height: u16) -> Self {
861 self.recorder = self.recorder.with_terminal_size(width, height);
862 self
863 }
864
865 pub fn start(&mut self) {
867 self.recorder.start();
868 }
869
870 pub fn pause(&mut self) {
872 self.recorder.pause();
873 }
874
875 pub fn resume(&mut self) {
877 self.recorder.resume();
878 }
879
880 pub fn state(&self) -> RecordingState {
882 self.recorder.state()
883 }
884
885 pub fn is_recording(&self) -> bool {
887 self.recorder.is_recording()
888 }
889
890 pub fn record(&mut self, event: &Event) -> bool {
894 if !self.filter.accepts(event) {
895 self.filtered_count += 1;
896 return false;
897 }
898 self.recorder.record(event)
899 }
900
901 pub fn filtered_count(&self) -> usize {
903 self.filtered_count
904 }
905
906 pub fn event_count(&self) -> usize {
908 self.recorder.event_count()
909 }
910
911 #[allow(unused_variables)]
913 pub fn finish(self) -> InputMacro {
914 let filtered = self.filtered_count;
915 let paused = self.recorder.total_paused();
916 let macro_data = self.recorder.finish_internal(false);
917 #[cfg(feature = "tracing")]
918 {
919 let meta = macro_data.metadata();
920 tracing::info!(
921 macro_event = "recorder_stop",
922 name = %meta.name,
923 events = macro_data.len(),
924 filtered,
925 duration_ms = macro_data.total_duration().as_millis() as u64,
926 paused_ms = paused.as_millis() as u64,
927 term_cols = meta.terminal_size.0,
928 term_rows = meta.terminal_size.1,
929 );
930 }
931 macro_data
932 }
933}
934
935#[cfg(test)]
936mod tests {
937 use super::*;
938 use crate::program::{Cmd, Model};
939 use crate::simulator::ProgramSimulator;
940 use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
941 use ftui_render::frame::Frame;
942 use proptest::prelude::*;
943
944 struct Counter {
947 value: i32,
948 }
949
950 #[derive(Debug)]
951 enum CounterMsg {
952 Increment,
953 Decrement,
954 Quit,
955 }
956
957 impl From<Event> for CounterMsg {
958 fn from(event: Event) -> Self {
959 match event {
960 Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
961 Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
962 Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
963 _ => CounterMsg::Increment,
964 }
965 }
966 }
967
968 impl Model for Counter {
969 type Message = CounterMsg;
970
971 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
972 match msg {
973 CounterMsg::Increment => {
974 self.value += 1;
975 Cmd::none()
976 }
977 CounterMsg::Decrement => {
978 self.value -= 1;
979 Cmd::none()
980 }
981 CounterMsg::Quit => Cmd::quit(),
982 }
983 }
984
985 fn view(&self, _frame: &mut Frame) {}
986 }
987
988 fn key_event(c: char) -> Event {
989 Event::Key(KeyEvent {
990 code: KeyCode::Char(c),
991 modifiers: Modifiers::empty(),
992 kind: KeyEventKind::Press,
993 })
994 }
995
996 #[test]
999 fn timed_event_immediate_has_zero_delay() {
1000 let te = TimedEvent::immediate(key_event('a'));
1001 assert_eq!(te.delay, Duration::ZERO);
1002 }
1003
1004 #[test]
1005 fn timed_event_new_preserves_delay() {
1006 let delay = Duration::from_millis(100);
1007 let te = TimedEvent::new(key_event('x'), delay);
1008 assert_eq!(te.delay, delay);
1009 }
1010
1011 #[test]
1014 fn macro_from_events_has_zero_delays() {
1015 let m = InputMacro::from_events("test", vec![key_event('+'), key_event('-')]);
1016 assert_eq!(m.len(), 2);
1017 assert!(!m.is_empty());
1018 assert_eq!(m.total_duration(), Duration::ZERO);
1019 for te in m.events() {
1020 assert_eq!(te.delay, Duration::ZERO);
1021 }
1022 }
1023
1024 #[test]
1025 fn macro_metadata() {
1026 let m = InputMacro::from_events("my_macro", vec![key_event('a')]);
1027 assert_eq!(m.metadata().name, "my_macro");
1028 assert_eq!(m.metadata().terminal_size, (80, 24));
1029 }
1030
1031 #[test]
1032 fn empty_macro() {
1033 let m = InputMacro::from_events("empty", vec![]);
1034 assert!(m.is_empty());
1035 assert_eq!(m.len(), 0);
1036 }
1037
1038 #[test]
1039 fn bare_events_extracts_events() {
1040 let events = vec![key_event('+'), key_event('-'), key_event('q')];
1041 let m = InputMacro::from_events("test", events.clone());
1042 let bare = m.bare_events();
1043 assert_eq!(bare.len(), 3);
1044 assert_eq!(bare, events);
1045 }
1046
1047 #[test]
1050 fn recorder_captures_events() {
1051 let mut rec = MacroRecorder::new("rec_test");
1052 rec.record_event(key_event('+'));
1053 rec.record_event(key_event('+'));
1054 rec.record_event(key_event('-'));
1055 assert_eq!(rec.event_count(), 3);
1056
1057 let m = rec.finish();
1058 assert_eq!(m.len(), 3);
1059 assert_eq!(m.metadata().name, "rec_test");
1060 }
1061
1062 #[test]
1063 fn recorder_with_terminal_size() {
1064 let rec = MacroRecorder::new("sized").with_terminal_size(120, 40);
1065 let m = rec.finish();
1066 assert_eq!(m.metadata().terminal_size, (120, 40));
1067 }
1068
1069 #[test]
1070 fn recorder_explicit_delays() {
1071 let mut rec = MacroRecorder::new("delayed");
1072 rec.record_event_with_delay(key_event('+'), Duration::from_millis(0));
1073 rec.record_event_with_delay(key_event('-'), Duration::from_millis(50));
1074 rec.record_event_with_delay(key_event('q'), Duration::from_millis(100));
1075
1076 let m = rec.finish();
1077 assert_eq!(m.events()[0].delay, Duration::from_millis(0));
1078 assert_eq!(m.events()[1].delay, Duration::from_millis(50));
1079 assert_eq!(m.events()[2].delay, Duration::from_millis(100));
1080 }
1081
1082 #[test]
1085 fn player_replays_all_events() {
1086 let m = InputMacro::from_events(
1087 "replay",
1088 vec![key_event('+'), key_event('+'), key_event('+')],
1089 );
1090
1091 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1092 sim.init();
1093
1094 let mut player = MacroPlayer::new(&m);
1095 assert_eq!(player.remaining(), 3);
1096 assert!(!player.is_done());
1097
1098 player.replay_all(&mut sim);
1099
1100 assert!(player.is_done());
1101 assert_eq!(player.remaining(), 0);
1102 assert_eq!(sim.model().value, 3);
1103 }
1104
1105 #[test]
1106 fn player_step_advances_position() {
1107 let m = InputMacro::from_events("step", vec![key_event('+'), key_event('+')]);
1108
1109 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1110 sim.init();
1111
1112 let mut player = MacroPlayer::new(&m);
1113 assert_eq!(player.position(), 0);
1114
1115 assert!(player.step(&mut sim));
1116 assert_eq!(player.position(), 1);
1117 assert_eq!(sim.model().value, 1);
1118
1119 assert!(player.step(&mut sim));
1120 assert_eq!(player.position(), 2);
1121 assert_eq!(sim.model().value, 2);
1122
1123 assert!(!player.step(&mut sim));
1124 }
1125
1126 #[test]
1127 fn player_stops_on_quit() {
1128 let m = InputMacro::from_events(
1129 "quit_test",
1130 vec![key_event('+'), key_event('q'), key_event('+')],
1131 );
1132
1133 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1134 sim.init();
1135
1136 let mut player = MacroPlayer::new(&m);
1137 player.replay_all(&mut sim);
1138
1139 assert_eq!(sim.model().value, 1);
1141 assert!(!sim.is_running());
1142 }
1143
1144 #[test]
1145 fn player_replay_until_respects_time() {
1146 let events = vec![
1147 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1148 TimedEvent::new(key_event('+'), Duration::from_millis(20)),
1149 TimedEvent::new(key_event('+'), Duration::from_millis(100)),
1150 ];
1151 let m = InputMacro::new(
1152 events,
1153 MacroMetadata {
1154 name: "timed".to_string(),
1155 terminal_size: (80, 24),
1156 total_duration: Duration::from_millis(130),
1157 },
1158 );
1159
1160 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1161 sim.init();
1162
1163 let mut player = MacroPlayer::new(&m);
1164
1165 player.replay_until(&mut sim, Duration::from_millis(50));
1167 assert_eq!(sim.model().value, 2);
1168 assert_eq!(player.position(), 2);
1169
1170 player.replay_until(&mut sim, Duration::from_millis(200));
1172 assert_eq!(sim.model().value, 3);
1173 assert!(player.is_done());
1174 }
1175
1176 #[test]
1177 fn player_elapsed_tracks_virtual_time() {
1178 let events = vec![
1179 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1180 TimedEvent::new(key_event('+'), Duration::from_millis(20)),
1181 ];
1182 let m = InputMacro::new(
1183 events,
1184 MacroMetadata {
1185 name: "elapsed".to_string(),
1186 terminal_size: (80, 24),
1187 total_duration: Duration::from_millis(30),
1188 },
1189 );
1190
1191 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1192 sim.init();
1193
1194 let mut player = MacroPlayer::new(&m);
1195 assert_eq!(player.elapsed(), Duration::ZERO);
1196
1197 player.step(&mut sim);
1198 assert_eq!(player.elapsed(), Duration::from_millis(10));
1199
1200 player.step(&mut sim);
1201 assert_eq!(player.elapsed(), Duration::from_millis(30));
1202 }
1203
1204 #[test]
1205 fn player_reset_restarts_playback() {
1206 let m = InputMacro::from_events("reset", vec![key_event('+'), key_event('+')]);
1207
1208 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1209 sim.init();
1210
1211 let mut player = MacroPlayer::new(&m);
1212 player.replay_all(&mut sim);
1213 assert_eq!(sim.model().value, 2);
1214 assert!(player.is_done());
1215
1216 player.reset();
1218 assert_eq!(player.position(), 0);
1219 assert!(!player.is_done());
1220
1221 let mut sim2 = ProgramSimulator::new(Counter { value: 10 });
1222 sim2.init();
1223 player.replay_all(&mut sim2);
1224 assert_eq!(sim2.model().value, 12);
1225 }
1226
1227 #[test]
1228 fn player_replay_with_sleeper_respects_delays() {
1229 let events = vec![
1230 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1231 TimedEvent::new(key_event('+'), Duration::from_millis(0)),
1232 TimedEvent::new(key_event('+'), Duration::from_millis(25)),
1233 ];
1234 let m = InputMacro::new(
1235 events,
1236 MacroMetadata {
1237 name: "timed_sleep".to_string(),
1238 terminal_size: (80, 24),
1239 total_duration: Duration::from_millis(35),
1240 },
1241 );
1242
1243 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1244 sim.init();
1245
1246 let mut player = MacroPlayer::new(&m);
1247 let mut sleeps = Vec::new();
1248 player.replay_with_sleeper(&mut sim, |d| sleeps.push(d));
1249
1250 assert_eq!(
1251 sleeps,
1252 vec![Duration::from_millis(10), Duration::from_millis(25)]
1253 );
1254 assert_eq!(sim.model().value, 3);
1255 }
1256
1257 #[test]
1260 fn playback_emits_due_events_in_order() {
1261 let events = vec![
1262 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1263 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1264 ];
1265 let m = InputMacro::new(
1266 events,
1267 MacroMetadata {
1268 name: "playback".to_string(),
1269 terminal_size: (80, 24),
1270 total_duration: Duration::from_millis(20),
1271 },
1272 );
1273
1274 let mut playback = MacroPlayback::new(m.clone());
1275 assert!(playback.advance(Duration::from_millis(5)).is_empty());
1276 let first = playback.advance(Duration::from_millis(5));
1277 assert_eq!(first.len(), 1);
1278 let second = playback.advance(Duration::from_millis(10));
1279 assert_eq!(second.len(), 1);
1280 assert!(playback.advance(Duration::from_millis(10)).is_empty());
1281 }
1282
1283 #[test]
1284 fn playback_speed_scales_time() {
1285 let events = vec![TimedEvent::new(key_event('+'), Duration::from_millis(10))];
1286 let m = InputMacro::new(
1287 events,
1288 MacroMetadata {
1289 name: "speed".to_string(),
1290 terminal_size: (80, 24),
1291 total_duration: Duration::from_millis(10),
1292 },
1293 );
1294
1295 let mut playback = MacroPlayback::new(m.clone()).with_speed(2.0);
1296 let events = playback.advance(Duration::from_millis(5));
1297 assert_eq!(events.len(), 1);
1298 }
1299
1300 #[test]
1301 fn playback_looping_handles_large_delta() {
1302 let events = vec![
1303 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1304 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1305 ];
1306 let m = InputMacro::new(
1307 events,
1308 MacroMetadata {
1309 name: "loop".to_string(),
1310 terminal_size: (80, 24),
1311 total_duration: Duration::from_millis(20),
1312 },
1313 );
1314
1315 let mut playback = MacroPlayback::new(m.clone()).with_looping(true);
1316 let events = playback.advance(Duration::from_millis(50));
1317 assert_eq!(events.len(), 5);
1318 }
1319
1320 #[test]
1321 fn playback_zero_duration_does_not_loop_forever() {
1322 let m = InputMacro::from_events("zero", vec![key_event('+'), key_event('+')]);
1323 let mut playback = MacroPlayback::new(m.clone()).with_looping(true);
1324
1325 let events = playback.advance(Duration::ZERO);
1326 assert_eq!(events.len(), 2);
1327 assert!(playback.advance(Duration::from_millis(10)).is_empty());
1328 }
1329
1330 #[test]
1331 fn macro_replay_with_sleeper_wrapper() {
1332 let events = vec![
1333 TimedEvent::new(key_event('+'), Duration::from_millis(5)),
1334 TimedEvent::new(key_event('+'), Duration::from_millis(10)),
1335 ];
1336 let m = InputMacro::new(
1337 events,
1338 MacroMetadata {
1339 name: "wrapper".to_string(),
1340 terminal_size: (80, 24),
1341 total_duration: Duration::from_millis(15),
1342 },
1343 );
1344
1345 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1346 sim.init();
1347
1348 let mut slept = Vec::new();
1349 m.replay_with_sleeper(&mut sim, |d| slept.push(d));
1350
1351 assert_eq!(
1352 slept,
1353 vec![Duration::from_millis(5), Duration::from_millis(10)]
1354 );
1355 assert_eq!(sim.model().value, 2);
1356 }
1357
1358 #[test]
1359 fn empty_macro_replay() {
1360 let m = InputMacro::from_events("empty", vec![]);
1361
1362 let mut sim = ProgramSimulator::new(Counter { value: 5 });
1363 sim.init();
1364
1365 let mut player = MacroPlayer::new(&m);
1366 assert!(player.is_done());
1367 player.replay_all(&mut sim);
1368 assert_eq!(sim.model().value, 5);
1369 }
1370
1371 #[test]
1372 fn macro_with_mixed_events() {
1373 let events = vec![
1374 key_event('+'),
1375 Event::Resize {
1376 width: 100,
1377 height: 50,
1378 },
1379 key_event('-'),
1380 Event::Focus(true),
1381 key_event('+'),
1382 ];
1383 let m = InputMacro::from_events("mixed", events);
1384
1385 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1386 sim.init();
1387
1388 let mut player = MacroPlayer::new(&m);
1389 player.replay_all(&mut sim);
1390
1391 assert_eq!(sim.model().value, 3);
1394 }
1395
1396 #[test]
1397 fn deterministic_replay() {
1398 let m = InputMacro::from_events(
1399 "determinism",
1400 vec![
1401 key_event('+'),
1402 key_event('+'),
1403 key_event('-'),
1404 key_event('+'),
1405 key_event('+'),
1406 ],
1407 );
1408
1409 let result1 = {
1411 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1412 sim.init();
1413 MacroPlayer::new(&m).replay_all(&mut sim);
1414 sim.model().value
1415 };
1416
1417 let result2 = {
1418 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1419 sim.init();
1420 MacroPlayer::new(&m).replay_all(&mut sim);
1421 sim.model().value
1422 };
1423
1424 assert_eq!(result1, result2);
1425 assert_eq!(result1, 3);
1426 }
1427
1428 #[test]
1431 fn event_recorder_starts_idle() {
1432 let rec = EventRecorder::new("test");
1433 assert_eq!(rec.state(), RecordingState::Idle);
1434 assert!(!rec.is_recording());
1435 assert_eq!(rec.event_count(), 0);
1436 }
1437
1438 #[test]
1439 fn event_recorder_start_activates() {
1440 let mut rec = EventRecorder::new("test");
1441 rec.start();
1442 assert_eq!(rec.state(), RecordingState::Recording);
1443 assert!(rec.is_recording());
1444 }
1445
1446 #[test]
1447 fn event_recorder_ignores_events_when_idle() {
1448 let mut rec = EventRecorder::new("test");
1449 assert!(!rec.record(&key_event('a')));
1450 assert_eq!(rec.event_count(), 0);
1451 }
1452
1453 #[test]
1454 fn event_recorder_records_when_active() {
1455 let mut rec = EventRecorder::new("test");
1456 rec.start();
1457 assert!(rec.record(&key_event('a')));
1458 assert!(rec.record(&key_event('b')));
1459 assert_eq!(rec.event_count(), 2);
1460
1461 let m = rec.finish();
1462 assert_eq!(m.len(), 2);
1463 }
1464
1465 #[test]
1466 fn event_recorder_pause_ignores_events() {
1467 let mut rec = EventRecorder::new("test");
1468 rec.start();
1469 rec.record(&key_event('a'));
1470 rec.pause();
1471 assert_eq!(rec.state(), RecordingState::Paused);
1472 assert!(!rec.is_recording());
1473
1474 assert!(!rec.record(&key_event('b')));
1476 assert_eq!(rec.event_count(), 1);
1477 }
1478
1479 #[test]
1480 fn event_recorder_resume_after_pause() {
1481 let mut rec = EventRecorder::new("test");
1482 rec.start();
1483 rec.record(&key_event('a'));
1484 rec.pause();
1485 rec.record(&key_event('b')); rec.resume();
1487 assert!(rec.is_recording());
1488 rec.record(&key_event('c'));
1489 assert_eq!(rec.event_count(), 2);
1490
1491 let m = rec.finish();
1492 assert_eq!(m.len(), 2);
1493 assert_eq!(m.bare_events()[0], key_event('a'));
1494 assert_eq!(m.bare_events()[1], key_event('c'));
1495 }
1496
1497 #[test]
1498 fn event_recorder_start_resumes_when_paused() {
1499 let mut rec = EventRecorder::new("test");
1500 rec.start();
1501 rec.pause();
1502 assert_eq!(rec.state(), RecordingState::Paused);
1503
1504 rec.start(); assert_eq!(rec.state(), RecordingState::Recording);
1506 }
1507
1508 #[test]
1509 fn event_recorder_pause_noop_when_idle() {
1510 let mut rec = EventRecorder::new("test");
1511 rec.pause();
1512 assert_eq!(rec.state(), RecordingState::Idle);
1513 }
1514
1515 #[test]
1516 fn event_recorder_resume_noop_when_idle() {
1517 let mut rec = EventRecorder::new("test");
1518 rec.resume();
1519 assert_eq!(rec.state(), RecordingState::Idle);
1520 }
1521
1522 #[test]
1523 fn event_recorder_discard() {
1524 let mut rec = EventRecorder::new("test");
1525 rec.start();
1526 rec.record(&key_event('a'));
1527 rec.record(&key_event('b'));
1528 let count = rec.discard();
1529 assert_eq!(count, 2);
1530 }
1531
1532 #[test]
1533 fn event_recorder_with_terminal_size() {
1534 let mut rec = EventRecorder::new("sized").with_terminal_size(120, 40);
1535 rec.start();
1536 rec.record(&key_event('x'));
1537 let m = rec.finish();
1538 assert_eq!(m.metadata().terminal_size, (120, 40));
1539 }
1540
1541 #[test]
1542 fn event_recorder_finish_produces_valid_macro() {
1543 let mut rec = EventRecorder::new("full_test");
1544 rec.start();
1545 rec.record(&key_event('+'));
1546 rec.record(&key_event('+'));
1547 rec.record(&key_event('-'));
1548
1549 let m = rec.finish();
1550 assert_eq!(m.len(), 3);
1551 assert_eq!(m.metadata().name, "full_test");
1552
1553 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1555 sim.init();
1556 MacroPlayer::new(&m).replay_all(&mut sim);
1557 assert_eq!(sim.model().value, 1); }
1559
1560 #[test]
1561 fn event_recorder_record_with_delay() {
1562 let mut rec = EventRecorder::new("delayed");
1563 rec.start();
1564 assert!(rec.record_with_delay(&key_event('a'), Duration::from_millis(50)));
1565 assert!(rec.record_with_delay(&key_event('b'), Duration::from_millis(100)));
1566 assert_eq!(rec.event_count(), 2);
1567
1568 let m = rec.finish();
1569 assert_eq!(m.events()[0].delay, Duration::from_millis(50));
1570 assert_eq!(m.events()[1].delay, Duration::from_millis(100));
1571 }
1572
1573 #[test]
1574 fn event_recorder_record_with_delay_ignores_when_idle() {
1575 let mut rec = EventRecorder::new("test");
1576 assert!(!rec.record_with_delay(&key_event('a'), Duration::from_millis(50)));
1577 assert_eq!(rec.event_count(), 0);
1578 }
1579
1580 #[test]
1583 fn filter_default_accepts_all() {
1584 let filter = RecordingFilter::default();
1585 assert!(filter.accepts(&key_event('a')));
1586 assert!(filter.accepts(&Event::Resize {
1587 width: 80,
1588 height: 24
1589 }));
1590 assert!(filter.accepts(&Event::Focus(true)));
1591 }
1592
1593 #[test]
1594 fn filter_keys_only() {
1595 let filter = RecordingFilter::keys_only();
1596 assert!(filter.accepts(&key_event('a')));
1597 assert!(!filter.accepts(&Event::Resize {
1598 width: 80,
1599 height: 24
1600 }));
1601 assert!(!filter.accepts(&Event::Focus(true)));
1602 }
1603
1604 #[test]
1605 fn filter_custom() {
1606 let filter = RecordingFilter {
1607 keys: true,
1608 mouse: false,
1609 resize: false,
1610 paste: true,
1611 focus: false,
1612 };
1613 assert!(filter.accepts(&key_event('a')));
1614 assert!(!filter.accepts(&Event::Resize {
1615 width: 80,
1616 height: 24
1617 }));
1618 assert!(!filter.accepts(&Event::Focus(false)));
1619 }
1620
1621 #[test]
1624 fn filtered_recorder_records_matching_events() {
1625 let mut rec = FilteredEventRecorder::new("filtered", RecordingFilter::default());
1626 rec.start();
1627 assert!(rec.record(&key_event('a')));
1628 assert_eq!(rec.event_count(), 1);
1629 assert_eq!(rec.filtered_count(), 0);
1630 }
1631
1632 #[test]
1633 fn filtered_recorder_skips_filtered_events() {
1634 let mut rec = FilteredEventRecorder::new("keys_only", RecordingFilter::keys_only());
1635 rec.start();
1636 assert!(rec.record(&key_event('a')));
1637 assert!(!rec.record(&Event::Focus(true)));
1638 assert!(!rec.record(&Event::Resize {
1639 width: 100,
1640 height: 50
1641 }));
1642 assert!(rec.record(&key_event('b')));
1643
1644 assert_eq!(rec.event_count(), 2);
1645 assert_eq!(rec.filtered_count(), 2);
1646 }
1647
1648 #[test]
1649 fn filtered_recorder_finish_produces_macro() {
1650 let mut rec = FilteredEventRecorder::new("test", RecordingFilter::keys_only());
1651 rec.start();
1652 rec.record(&key_event('+'));
1653 rec.record(&Event::Focus(true)); rec.record(&key_event('+'));
1655
1656 let m = rec.finish();
1657 assert_eq!(m.len(), 2);
1658
1659 let mut sim = ProgramSimulator::new(Counter { value: 0 });
1660 sim.init();
1661 MacroPlayer::new(&m).replay_all(&mut sim);
1662 assert_eq!(sim.model().value, 2);
1663 }
1664
1665 #[test]
1666 fn filtered_recorder_pause_resume() {
1667 let mut rec = FilteredEventRecorder::new("test", RecordingFilter::default());
1668 rec.start();
1669 rec.record(&key_event('a'));
1670 rec.pause();
1671 assert!(!rec.record(&key_event('b'))); rec.resume();
1673 rec.record(&key_event('c'));
1674 assert_eq!(rec.event_count(), 2);
1675 }
1676
1677 #[test]
1678 fn filtered_recorder_with_terminal_size() {
1679 let mut rec = FilteredEventRecorder::new("sized", RecordingFilter::default())
1680 .with_terminal_size(200, 60);
1681 rec.start();
1682 rec.record(&key_event('x'));
1683 let m = rec.finish();
1684 assert_eq!(m.metadata().terminal_size, (200, 60));
1685 }
1686
1687 #[derive(Default)]
1690 struct EventSink {
1691 events: Vec<Event>,
1692 }
1693
1694 #[derive(Debug, Clone)]
1695 struct EventMsg(Event);
1696
1697 impl From<Event> for EventMsg {
1698 fn from(event: Event) -> Self {
1699 Self(event)
1700 }
1701 }
1702
1703 impl Model for EventSink {
1704 type Message = EventMsg;
1705
1706 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1707 self.events.push(msg.0);
1708 Cmd::none()
1709 }
1710
1711 fn view(&self, _frame: &mut Frame) {}
1712 }
1713
1714 proptest! {
1715 #[test]
1716 fn recorder_with_explicit_delays_roundtrips(pairs in proptest::collection::vec((0u8..=25, 0u16..=2000), 0..32)) {
1717 let mut recorder = MacroRecorder::new("prop").with_terminal_size(80, 24);
1718 let mut expected_total = Duration::ZERO;
1719 let mut expected_events = Vec::with_capacity(pairs.len());
1720
1721 for (ch_idx, delay_ms) in &pairs {
1722 let ch = char::from(b'a' + *ch_idx);
1723 let delay = Duration::from_millis(*delay_ms as u64);
1724 expected_total += delay;
1725 let ev = key_event(ch);
1726 expected_events.push(ev.clone());
1727 recorder.record_event_with_delay(ev, delay);
1728 }
1729
1730 let m = recorder.finish();
1731 prop_assert_eq!(m.len(), pairs.len());
1732 prop_assert_eq!(m.metadata().terminal_size, (80, 24));
1733 prop_assert_eq!(m.total_duration(), expected_total);
1734 prop_assert_eq!(m.bare_events(), expected_events);
1735 }
1736
1737 #[test]
1738 fn player_replays_events_in_order(pairs in proptest::collection::vec((0u8..=25, 0u16..=2000), 0..32)) {
1739 let mut timed = Vec::with_capacity(pairs.len());
1740 let mut total = Duration::ZERO;
1741 let mut expected_events = Vec::with_capacity(pairs.len());
1742
1743 for (ch_idx, delay_ms) in &pairs {
1744 let ch = char::from(b'a' + *ch_idx);
1745 let delay = Duration::from_millis(*delay_ms as u64);
1746 total += delay;
1747 let ev = key_event(ch);
1748 expected_events.push(ev.clone());
1749 timed.push(TimedEvent::new(ev, delay));
1750 }
1751
1752 let m = InputMacro::new(timed, MacroMetadata {
1753 name: "prop".to_string(),
1754 terminal_size: (80, 24),
1755 total_duration: total,
1756 });
1757
1758 let mut sim = ProgramSimulator::new(EventSink::default());
1759 sim.init();
1760 let mut player = MacroPlayer::new(&m);
1761 player.replay_all(&mut sim);
1762
1763 prop_assert_eq!(sim.model().events.clone(), expected_events);
1764 prop_assert_eq!(player.elapsed(), total);
1765 }
1766 }
1767}