use std::cfg_select;
pub use openlogi_core::binding::ButtonId;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EventDevice {
pub vendor_id: Option<u32>,
pub product_id: Option<u32>,
pub product_name: Option<String>,
}
#[derive(Clone, Debug)]
pub enum MouseEvent {
Button {
id: ButtonId,
pressed: bool,
},
Scroll {
delta_x: f32,
delta_y: f32,
from_trackpad: bool,
device: Option<EventDevice>,
},
Moved {
delta_x: i32,
delta_y: i32,
},
CaptureInterrupted,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EventDisposition {
PassThrough,
Suppress,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TapLocation {
Hid,
Session,
AnnotatedSession,
Other(u32),
}
#[derive(Clone, Debug)]
pub struct EventTapInfo {
pub tap_id: u32,
pub location: TapLocation,
pub active: bool,
pub enabled: bool,
pub owner_pid: i32,
pub owner_name: Option<String>,
pub target_pid: Option<i32>,
}
impl EventTapInfo {
#[must_use]
pub fn gates_input(&self) -> bool {
self.active && self.enabled && self.location == TapLocation::Hid
}
#[must_use]
pub fn known_input_conflict(&self) -> Option<&'static str> {
const KNOWN: &[(&str, &str)] = &[
("logioptionsplus", "Logi Options+"),
("logioptions", "Logitech Options"),
("logimgr", "Logitech Options"),
("lccdaemon", "Logitech Control Center"),
("steermouse", "SteerMouse"),
("bettermouse", "BetterMouse"),
("usboverdrive", "USB Overdrive"),
("mac mouse fix", "Mac Mouse Fix"),
("linearmouse", "LinearMouse"),
("smoothscroll", "SmoothScroll"),
];
let name = self.owner_name.as_deref()?.to_ascii_lowercase();
KNOWN
.iter()
.find(|(needle, _)| name.contains(needle))
.map(|&(_, label)| label)
}
}
#[derive(Debug, thiserror::Error)]
pub enum HookError {
#[error("mouse event hook is not supported on this platform")]
Unsupported,
#[error(
"macOS Accessibility permission is required to capture mouse events; \
grant it in System Settings → Privacy & Security → Accessibility"
)]
AccessibilityDenied,
#[error("CGEventTap setup failed: {0}")]
MacOsTap(String),
#[cfg(target_os = "linux")]
#[error(
"no mouse device found under /dev/input; \
ensure a pointing device is connected and the process has read permission \
(add user to the `input` group or add a udev rule)"
)]
NoDeviceFound,
#[cfg(target_os = "linux")]
#[error("Linux input error: {0}")]
Linux(#[source] std::io::Error),
#[error("Windows mouse hook setup failed: {0}")]
WindowsHook(String),
}
pub struct Hook {
#[cfg(target_os = "macos")]
inner: Option<macos::HookInner>,
#[cfg(target_os = "linux")]
inner: Option<linux::HookInner>,
#[cfg(target_os = "windows")]
inner: Option<windows::HookInner>,
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
never: std::convert::Infallible,
}
impl Drop for Hook {
fn drop(&mut self) {
self.shutdown();
}
}
impl Hook {
pub fn start(
cb: impl Fn(MouseEvent) -> EventDisposition + Send + Sync + 'static,
) -> Result<Self, HookError> {
cfg_select! {
target_os = "macos" => {
macos::start(cb).map(|inner| Self { inner: Some(inner) })
}
target_os = "linux" => {
linux::start(cb).map(|inner| Self { inner: Some(inner) })
}
target_os = "windows" => {
windows::start(cb).map(|inner| Self { inner: Some(inner) })
}
_ => {
let _ = cb;
Err(HookError::Unsupported)
}
}
}
pub fn stop(mut self) {
self.shutdown();
}
fn shutdown(&mut self) {
cfg_select! {
target_os = "macos" => {
if let Some(inner) = self.inner.take() {
macos::stop(inner);
}
}
target_os = "linux" => {
if let Some(inner) = self.inner.take() {
linux::stop(inner);
}
}
target_os = "windows" => {
if let Some(inner) = self.inner.take() {
windows::stop(inner);
}
}
_ => {
}
}
}
#[must_use]
pub fn has_accessibility() -> bool {
cfg_select! {
target_os = "macos" => { macos::has_accessibility() }
_ => { true }
}
}
pub fn prompt_accessibility() {
cfg_select! {
target_os = "macos" => { macos::prompt_accessibility(); }
_ => {}
}
}
#[must_use]
pub fn list_event_taps() -> Vec<EventTapInfo> {
cfg_select! {
target_os = "macos" => { macos::list_event_taps() }
_ => { Vec::new() }
}
}
}
#[must_use]
pub fn frontmost_bundle_id() -> Option<String> {
cfg_select! {
target_os = "macos" => { macos::frontmost_bundle_id() }
target_os = "linux" => { linux::frontmost_bundle_id() }
target_os = "windows" => { windows::frontmost_process_path() }
_ => { None }
}
}
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(test)]
mod tests;