use std::io::Write;
use std::path::PathBuf;
use std::time::Instant;
#[derive(Debug, Clone, PartialEq)]
pub enum RobotAction {
Sleep(u64),
MouseMove(f32, f32),
MouseDown,
MouseUp,
Key(String),
}
#[cfg(feature = "robot")]
pub fn execute_actions(robot: &crate::Robot, actions: &[RobotAction]) {
for action in actions {
match action {
RobotAction::Sleep(ms) => {
std::thread::sleep(std::time::Duration::from_millis(*ms));
}
RobotAction::MouseMove(x, y) => {
let _ = robot.mouse_move(*x, *y);
}
RobotAction::MouseDown => {
let _ = robot.mouse_down();
}
RobotAction::MouseUp => {
let _ = robot.mouse_up();
}
RobotAction::Key(key) => {
let _ = robot.send_key(key);
}
}
}
}
#[derive(Debug, Clone)]
pub enum RecordedEvent {
MouseMove {
time_ms: u64,
x: f32,
y: f32,
},
MouseDown {
time_ms: u64,
},
MouseUp {
time_ms: u64,
},
KeyDown {
time_ms: u64,
key: String,
},
KeyUp {
time_ms: u64,
key: String,
},
}
pub struct InputRecorder {
start_time: Instant,
events: Vec<RecordedEvent>,
output_path: PathBuf,
last_mouse_pos: Option<(f32, f32)>,
}
impl InputRecorder {
pub fn new(output_path: impl Into<PathBuf>) -> Self {
let path = output_path.into();
eprintln!("[Recorder] Recording started - will save to {:?}", path);
Self {
start_time: Instant::now(),
events: Vec::new(),
output_path: path,
last_mouse_pos: None,
}
}
fn elapsed_ms(&self) -> u64 {
self.start_time.elapsed().as_millis() as u64
}
pub fn record_mouse_move(&mut self, x: f32, y: f32) {
if let Some((lx, ly)) = self.last_mouse_pos {
if (x - lx).abs() < 0.5 && (y - ly).abs() < 0.5 {
return;
}
}
self.last_mouse_pos = Some((x, y));
let time_ms = self.elapsed_ms();
self.events.push(RecordedEvent::MouseMove { time_ms, x, y });
}
pub fn record_mouse_down(&mut self) {
let time_ms = self.elapsed_ms();
self.events.push(RecordedEvent::MouseDown { time_ms });
}
pub fn record_mouse_up(&mut self) {
let time_ms = self.elapsed_ms();
self.events.push(RecordedEvent::MouseUp { time_ms });
}
pub fn record_key_down(&mut self, key: &str) {
let time_ms = self.elapsed_ms();
self.events.push(RecordedEvent::KeyDown {
time_ms,
key: key.to_string(),
});
}
pub fn record_key_up(&mut self, key: &str) {
let time_ms = self.elapsed_ms();
self.events.push(RecordedEvent::KeyUp {
time_ms,
key: key.to_string(),
});
}
pub fn finish(&self) -> std::io::Result<()> {
if self.events.is_empty() {
eprintln!("[Recorder] No events recorded, skipping file generation");
return Ok(());
}
eprintln!(
"[Recorder] Generating robot test with {} events to {:?}",
self.events.len(),
self.output_path
);
let mut file = std::fs::File::create(&self.output_path)?;
let actions = self.events_to_actions();
writeln!(file, "//! Auto-generated robot test from recording")?;
writeln!(file, "//! Generated at: {}", chrono_lite())?;
writeln!(file, "//! Events: {}", self.events.len())?;
writeln!(file, "//! Actions: {}", actions.len())?;
writeln!(file)?;
writeln!(
file,
"use cranpose::recorder::{{RobotAction, execute_actions}};"
)?;
writeln!(file, "use cranpose::AppLauncher;")?;
writeln!(file, "use RobotAction::*;")?;
writeln!(file, "use std::time::Duration;")?;
writeln!(file)?;
writeln!(file, "const ACTIONS: &[RobotAction] = &[")?;
for action in &actions {
let action_str = match action {
RobotAction::Sleep(ms) => format!(" Sleep({}),", ms),
RobotAction::MouseMove(x, y) => format!(" MouseMove({:.1}, {:.1}),", x, y),
RobotAction::MouseDown => " MouseDown,".to_string(),
RobotAction::MouseUp => " MouseUp,".to_string(),
RobotAction::Key(key) => format!(" Key(\"{}\".into()),", key),
};
writeln!(file, "{}", action_str)?;
}
writeln!(file, "];")?;
writeln!(file)?;
writeln!(file, "fn main() {{")?;
writeln!(file, " AppLauncher::new()")?;
writeln!(file, " .with_headless(true)")?;
writeln!(file, " .with_test_driver(|robot| {{")?;
writeln!(
file,
" std::thread::sleep(Duration::from_millis(500));"
)?;
writeln!(file, " let _ = robot.wait_for_idle();")?;
writeln!(file)?;
writeln!(file, " execute_actions(&robot, ACTIONS);")?;
writeln!(file)?;
writeln!(
file,
" std::thread::sleep(Duration::from_secs(1));"
)?;
writeln!(file, " let _ = robot.exit();")?;
writeln!(file, " }})")?;
writeln!(file, " .run(|| {{")?;
writeln!(
file,
" // TODO: Replace with your app's composable"
)?;
writeln!(file, " // desktop_app::app::combined_app();")?;
writeln!(file, " }});")?;
writeln!(file, "}}")?;
eprintln!("[Recorder] Robot test saved to {:?}", self.output_path);
Ok(())
}
fn events_to_actions(&self) -> Vec<RobotAction> {
let mut actions = Vec::new();
let mut last_time_ms = 0u64;
for event in &self.events {
let (time_ms, action) = match event {
RecordedEvent::MouseMove { time_ms, x, y } => {
(*time_ms, Some(RobotAction::MouseMove(*x, *y)))
}
RecordedEvent::MouseDown { time_ms } => (*time_ms, Some(RobotAction::MouseDown)),
RecordedEvent::MouseUp { time_ms } => (*time_ms, Some(RobotAction::MouseUp)),
RecordedEvent::KeyDown { time_ms, key } => {
(*time_ms, Some(RobotAction::Key(key.clone())))
}
RecordedEvent::KeyUp { time_ms: _, key: _ } => {
continue;
}
};
let delta = time_ms.saturating_sub(last_time_ms);
if delta > 5 {
actions.push(RobotAction::Sleep(delta));
}
if let Some(action) = action {
actions.push(action);
}
last_time_ms = time_ms;
}
actions
}
}
impl Drop for InputRecorder {
fn drop(&mut self) {
if let Err(e) = self.finish() {
eprintln!("[Recorder] Failed to save recording: {}", e);
}
}
}
fn chrono_lite() -> String {
"timestamp".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
#[test]
fn test_recorder_generates_data_driven_code() {
let temp_path = std::env::temp_dir().join("test_recording.rs");
{
let mut recorder = InputRecorder::new(&temp_path);
recorder.record_mouse_move(100.0, 200.0);
recorder.record_mouse_down();
recorder.record_mouse_up();
}
let mut content = String::new();
std::fs::File::open(&temp_path)
.unwrap()
.read_to_string(&mut content)
.unwrap();
assert!(content.contains("use cranpose::recorder::{RobotAction, execute_actions};"));
assert!(content.contains("const ACTIONS: &[RobotAction] = &["));
assert!(content.contains("MouseMove(100.0, 200.0)"));
assert!(content.contains("MouseDown,"));
assert!(content.contains("MouseUp,"));
assert!(content.contains("execute_actions(&robot, ACTIONS);"));
std::fs::remove_file(&temp_path).ok();
}
#[test]
fn test_events_to_actions_conversion() {
let mut recorder = InputRecorder::new("/dev/null");
recorder.events.push(RecordedEvent::MouseMove {
time_ms: 0,
x: 100.0,
y: 200.0,
});
recorder
.events
.push(RecordedEvent::MouseDown { time_ms: 50 });
recorder.events.push(RecordedEvent::MouseMove {
time_ms: 60,
x: 150.0,
y: 250.0,
});
recorder
.events
.push(RecordedEvent::MouseUp { time_ms: 100 });
let actions = recorder.events_to_actions();
assert_eq!(actions.len(), 7);
assert!(matches!(actions[0], RobotAction::MouseMove(100.0, 200.0)));
assert!(matches!(actions[1], RobotAction::Sleep(50)));
assert!(matches!(actions[2], RobotAction::MouseDown));
assert!(matches!(actions[3], RobotAction::Sleep(10)));
assert!(matches!(actions[4], RobotAction::MouseMove(150.0, 250.0)));
assert!(matches!(actions[5], RobotAction::Sleep(40)));
assert!(matches!(actions[6], RobotAction::MouseUp));
}
#[test]
fn test_robot_action_enum() {
let actions = [
RobotAction::Sleep(100),
RobotAction::MouseMove(50.0, 75.5),
RobotAction::MouseDown,
RobotAction::MouseUp,
RobotAction::Key("Enter".to_string()),
];
assert_eq!(actions.len(), 5);
assert_eq!(actions[0], RobotAction::Sleep(100));
assert_eq!(actions[1], RobotAction::MouseMove(50.0, 75.5));
assert_eq!(actions[2], RobotAction::MouseDown);
assert_eq!(actions[3], RobotAction::MouseUp);
assert_eq!(actions[4], RobotAction::Key("Enter".to_string()));
}
}