use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
use enigo::{Coordinate, Direction, Enigo, Key, Keyboard, Mouse, Settings};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ActivityError {
#[error("failed to initialize input simulation: {0}")]
Connection(#[from] enigo::NewConError),
#[error("failed to simulate input: {0}")]
Input(#[from] enigo::InputError),
#[error("activity thread terminated unexpectedly")]
Terminated,
}
const STOP_POLL_INTERVAL: Duration = Duration::from_millis(100);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ActivityMethod {
#[default]
Mouse,
Key,
}
pub struct ActivitySimulator {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl ActivitySimulator {
pub fn start(method: ActivityMethod, interval: Duration) -> Result<Self, ActivityError> {
let stop = Arc::new(AtomicBool::new(false));
let stop_worker = Arc::clone(&stop);
let (init_tx, init_rx) = mpsc::channel();
let handle = thread::spawn(move || {
let mut enigo = match Enigo::new(&Settings::default()) {
Ok(enigo) => {
if init_tx.send(Ok(())).is_err() {
return;
}
enigo
}
Err(e) => {
let _ = init_tx.send(Err(ActivityError::from(e)));
return;
}
};
run(&mut enigo, method, interval, &stop_worker);
});
match init_rx.recv() {
Ok(Ok(())) => Ok(Self {
stop,
handle: Some(handle),
}),
Ok(Err(e)) => {
let _ = handle.join();
Err(e)
}
Err(_) => {
let _ = handle.join();
Err(ActivityError::Terminated)
}
}
}
}
impl Drop for ActivitySimulator {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
fn run(enigo: &mut Enigo, method: ActivityMethod, interval: Duration, stop: &AtomicBool) {
while !stop.load(Ordering::Relaxed) {
let result = match method {
ActivityMethod::Mouse => nudge_mouse(enigo),
ActivityMethod::Key => tap_key(enigo),
};
if let Err(e) = result {
eprintln!("keep-active: activity simulation stopped: {e}");
return;
}
sleep_interruptible(interval, stop);
}
}
fn nudge_mouse(enigo: &mut Enigo) -> Result<(), ActivityError> {
enigo.move_mouse(1, 0, Coordinate::Rel)?;
thread::sleep(Duration::from_millis(50));
enigo.move_mouse(-1, 0, Coordinate::Rel)?;
Ok(())
}
fn tap_key(enigo: &mut Enigo) -> Result<(), ActivityError> {
enigo.key(Key::F15, Direction::Click)?;
Ok(())
}
fn sleep_interruptible(total: Duration, stop: &AtomicBool) {
let mut remaining = total;
while !remaining.is_zero() {
if stop.load(Ordering::Relaxed) {
return;
}
let nap = remaining.min(STOP_POLL_INTERVAL);
thread::sleep(nap);
remaining -= nap;
}
}