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}