use env_logger::Env;
use input_capture::InputCaptureError;
use input_emulation::InputEmulationError;
use mousehop::{
capture_test,
config::{self, Command, Config, ConfigError},
emulation_test,
service::{Service, ServiceError},
};
use mousehop_cli::CliError;
#[cfg(feature = "gtk")]
use mousehop_gtk::GtkError;
use mousehop_ipc::{IpcError, IpcListenerCreationError};
use std::{
future::Future,
io,
process::{self, Child},
};
use thiserror::Error;
use tokio::task::LocalSet;
#[derive(Debug, Error)]
enum MousehopError {
#[error(transparent)]
Service(#[from] ServiceError),
#[error(transparent)]
IpcError(#[from] IpcError),
#[error(transparent)]
Config(#[from] ConfigError),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Capture(#[from] InputCaptureError),
#[error(transparent)]
Emulation(#[from] InputEmulationError),
#[cfg(feature = "gtk")]
#[error(transparent)]
Gtk(#[from] GtkError),
#[error(transparent)]
Cli(#[from] CliError),
}
fn main() {
let env = Env::default().filter_or("MOUSEHOP_LOG_LEVEL", "info");
env_logger::init_from_env(env);
if let Err(e) = run() {
log::error!("{e}");
process::exit(1);
}
}
fn run() -> Result<(), MousehopError> {
let config = config::Config::new()?;
match config.command() {
Some(command) => match command {
Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?,
Command::TestCapture(args) => run_async(capture_test::run(config, args))?,
Command::Cli(cli_args) => run_async(mousehop_cli::run(cli_args))?,
Command::Daemon => {
match run_async(run_service(config)) {
Err(MousehopError::Service(ServiceError::IpcListen(
IpcListenerCreationError::AlreadyRunning,
))) => log::info!("service already running!"),
r => r?,
}
}
#[cfg(target_os = "macos")]
Command::AxProbe => {
let granted = mousehop::macos_tcc_probe::is_accessibility_granted();
process::exit(if granted { 0 } else { 1 });
}
},
None => {
#[cfg(feature = "gtk")]
{
let mut service = start_service()?;
let res = mousehop_gtk::run(config::local_commit());
#[cfg(unix)]
{
let pid = service.id() as libc::pid_t;
unsafe {
libc::kill(pid, libc::SIGINT);
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
loop {
match service.try_wait() {
Ok(Some(_)) => break,
Ok(None) if std::time::Instant::now() >= deadline => {
log::warn!(
"daemon child did not exit on SIGINT in 3s — sending SIGKILL"
);
let _ = service.kill();
let _ = service.wait();
break;
}
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(50)),
Err(e) => {
log::error!("waiting for daemon child: {e}");
break;
}
}
}
}
#[cfg(not(unix))]
{
let _ = service.kill();
let _ = service.wait();
}
res?;
}
#[cfg(not(feature = "gtk"))]
{
match run_async(run_service(config)) {
Err(MousehopError::Service(ServiceError::IpcListen(
IpcListenerCreationError::AlreadyRunning,
))) => log::info!("service already running!"),
r => r?,
}
}
}
}
Ok(())
}
fn run_async<F, E>(f: F) -> Result<(), MousehopError>
where
F: Future<Output = Result<(), E>>,
MousehopError: From<E>,
{
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
Ok(runtime.block_on(LocalSet::new().run_until(f))?)
}
fn start_service() -> Result<Child, io::Error> {
let child = process::Command::new(std::env::current_exe()?)
.args(std::env::args().skip(1))
.arg("daemon")
.spawn()?;
Ok(child)
}
async fn run_service(config: Config) -> Result<(), ServiceError> {
let release_bind = config.release_bind();
let config_path = config.config_path().to_owned();
let mut service = Service::new(config).await?;
log::info!("using config: {config_path:?}");
log::info!("Press {release_bind:?} to release the mouse");
#[cfg(target_os = "macos")]
mousehop::macos_tcc_watch::spawn();
service.run().await?;
log::info!("service exited!");
Ok(())
}