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