use std::cfg_select;
use std::sync::Arc;
use thiserror::Error;
pub use openlogi_core::app::ForegroundApp;
pub use openlogi_core::binding::ButtonId;
pub use openlogi_core::config::KeyModifiers;
pub use openlogi_core::scroll::ScrollDelta;
mod pointer;
pub use pointer::{
PointerContext, PointerTarget, pointer_context, pointer_context_supported,
pointer_target_is_focused,
};
pub const LOGITECH_VENDOR_ID: u32 = openlogi_core::hid::LOGITECH_VENDOR_ID as u32;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CursorPosition {
pub x: f64,
pub y: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EventDevice {
pub vendor_id: Option<u32>,
pub product_id: Option<u32>,
pub product_name: Option<String>,
}
impl EventDevice {
#[must_use]
pub fn is_trackpad_like(&self) -> bool {
self.product_name.as_deref().is_some_and(|n| {
let n = n.to_ascii_lowercase();
n.contains("trackpad") || n.contains("touchpad") || n.contains("touch pad")
})
}
#[must_use]
pub fn is_logitech(&self) -> bool {
if self.vendor_id == Some(LOGITECH_VENDOR_ID) {
return true;
}
self.product_name.as_deref().is_some_and(|n| {
let n = n.to_ascii_lowercase();
n.contains("logitech") || n.starts_with("logi ")
})
}
}
#[must_use]
pub fn source_is_remappable(device: Option<&EventDevice>) -> bool {
match device {
Some(d) if d.is_trackpad_like() => false,
Some(d) => d.is_logitech(),
None => false,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeyEvent {
pub keycode: u16,
pub pressed: bool,
pub modifiers: KeyModifiers,
}
#[derive(Clone, Debug)]
pub enum HookEvent {
Mouse(MouseEvent),
Key(KeyEvent),
}
#[derive(Clone, Debug)]
pub enum MouseEvent {
Button {
id: ButtonId,
pressed: bool,
device: Option<EventDevice>,
},
Scroll {
delta: ScrollDelta,
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),
#[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,
#[error("Linux input error: {0}")]
Linux(#[source] std::io::Error),
#[error("Windows mouse hook setup failed: {0}")]
WindowsHook(String),
}
trait HookBackend {
type Running;
fn start(
cb: impl Fn(HookEvent) -> EventDisposition + Send + Sync + 'static,
) -> Result<Self::Running, HookError>;
fn stop(running: Self::Running);
fn is_running(_running: &Self::Running) -> bool {
true
}
fn has_accessibility() -> bool {
true
}
fn prompt_accessibility() {}
fn list_event_taps() -> Vec<EventTapInfo> {
Vec::new()
}
fn frontmost_app() -> Option<ForegroundApp> {
None
}
fn cursor_position() -> Option<CursorPosition> {
None
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
struct Unsupported;
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
impl HookBackend for Unsupported {
type Running = std::convert::Infallible;
fn start(
_cb: impl Fn(HookEvent) -> EventDisposition + Send + Sync + 'static,
) -> Result<Self::Running, HookError> {
Err(HookError::Unsupported)
}
fn stop(running: Self::Running) {
match running {}
}
}
cfg_select! {
target_os = "macos" => { type Backend = macos::Backend; }
target_os = "linux" => { type Backend = linux::Backend; }
target_os = "windows" => { type Backend = windows::Backend; }
_ => { type Backend = Unsupported; }
}
pub struct Hook {
inner: Option<<Backend as HookBackend>::Running>,
}
impl Drop for Hook {
fn drop(&mut self) {
self.shutdown();
}
}
impl Hook {
pub fn start(
cb: impl Fn(HookEvent) -> EventDisposition + Send + Sync + 'static,
) -> Result<Self, HookError> {
Backend::start(cb).map(|inner| Self { inner: Some(inner) })
}
pub fn stop(mut self) {
self.shutdown();
}
#[must_use]
pub fn is_running(&self) -> bool {
self.inner.as_ref().is_some_and(Backend::is_running)
}
fn shutdown(&mut self) {
if let Some(inner) = self.inner.take() {
Backend::stop(inner);
}
}
#[must_use]
pub fn has_accessibility() -> bool {
Backend::has_accessibility()
}
pub fn prompt_accessibility() {
Backend::prompt_accessibility();
}
#[must_use]
pub fn list_event_taps() -> Vec<EventTapInfo> {
Backend::list_event_taps()
}
}
#[must_use]
pub fn frontmost_application() -> Option<ForegroundApp> {
Backend::frontmost_app()
}
#[must_use]
pub fn frontmost_safari_pid() -> Option<i32> {
#[cfg(target_os = "macos")]
{
macos::frontmost_safari_pid()
}
#[cfg(not(target_os = "macos"))]
{
None
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ForegroundApplicationObserverError {
#[error("foreground-application observation is unsupported on this platform")]
Unsupported,
#[error("foreground-application observer failed: {0}")]
Platform(String),
}
#[must_use]
pub struct ForegroundApplicationObserver {
#[cfg(target_os = "macos")]
platform: macos::ForegroundApplicationObserver,
#[cfg(target_os = "linux")]
platform: linux::ForegroundApplicationObserver,
#[cfg(target_os = "windows")]
platform: windows::foreground::ForegroundApplicationObserver,
}
impl ForegroundApplicationObserver {
pub fn check_health(&self) -> Result<(), ForegroundApplicationObserverError> {
#[cfg(target_os = "linux")]
{
self.platform
.check_health()
.map_err(|error| ForegroundApplicationObserverError::Platform(error.to_owned()))
}
#[cfg(target_os = "windows")]
{
self.platform
.check_health()
.map_err(|error| ForegroundApplicationObserverError::Platform(error.to_string()))
}
#[cfg(target_os = "macos")]
{
let _ = &self.platform;
Ok(())
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Err(ForegroundApplicationObserverError::Unsupported)
}
}
}
pub fn watch_frontmost_application_changes(
on_change: impl Fn() + Send + Sync + 'static,
) -> Result<ForegroundApplicationObserver, ForegroundApplicationObserverError> {
let on_change: Arc<dyn Fn() + Send + Sync> = Arc::new(on_change);
#[cfg(target_os = "macos")]
{
let native_callback = Arc::clone(&on_change);
let platform = macos::watch_frontmost_application_activations(move |_| native_callback());
on_change();
Ok(ForegroundApplicationObserver { platform })
}
#[cfg(target_os = "linux")]
{
let platform = linux::watch_frontmost_application_activations(move |_| on_change())
.map_err(|error| ForegroundApplicationObserverError::Platform(error.to_string()))?;
Ok(ForegroundApplicationObserver { platform })
}
#[cfg(target_os = "windows")]
{
let platform = windows::foreground::watch_frontmost_application_activations(move |_| {
on_change();
})
.map_err(|error| ForegroundApplicationObserverError::Platform(error.to_string()))?;
Ok(ForegroundApplicationObserver { platform })
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let _ = on_change;
Err(ForegroundApplicationObserverError::Unsupported)
}
}
#[cfg(target_os = "macos")]
pub fn watch_frontmost_application_activations(
on_activation: impl Fn(Option<ForegroundApp>) + Send + Sync + 'static,
) -> ForegroundApplicationObserver {
ForegroundApplicationObserver {
platform: macos::watch_frontmost_application_activations(on_activation),
}
}
#[must_use]
pub fn cursor_position() -> Option<CursorPosition> {
Backend::cursor_position()
}
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(any(target_os = "windows", test))]
mod windows;
#[cfg(test)]
mod tests;