use crate::Hook;
use crate::error::{Error, Result};
use crate::event::{Event, EventType};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordedEvent {
pub elapsed: Duration,
pub event: Event,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recording {
pub events: Vec<RecordedEvent>,
pub created_at: SystemTime,
pub description: Option<String>,
}
impl Recording {
pub fn new() -> Self {
Self {
events: Vec::new(),
created_at: SystemTime::now(),
description: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn duration(&self) -> Duration {
self.events
.last()
.map(|e| e.elapsed)
.unwrap_or(Duration::ZERO)
}
pub fn event_count(&self) -> usize {
self.events.len()
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let json = serde_json::to_string_pretty(self)
.map_err(|e| Error::Other(format!("Failed to serialize recording: {}", e)))?;
std::fs::write(path, json)
.map_err(|e| Error::Other(format!("Failed to write recording file: {}", e)))?;
Ok(())
}
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let json = std::fs::read_to_string(path)
.map_err(|e| Error::Other(format!("Failed to read recording file: {}", e)))?;
let recording: Recording = serde_json::from_str(&json)
.map_err(|e| Error::Other(format!("Failed to deserialize recording: {}", e)))?;
Ok(recording)
}
pub fn playback(&self) -> Result<()> {
self.playback_with_speed(1.0)
}
pub fn playback_with_speed(&self, speed: f64) -> Result<()> {
if speed <= 0.0 {
return Err(Error::Other("Playback speed must be positive".into()));
}
if self.events.is_empty() {
return Ok(());
}
let start = Instant::now();
let mut _last_elapsed = Duration::ZERO;
for recorded in &self.events {
match recorded.event.event_type {
EventType::HookEnabled | EventType::HookDisabled => continue,
_ => {}
}
let target_elapsed = recorded.elapsed.as_secs_f64() / speed;
let target_duration = Duration::from_secs_f64(target_elapsed);
let elapsed = start.elapsed();
if target_duration > elapsed {
std::thread::sleep(target_duration - elapsed);
}
crate::platform::simulate(&recorded.event)?;
_last_elapsed = recorded.elapsed;
}
Ok(())
}
pub fn playback_fast(&self) -> Result<()> {
for recorded in &self.events {
match recorded.event.event_type {
EventType::HookEnabled | EventType::HookDisabled => continue,
_ => {}
}
crate::platform::simulate(&recorded.event)?;
}
Ok(())
}
}
impl Default for Recording {
fn default() -> Self {
Self::new()
}
}
pub struct EventRecorder {
recording: Arc<Mutex<Option<Recording>>>,
start_time: Arc<Mutex<Option<Instant>>>,
hook: Option<Hook>,
running: Arc<AtomicBool>,
}
impl EventRecorder {
pub fn new() -> Self {
Self {
recording: Arc::new(Mutex::new(None)),
start_time: Arc::new(Mutex::new(None)),
hook: None,
running: Arc::new(AtomicBool::new(false)),
}
}
pub fn start_recording(&mut self) -> Result<()> {
if self.running.load(Ordering::SeqCst) {
return Err(Error::AlreadyRunning);
}
let recording = self.recording.clone();
let start_time = self.start_time.clone();
let running = self.running.clone();
{
let mut rec = recording
.lock()
.map_err(|_| Error::ThreadError("recording mutex poisoned".into()))?;
*rec = Some(Recording::new());
}
{
let mut time = start_time
.lock()
.map_err(|_| Error::ThreadError("time mutex poisoned".into()))?;
*time = Some(Instant::now());
}
let hook = Hook::new();
hook.run_async(move |event: &Event| {
if !running.load(Ordering::SeqCst) {
return;
}
match event.event_type {
EventType::HookEnabled | EventType::HookDisabled => return,
_ => {}
}
let elapsed = {
let time = start_time.lock();
match time {
Ok(t) => t.map(|instant| instant.elapsed()).unwrap_or(Duration::ZERO),
Err(_) => return, }
};
let recorded = RecordedEvent {
elapsed,
event: event.clone(),
};
if let Ok(ref mut r) = recording.lock()
&& let Some(ref mut rec) = **r
{
rec.events.push(recorded);
}
})?;
self.running.store(true, Ordering::SeqCst);
self.hook = Some(hook);
Ok(())
}
pub fn stop_recording(&mut self) -> Result<Recording> {
if !self.running.swap(false, Ordering::SeqCst) {
return Err(Error::NotRunning);
}
if let Some(hook) = self.hook.take() {
hook.stop()?;
}
let mut rec = self
.recording
.lock()
.map_err(|_| Error::ThreadError("recording mutex poisoned".into()))?;
rec.take()
.ok_or_else(|| Error::Other("No recording available".into()))
}
pub fn is_recording(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
pub fn record_for(duration: Duration) -> Result<Recording> {
let mut recorder = Self::new();
recorder.start_recording()?;
std::thread::sleep(duration);
recorder.stop_recording()
}
}
impl Default for EventRecorder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recording_new() {
let recording = Recording::new();
assert!(recording.events.is_empty());
assert_eq!(recording.duration(), Duration::ZERO);
assert_eq!(recording.event_count(), 0);
}
#[test]
fn test_recording_with_description() {
let recording = Recording::new().with_description("Test macro");
assert_eq!(recording.description, Some("Test macro".to_string()));
}
#[test]
fn test_recording_duration() {
let mut recording = Recording::new();
recording.events.push(RecordedEvent {
elapsed: Duration::from_secs(5),
event: Event::new(EventType::KeyPressed),
});
assert_eq!(recording.duration(), Duration::from_secs(5));
}
#[test]
fn test_save_load_roundtrip() {
let mut recording = Recording::new().with_description("Test");
recording.events.push(RecordedEvent {
elapsed: Duration::from_millis(100),
event: Event::key_pressed(crate::Key::KeyA, 30),
});
let temp_path = std::env::temp_dir().join("monio_test_recording.json");
recording.save(&temp_path).unwrap();
let loaded = Recording::load(&temp_path).unwrap();
assert_eq!(loaded.description, recording.description);
assert_eq!(loaded.event_count(), recording.event_count());
std::fs::remove_file(&temp_path).unwrap();
}
}