global_mousemove/lib.rs
1//! A minimal library to listen for global mousemove events.
2//!
3//! Supports Linux (X11), macOS, and Windows. See [`listen`] for details.
4
5use std::cell::RefCell;
6
7#[cfg(target_os = "linux")]
8mod linux;
9#[cfg(target_os = "macos")]
10mod macos;
11#[cfg(target_os = "windows")]
12mod windows;
13
14/// Errors that can occur when trying to listen for mousemove events.
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum ListenError {
18 #[cfg(target_os = "linux")]
19 #[error("Failed to connect to X display server")]
20 XOpenDisplay,
21 #[cfg(target_os = "linux")]
22 #[error("X Record extension does not exist")]
23 XRecordExtensionMissing,
24 #[cfg(target_os = "linux")]
25 #[error("Failed to allocate X RecordRange structure")]
26 XRecordAllocRange,
27 #[cfg(target_os = "linux")]
28 #[error("Failed to create X Record context")]
29 XRecordCreateContext,
30 #[cfg(target_os = "linux")]
31 #[error("Failed to enable X Record context")]
32 XRecordEnableContext,
33 #[cfg(target_os = "macos")]
34 #[error("Failed to create CGEvent tap")]
35 CGEventTap,
36 #[cfg(target_os = "macos")]
37 #[error("Failed to create CFRunLoop source")]
38 CFRunLoopSource,
39 #[cfg(target_os = "windows")]
40 #[error("Failed to install WH_MOUSE_LL hook, error code: {0}")]
41 WHMouseHook(u32),
42}
43
44/// A mousemove event.
45#[derive(Debug)]
46pub struct MouseMoveEvent {
47 /// The x-coordinate of the mouse pointer in physical pixels.
48 pub x: f64,
49 /// The y-coordinate of the mouse pointer in physical pixels.
50 pub y: f64,
51}
52
53type MouseMoveCallback = Box<dyn FnMut(MouseMoveEvent)>;
54
55thread_local! {
56 pub(crate) static GLOBAL_CALLBACK: RefCell<Option<MouseMoveCallback>> = RefCell::new(None);
57}
58
59/// Listen for global mousemove events.
60///
61/// ### OS Caveats
62///
63/// - **Linux**: Only X11 supported with the X Record extension.
64/// - **macOS**: The application may require "Input Monitoring" permissions to
65/// capture global mouse events. Calling this function may automatically
66/// prompt the user to grant these permissions.
67/// - **Windows**: No special requirements.
68///
69/// ### Examples
70///
71/// ```no_run
72/// global_mousemove::listen(|event| {
73/// println!("Mouse moved: ({}, {})", event.x, event.y);
74/// });
75/// ```
76///
77/// The above example listens for global mousemove events and prints the
78/// coordinates of the mouse pointer each time it moves. Note that this blocks
79/// the current thread. To run it in the background, consider spawning a
80/// separate thread and use a channel. For example:
81///
82/// ```no_run
83/// use std::sync::mpsc;
84///
85/// let (tx, rx) = mpsc::channel();
86///
87/// std::thread::spawn(move || {
88/// let _ = global_mousemove::listen(move |event| {
89/// let _ = tx.send(event);
90/// });
91/// });
92///
93/// while let Ok(event) = rx.recv() {
94/// println!("Mouse moved: ({}, {})", event.x, event.y);
95/// }
96/// ```
97///
98/// The listener will be stopped when the thread is terminated. There is
99/// currently no way to stop the listener gracefully. However, you can
100/// effectively stop it by having a very cheap check that short-circuits the
101/// callback. For example, we can use an atomic boolean:
102///
103/// ```no_run
104/// use std::sync::atomic::{AtomicBool, Ordering};
105/// use std::sync::Arc;
106///
107/// let listening = Arc::new(AtomicBool::new(true));
108/// let listening_cloned = Arc::clone(&listening);
109///
110/// std::thread::spawn(move || {
111/// let _ = global_mousemove::listen(move |event| {
112/// if !listening_cloned.load(Ordering::Relaxed) {
113/// return;
114/// }
115/// println!("Mouse moved: ({}, {})", event.x, event.y);
116/// });
117/// });
118///
119/// // To stop listening:
120/// // listening.store(false, Ordering::Relaxed);
121/// ```
122pub fn listen<T>(callback: T) -> Result<(), ListenError>
123where
124 T: FnMut(MouseMoveEvent) + 'static,
125{
126 #[cfg(target_os = "linux")]
127 crate::linux::listen(callback)?;
128 #[cfg(target_os = "macos")]
129 crate::macos::listen(callback)?;
130 #[cfg(target_os = "windows")]
131 crate::windows::listen(callback)?;
132
133 Ok(())
134}