├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix └── src ├── bin └── main.rs ├── common.rs ├── darwin.rs ├── error.rs ├── lib.rs ├── nix ├── mod.rs ├── uinput.rs └── x11.rs └── windows.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mouce" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Emre Bicer "] 6 | license = "MIT" 7 | description = "A library that aims to help simulating and listening mouse actions across different platforms." 8 | homepage = "https://github.com/emrebicer/mouce" 9 | repository = "https://github.com/emrebicer/mouce" 10 | readme = "README.md" 11 | keywords = ["mouse", "events", "listen", "simulate", "click"] 12 | categories = ["accessibility", "command-line-utilities"] 13 | 14 | [profile.release] 15 | # Keep the opt-level at 0 for release mode, otherwise uinput module 16 | # does not work as expected. Currently there is no way to target only 17 | # unix-like systems, so this profle change applies to all systems. 18 | # 19 | # This change does not seem to affect the running speed (ran tests with different 20 | # opt-levels and benchmarked them with hyperfine, they all ran at very similar speeds). 21 | # But changing the opt-level results in a small increase on the executable size. 22 | opt-level = 0 23 | 24 | [features] 25 | default = ["x11"] 26 | cli = ["clap"] 27 | x11 = [] 28 | 29 | [lib] 30 | name = "mouce" 31 | path = "src/lib.rs" 32 | 33 | [[bin]] 34 | name = "mouce" 35 | path = "src/bin/main.rs" 36 | required-features = ["cli"] 37 | 38 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 39 | 40 | [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] 41 | glob = "0.3.0" 42 | 43 | [dependencies] 44 | clap = { version = "4.5.30", features = ["derive"], optional = true } 45 | thiserror = { version = "1.0.58" } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Emre Biçer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mouce 2 | Mouce is a library written in Rust that aims to help simulating and listening mouse actions across different platforms. 3 | ## Supported platforms 4 | - **Windows** ✅ 5 | - Tested on Windows 10 6 | - Uses User32 system library 7 | - **MacOS** ✅ 8 | - Tested on a MacBook Pro (Retina, 13-inch, Mid 2014) with Big Sur installed on it 9 | - Uses CoreGraphics and CoreFoundation frameworks 10 | - **Unix-like systems** 11 | - **X11** ✅ 12 | - Tested on i3wm Arch Linux 13 | - Uses X11 and XTest libraries 14 | - **Others (partially supported)** ❌ 15 | - For other systems, you can disable the x11 feature and the library will use **uinput** 16 | - Use `--no-default-features` argument with cargo 17 | - Or disable default features in `Cargo.toml` 18 | ```toml 19 | [dependencies] 20 | mouce = { version = "x.y.z", default-features = false } 21 | ``` 22 | - While using **uinput** there are some limitations for the library 23 | - ```get_position``` function is not implemented as **uinput** does not provide such a feature 24 | - The rest of the actions work and tested on KDE Wayland and sway 25 | ## Library interface 26 | ```rust 27 | /// Move the mouse to the given `x`, `y` coordinates in logical pixel space 28 | fn move_to(&self, x: i32, y: i32) -> Result<(), Error>; 29 | /// Move the mouse relative to the current position in logical pixel space 30 | fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<(), Error>; 31 | /// Get the current position of the mouse in logical pixel space 32 | fn get_position(&self) -> Result<(i32, i32), Error>; 33 | /// Press down the given mouse button 34 | fn press_button(&self, button: MouseButton) -> Result<(), Error>; 35 | /// Release the given mouse button 36 | fn release_button(&self, button: MouseButton) -> Result<(), Error>; 37 | /// Click the given mouse button 38 | fn click_button(&self, button: MouseButton) -> Result<(), Error>; 39 | /// Scroll the mouse wheel towards to the given direction 40 | fn scroll_wheel(&self, direction: ScrollDirection, scroll_unit: ScrollUnit, distance: u32) -> Result<(), Error>; 41 | /// Attach a callback function to mouse events 42 | fn hook(&mut self, callback: Box) -> Result; 43 | /// Remove the callback function with the given `CallbackId` 44 | fn unhook(&mut self, callback_id: CallbackId) -> Result<(), Error>; 45 | /// Remove all callback functions 46 | fn unhook_all(&mut self) -> Result<(), Error>; 47 | ``` 48 | ## Example 49 | This example program moves the mouse from left to right; 50 | ```rust 51 | use std::thread; 52 | use std::time::Duration; 53 | 54 | use mouce::{Mouse, MouseActions}; 55 | 56 | fn main() { 57 | let mouse_manager = Mouse::new(); 58 | 59 | let mut x = 0; 60 | while x < 1920 { 61 | let _ = mouse_manager.move_to(x, 540); 62 | x += 1; 63 | thread::sleep(Duration::from_millis(2)); 64 | } 65 | } 66 | ``` 67 | To see more examples, you can look at the documentation by running; 68 | ```fish 69 | cargo doc --open 70 | ``` 71 | ## CLI binary 72 | mouce comes with an example CLI program that uses mouce library functions. 73 | You can install the binary with; 74 | ```fish 75 | cargo install mouce --features="cli" 76 | ``` 77 | and see ```mouce --help``` for further details. 78 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 0, 6 | "narHash": "sha256-F7thesZPvAMSwjRu0K8uFshTk3ZZSNAsXTIFvXBT+34=", 7 | "path": "/nix/store/ljk3wvwqqzbmz0pys49ii18qmsi5aibc-source", 8 | "type": "path" 9 | }, 10 | "original": { 11 | "id": "nixpkgs", 12 | "type": "indirect" 13 | } 14 | }, 15 | "nixpkgs-unstable": { 16 | "locked": { 17 | "lastModified": 1733392399, 18 | "narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=", 19 | "owner": "nixos", 20 | "repo": "nixpkgs", 21 | "rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661", 22 | "type": "github" 23 | }, 24 | "original": { 25 | "owner": "nixos", 26 | "ref": "nixos-unstable", 27 | "repo": "nixpkgs", 28 | "type": "github" 29 | } 30 | }, 31 | "root": { 32 | "inputs": { 33 | "nixpkgs": "nixpkgs", 34 | "nixpkgs-unstable": "nixpkgs-unstable" 35 | } 36 | } 37 | }, 38 | "root": "root", 39 | "version": 7 40 | } 41 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Mouce development flake"; 3 | 4 | inputs = { 5 | nixpkgs-unstable.url = "github:nixos/nixpkgs?ref=nixos-unstable"; 6 | }; 7 | 8 | outputs = { self, nixpkgs, nixpkgs-unstable, ... }@inputs: 9 | 10 | let 11 | system = "x86_64-linux"; 12 | pkgs = nixpkgs-unstable.legacyPackages.${system}; 13 | in { 14 | devShells.${system}.default = pkgs.mkShellNoCC { 15 | buildInputs = with pkgs; [ 16 | gcc13 17 | xorg.libX11 18 | xorg.libXtst 19 | cargo 20 | rustc 21 | rust-analyzer 22 | clippy 23 | ]; 24 | }; 25 | 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use mouce::common::ScrollUnit; 3 | use mouce::MouseActions; 4 | use std::thread::sleep; 5 | use std::time::Duration; 6 | 7 | #[cfg(feature = "cli")] 8 | #[derive(Parser)] 9 | #[command( 10 | name = "mouce", 11 | about = "A CLI tool that simulates mouse actions using the mouce library", 12 | author = "Emre Bicer", 13 | version = env!("CARGO_PKG_VERSION") 14 | )] 15 | struct Cli { 16 | #[command(subcommand)] 17 | command: Commands, 18 | } 19 | 20 | #[cfg(feature = "cli")] 21 | #[derive(Subcommand)] 22 | enum Commands { 23 | /// Moves the mouse to the given position in logical pixel space 24 | MoveTo { 25 | #[arg(long, short)] 26 | x_position: i32, 27 | #[arg(long, short)] 28 | y_position: i32, 29 | }, 30 | /// Move the mouse relative to the current position in logical pixel space 31 | MoveRelative { 32 | #[arg(long, short)] 33 | x_offset: i32, 34 | #[arg(long, short)] 35 | y_offset: i32, 36 | }, 37 | /// Get the current position of the mouse in logical pixel space, outputs `x` and `y` coordinates separated with a space 38 | GetPosition, 39 | /// Press the given mouse button 40 | PressButton { 41 | #[arg(long, short)] 42 | button: String, 43 | }, 44 | /// Release the given mouse button 45 | ReleaseButton { 46 | #[arg(long, short)] 47 | button: String, 48 | }, 49 | /// Click the given mouse button 50 | ClickButton { 51 | #[arg(long, short)] 52 | button: String, 53 | }, 54 | /// Scroll the mouse wheel towards the given direction 55 | ScrollWheel { 56 | #[arg(long)] 57 | direction: String, 58 | #[arg(long)] 59 | distance: u32, 60 | #[arg(long, short)] 61 | unit: String, 62 | }, 63 | /// Listen mouse events and print them to the terminal 64 | Listen, 65 | /// Listen for left mouse button press and print the position when pressed 66 | GetPositionOnClick, 67 | } 68 | 69 | fn main() -> Result<(), Box> { 70 | let cli = Cli::parse(); 71 | let mut mouse_manager = mouce::Mouse::new(); 72 | 73 | match cli.command { 74 | Commands::MoveTo { 75 | x_position, 76 | y_position, 77 | } => { 78 | mouse_manager.move_to(x_position, y_position)?; 79 | } 80 | Commands::MoveRelative { x_offset, y_offset } => { 81 | mouse_manager.move_relative(x_offset, y_offset)?; 82 | } 83 | Commands::GetPosition => { 84 | let (x, y) = mouse_manager.get_position()?; 85 | println!("{x} {y}"); 86 | } 87 | Commands::PressButton { button } => { 88 | let button = get_mouse_button(&button)?; 89 | mouse_manager.press_button(button)?; 90 | } 91 | Commands::ReleaseButton { button } => { 92 | let button = get_mouse_button(&button)?; 93 | mouse_manager.release_button(button)?; 94 | } 95 | Commands::ClickButton { button } => { 96 | let button = get_mouse_button(&button)?; 97 | mouse_manager.click_button(button)?; 98 | } 99 | Commands::ScrollWheel { 100 | direction, 101 | distance, 102 | unit, 103 | } => { 104 | let direction = get_scroll_direction(&direction)?; 105 | let scroll_unit = match unit.as_str() { 106 | "pixel" | "p" => ScrollUnit::Pixel, 107 | "line" | "l" => ScrollUnit::Line, 108 | _ => { 109 | return Err(Box::new(std::io::Error::new( 110 | std::io::ErrorKind::InvalidInput, 111 | "scroll unit must be pixel or line", 112 | ))) 113 | } 114 | }; 115 | mouse_manager.scroll_wheel(direction, scroll_unit, distance)?; 116 | } 117 | Commands::Listen => { 118 | mouse_manager.hook(Box::new(|event| { 119 | println!("{:?}", event); 120 | }))?; 121 | loop { 122 | sleep(Duration::from_secs(u64::max_value())); 123 | } 124 | } 125 | Commands::GetPositionOnClick => { 126 | let manager_clone = mouse_manager.clone(); 127 | mouse_manager.hook(Box::new(move |e| { 128 | match e { 129 | mouce::common::MouseEvent::Press(mouce::common::MouseButton::Left) => { 130 | match manager_clone.get_position() { 131 | Ok((x, y)) => println!("{x} {y}"), 132 | Err(err) => println!("Failed to get current position: {err}"), 133 | } 134 | } 135 | _ => {} 136 | }; 137 | }))?; 138 | loop { 139 | sleep(Duration::from_secs(u64::max_value())); 140 | } 141 | } 142 | } 143 | 144 | Ok(()) 145 | } 146 | 147 | fn get_mouse_button( 148 | button: &str, 149 | ) -> Result> { 150 | match button { 151 | "left" => Ok(mouce::common::MouseButton::Left), 152 | "right" => Ok(mouce::common::MouseButton::Right), 153 | "middle" => Ok(mouce::common::MouseButton::Middle), 154 | _ => Err(Box::new(std::io::Error::new( 155 | std::io::ErrorKind::InvalidInput, 156 | format!( 157 | "{} is not accepted as a button, please use left, right or middle", 158 | button 159 | ), 160 | ))), 161 | } 162 | } 163 | 164 | fn get_scroll_direction( 165 | direction: &str, 166 | ) -> Result> { 167 | match direction { 168 | "up" => Ok(mouce::common::ScrollDirection::Up), 169 | "down" => Ok(mouce::common::ScrollDirection::Down), 170 | "right" => Ok(mouce::common::ScrollDirection::Right), 171 | "left" => Ok(mouce::common::ScrollDirection::Left), 172 | _ => Err(Box::new(std::io::Error::new( 173 | std::io::ErrorKind::InvalidInput, 174 | format!( 175 | "{} is not accepted as a direction, please use up, down, right or left", 176 | direction 177 | ), 178 | ))), 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | 3 | pub type CallbackId = u8; 4 | 5 | #[derive(Debug, Copy, Clone)] 6 | pub enum MouseButton { 7 | Left, 8 | Middle, 9 | Right, 10 | } 11 | 12 | #[derive(Debug, Copy, Clone)] 13 | pub enum ScrollDirection { 14 | Up, 15 | Down, 16 | Right, 17 | Left, 18 | } 19 | 20 | #[derive(Debug, Copy, Clone)] 21 | pub enum MouseEvent { 22 | RelativeMove(i32, i32), 23 | AbsoluteMove(i32, i32), 24 | Press(MouseButton), 25 | Release(MouseButton), 26 | Scroll(ScrollDirection, u32), 27 | } 28 | 29 | #[derive(Debug, Copy, Clone)] 30 | pub enum ScrollUnit { 31 | Pixel, 32 | Line, 33 | } 34 | 35 | pub trait MouseActions { 36 | /// Move the mouse to the given `x`, `y` coordinates in logical pixel space 37 | /// 38 | /// # Examples 39 | /// 40 | /// ```rust,no_run 41 | /// use mouce::Mouse; 42 | /// use mouce::MouseActions; 43 | /// 44 | /// let manager = Mouse::new(); 45 | /// assert_eq!(manager.move_to(0, 0), Ok(())); 46 | /// ``` 47 | fn move_to(&self, x: i32, y: i32) -> Result<(), Error>; 48 | /// Move the mouse relative to the current position in logical pixel space 49 | /// 50 | /// # Examples 51 | /// 52 | /// ```rust,no_run 53 | /// use mouce::Mouse; 54 | /// use mouce::MouseActions; 55 | /// 56 | /// let manager = Mouse::new(); 57 | /// assert_eq!(manager.move_relative(100, 100), Ok(())); 58 | /// ``` 59 | fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<(), Error>; 60 | /// Get the current position of the mouse in logical pixel space 61 | /// 62 | /// # Examples 63 | /// 64 | /// 65 | /// ```rust,no_run 66 | /// use mouce::Mouse; 67 | /// use mouce::MouseActions; 68 | /// use mouce::error::Error; 69 | /// 70 | /// let manager = Mouse::new(); 71 | /// manager.move_to(0, 0); 72 | /// // This function may not be implemented on some platforms such as Linux Wayland 73 | /// let valid_outs = vec![Ok((0, 0)), Err(Error::NotImplemented)]; 74 | /// assert!(valid_outs.contains(&manager.get_position())); 75 | /// ``` 76 | fn get_position(&self) -> Result<(i32, i32), Error>; 77 | /// Press down the given mouse button 78 | /// 79 | /// # Examples 80 | /// 81 | /// ```rust,no_run 82 | /// use mouce::Mouse; 83 | /// use mouce::MouseActions; 84 | /// use mouce::common::MouseButton; 85 | /// 86 | /// let manager = Mouse::new(); 87 | /// assert_eq!(manager.press_button(&MouseButton::Left), Ok(())); 88 | /// ``` 89 | fn press_button(&self, button: MouseButton) -> Result<(), Error>; 90 | /// Release the given mouse button 91 | /// 92 | /// # Examples 93 | /// 94 | /// ```rust,no_run 95 | /// use mouce::Mouse; 96 | /// use mouce::MouseActions; 97 | /// use mouce::common::MouseButton; 98 | /// 99 | /// let manager = Mouse::new(); 100 | /// assert_eq!(manager.release_button(&MouseButton::Left), Ok(())); 101 | /// ``` 102 | fn release_button(&self, button: MouseButton) -> Result<(), Error>; 103 | /// Click the given mouse button 104 | /// 105 | /// # Examples 106 | /// 107 | /// ```rust,no_run 108 | /// use mouce::Mouse; 109 | /// use mouce::MouseActions; 110 | /// use mouce::common::MouseButton; 111 | /// 112 | /// let manager = Mouse::new(); 113 | /// assert_eq!(manager.click_button(&MouseButton::Left), Ok(())); 114 | /// ``` 115 | fn click_button(&self, button: MouseButton) -> Result<(), Error>; 116 | /// Scroll the mouse wheel towards to the given direction 117 | /// 118 | /// All platforms allow scrolling with the Line unit, 119 | /// however only darwin and windows support pixel scrolling 120 | /// 121 | /// # Examples 122 | /// 123 | /// ```rust,no_run 124 | /// use mouce::Mouse; 125 | /// use mouce::MouseActions; 126 | /// use mouce::common::ScrollDirection; 127 | /// use std::{thread, time}; 128 | /// 129 | /// let manager = Mouse::new(); 130 | /// let sleep_duration = time::Duration::from_millis(250); 131 | /// 132 | /// for _ in 0..5 { 133 | /// assert_eq!(manager.scroll_wheel(&ScrollDirection::Down, 1), Ok(())); 134 | /// thread::sleep(sleep_duration); 135 | /// } 136 | /// 137 | /// for _ in 0..5 { 138 | /// assert_eq!(manager.scroll_wheel(&ScrollDirection::Up, 1), Ok(())); 139 | /// thread::sleep(sleep_duration); 140 | /// } 141 | /// ``` 142 | fn scroll_wheel( 143 | &self, 144 | direction: ScrollDirection, 145 | scroll_unit: ScrollUnit, 146 | distance: u32, 147 | ) -> Result<(), Error>; 148 | /// Attach a callback function to mouse events 149 | /// 150 | /// # Examples 151 | /// 152 | /// ```rust,no_run 153 | /// use mouce::Mouse; 154 | /// use mouce::MouseActions; 155 | /// use mouce::error::Error; 156 | /// 157 | /// let mut manager = Mouse::new(); 158 | /// let hook_result = manager.hook(Box::new(|e| println!("New event: {:?}", e))); 159 | /// match hook_result { 160 | /// Ok(id) => { 161 | /// assert_eq!(manager.unhook(id), Ok(())); 162 | /// } 163 | /// // Hooking may require user privileges on some systems 164 | /// // e.g. requires super user for Linux 165 | /// Err(err) => assert_eq!(Error::PermissionDenied, err), 166 | /// } 167 | /// ``` 168 | fn hook(&mut self, callback: Box) -> Result; 169 | /// Remove the callback function with the given `CallbackId` 170 | fn unhook(&mut self, callback_id: CallbackId) -> Result<(), Error>; 171 | /// Remove all callback functions 172 | /// 173 | /// # Examples 174 | /// 175 | /// ```rust,no_run 176 | /// use mouce::Mouse; 177 | /// use mouce::MouseActions; 178 | /// 179 | /// let mut manager = Mouse::new(); 180 | /// assert_eq!(manager.unhook_all(), Ok(())); 181 | /// ``` 182 | fn unhook_all(&mut self) -> Result<(), Error>; 183 | } 184 | 185 | #[cfg(test)] 186 | mod tests { 187 | use crate::common::ScrollUnit; 188 | use crate::error::Error; 189 | use crate::MouseActions; 190 | use crate::{common::MouseButton, common::ScrollDirection, Mouse}; 191 | use std::sync::Mutex; 192 | use std::{thread, time}; 193 | 194 | // Prevent test running in parallel, 195 | static TEST_EXECUTER: Mutex = Mutex::new(Executor); 196 | #[derive(Clone, Copy)] 197 | struct Executor; 198 | 199 | impl Executor { 200 | fn run_test(self, f: impl FnOnce()) { 201 | f(); 202 | } 203 | } 204 | 205 | #[test] 206 | #[ignore] 207 | fn move_to_right_bottom() { 208 | TEST_EXECUTER.lock().unwrap().run_test(|| { 209 | let manager = Mouse::new(); 210 | assert_eq!(manager.move_to(1920, 1080), Ok(())); 211 | }); 212 | } 213 | 214 | #[test] 215 | #[ignore] 216 | fn move_to_top_left() { 217 | TEST_EXECUTER.lock().unwrap().run_test(|| { 218 | let manager = Mouse::new(); 219 | assert_eq!(manager.move_to(0, 0), Ok(())); 220 | }); 221 | } 222 | 223 | #[test] 224 | #[ignore] 225 | fn move_to_left_to_right() { 226 | TEST_EXECUTER.lock().unwrap().run_test(|| { 227 | let manager = Mouse::new(); 228 | let sleep_duration = time::Duration::from_millis(1); 229 | let mut x = 0; 230 | while x < 1920 { 231 | assert_eq!(manager.move_to(x, 540), Ok(())); 232 | x += 1; 233 | thread::sleep(sleep_duration); 234 | } 235 | }); 236 | } 237 | 238 | #[test] 239 | #[ignore] 240 | fn move_relative_left_to_right() { 241 | TEST_EXECUTER.lock().unwrap().run_test(|| { 242 | let manager = Mouse::new(); 243 | let sleep_duration = time::Duration::from_millis(1); 244 | let mut x = 0; 245 | assert_eq!(manager.move_to(0, 540), Ok(())); 246 | while x < 1920 { 247 | assert_eq!(manager.move_relative(1, 0), Ok(())); 248 | x += 1; 249 | thread::sleep(sleep_duration); 250 | } 251 | }); 252 | } 253 | 254 | #[test] 255 | #[ignore] 256 | fn move_to_top_to_bottom() { 257 | TEST_EXECUTER.lock().unwrap().run_test(|| { 258 | let manager = Mouse::new(); 259 | let sleep_duration = time::Duration::from_millis(1); 260 | let mut y = 0; 261 | while y < 1080 { 262 | assert_eq!(manager.move_to(960, y), Ok(())); 263 | y += 1; 264 | thread::sleep(sleep_duration); 265 | } 266 | }); 267 | } 268 | 269 | #[test] 270 | #[ignore] 271 | fn move_relative_top_to_bottom() { 272 | TEST_EXECUTER.lock().unwrap().run_test(|| { 273 | let manager = Mouse::new(); 274 | let sleep_duration = time::Duration::from_millis(1); 275 | let mut y = 0; 276 | assert_eq!(manager.move_to(960, 0), Ok(())); 277 | while y < 1080 { 278 | assert_eq!(manager.move_relative(0, 1), Ok(())); 279 | y += 1; 280 | thread::sleep(sleep_duration); 281 | } 282 | }); 283 | } 284 | 285 | #[test] 286 | #[ignore] 287 | fn get_position() { 288 | TEST_EXECUTER.lock().unwrap().run_test(|| { 289 | let manager = Mouse::new(); 290 | match manager.get_position() { 291 | Ok(_) => { 292 | let positions = vec![ 293 | (0, 0), 294 | (100, 100), 295 | (250, 250), 296 | (325, 325), 297 | (400, 100), 298 | (100, 400), 299 | ]; 300 | 301 | let mut x; 302 | let mut y; 303 | for position in positions.iter() { 304 | assert_eq!(manager.move_to(position.0, position.1), Ok(())); 305 | (x, y) = manager.get_position().unwrap(); 306 | assert_eq!(x, position.0 as i32); 307 | assert_eq!(y, position.1 as i32); 308 | } 309 | } 310 | Err(error) => assert_eq!(error, Error::NotImplemented), 311 | } 312 | }); 313 | } 314 | 315 | #[test] 316 | #[ignore] 317 | fn left_click() { 318 | TEST_EXECUTER.lock().unwrap().run_test(|| { 319 | let manager = Mouse::new(); 320 | assert_eq!(manager.click_button(MouseButton::Left), Ok(())); 321 | }); 322 | } 323 | 324 | #[test] 325 | #[ignore] 326 | fn scroll_down() { 327 | TEST_EXECUTER.lock().unwrap().run_test(|| { 328 | let manager = Mouse::new(); 329 | for _ in 0..10 { 330 | assert_eq!( 331 | manager.scroll_wheel(ScrollDirection::Down, ScrollUnit::Line, 1), 332 | Ok(()) 333 | ); 334 | let sleep_duration = time::Duration::from_millis(250); 335 | thread::sleep(sleep_duration); 336 | } 337 | }); 338 | } 339 | 340 | #[test] 341 | #[ignore] 342 | fn scroll_up() { 343 | TEST_EXECUTER.lock().unwrap().run_test(|| { 344 | let manager = Mouse::new(); 345 | for _ in 0..10 { 346 | assert_eq!( 347 | manager.scroll_wheel(ScrollDirection::Up, ScrollUnit::Line, 1), 348 | Ok(()) 349 | ); 350 | let sleep_duration = time::Duration::from_millis(250); 351 | thread::sleep(sleep_duration); 352 | } 353 | }); 354 | } 355 | 356 | #[test] 357 | #[ignore] 358 | fn scroll_right() { 359 | TEST_EXECUTER.lock().unwrap().run_test(|| { 360 | let manager = Mouse::new(); 361 | for _ in 0..10 { 362 | assert_eq!( 363 | manager.scroll_wheel(ScrollDirection::Right, ScrollUnit::Line, 1), 364 | Ok(()) 365 | ); 366 | let sleep_duration = time::Duration::from_millis(250); 367 | thread::sleep(sleep_duration); 368 | } 369 | }); 370 | } 371 | 372 | #[test] 373 | #[ignore] 374 | fn scroll_left() { 375 | TEST_EXECUTER.lock().unwrap().run_test(|| { 376 | let manager = Mouse::new(); 377 | for _ in 0..10 { 378 | assert_eq!( 379 | manager.scroll_wheel(ScrollDirection::Left, ScrollUnit::Line, 1), 380 | Ok(()) 381 | ); 382 | let sleep_duration = time::Duration::from_millis(250); 383 | thread::sleep(sleep_duration); 384 | } 385 | }); 386 | } 387 | 388 | #[test] 389 | #[ignore] 390 | fn hook_and_unhook() { 391 | TEST_EXECUTER.lock().unwrap().run_test(|| { 392 | let mut manager = Mouse::new(); 393 | assert_eq!(manager.unhook(5), Err(Error::UnhookFailed)); 394 | let hook_result = manager.hook(Box::new(|e| println!("{:?}", e))); 395 | match hook_result { 396 | Ok(id) => { 397 | assert_eq!(manager.unhook(id), Ok(())); 398 | 399 | manager.hook(Box::new(|e| println!("{:?}", e))).unwrap(); 400 | manager.hook(Box::new(|e| println!("{:?}", e))).unwrap(); 401 | manager.hook(Box::new(|e| println!("{:?}", e))).unwrap(); 402 | let id = manager.hook(Box::new(|e| println!("{:?}", e))).unwrap(); 403 | 404 | manager.unhook_all().unwrap(); 405 | assert_eq!(manager.unhook(id), Err(Error::UnhookFailed)); 406 | } 407 | Err(err) => assert_eq!(Error::PermissionDenied, err), 408 | } 409 | }); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/darwin.rs: -------------------------------------------------------------------------------- 1 | /// 2 | /// This module contains the mouse action functions 3 | /// for the darwin systems (MacOS) 4 | /// Uses the CoreGraphics (a.k.a Quartz) framework 5 | /// 6 | use crate::common::{ 7 | CallbackId, MouseActions, MouseButton, MouseEvent, ScrollDirection, ScrollUnit, 8 | }; 9 | use crate::error::Error; 10 | use std::collections::HashMap; 11 | use std::os::raw::{c_double, c_int, c_long, c_uint, c_ulong, c_void}; 12 | use std::ptr::null_mut; 13 | use std::sync::Mutex; 14 | use std::thread; 15 | 16 | static mut TAP_EVENT_REF: Option = None; 17 | static mut CALLBACKS: Option>>> = None; 18 | 19 | #[derive(Clone)] 20 | pub struct DarwinMouseManager { 21 | callback_counter: CallbackId, 22 | is_listening: bool, 23 | } 24 | 25 | impl DarwinMouseManager { 26 | pub fn new() -> Self { 27 | DarwinMouseManager { 28 | callback_counter: 0, 29 | is_listening: false, 30 | } 31 | } 32 | 33 | fn create_mouse_event( 34 | &self, 35 | event_type: CGEventType, 36 | mouse_button: CGMouseButton, 37 | ) -> Result<(), Error> { 38 | let (pos_x, pos_y) = self.get_position()?; 39 | let position = CGPoint { 40 | x: pos_x as c_double, 41 | y: pos_y as c_double, 42 | }; 43 | 44 | unsafe { 45 | let event = CGEventCreateMouseEvent(null_mut(), event_type, position, mouse_button); 46 | if event == null_mut() { 47 | return Err(Error::CGCouldNotCreateEvent); 48 | } 49 | CGEventPost(CGEventTapLocation::CGHIDEventTap, event); 50 | CFRelease(event as CFTypeRef); 51 | } 52 | 53 | Ok(()) 54 | } 55 | 56 | fn create_scroll_wheel_event( 57 | &self, 58 | distance: c_int, 59 | scroll_unit: ScrollUnit, 60 | direction: ScrollDirection, 61 | ) -> Result<(), Error> { 62 | unsafe { 63 | let unit = match scroll_unit { 64 | ScrollUnit::Pixel => CGScrollEventUnit::Pixel, 65 | ScrollUnit::Line => CGScrollEventUnit::Line, 66 | }; 67 | 68 | let event = match direction { 69 | ScrollDirection::Up | ScrollDirection::Down => { 70 | CGEventCreateScrollWheelEvent(null_mut(), unit, 2, distance, 0) 71 | } 72 | ScrollDirection::Right | ScrollDirection::Left => { 73 | CGEventCreateScrollWheelEvent(null_mut(), unit, 2, 0, distance) 74 | } 75 | }; 76 | 77 | if event == null_mut() { 78 | return Err(Error::CGCouldNotCreateEvent); 79 | } 80 | CGEventPost(CGEventTapLocation::CGHIDEventTap, event); 81 | CFRelease(event as CFTypeRef); 82 | } 83 | Ok(()) 84 | } 85 | 86 | fn start_listener(&mut self) -> Result<(), Error> { 87 | thread::spawn(move || { 88 | unsafe extern "C" fn mouse_on_event_callback( 89 | _proxy: *const c_void, 90 | event_type: CGEventType, 91 | cg_event: CGEventRef, 92 | _user_info: *mut c_void, 93 | ) -> CGEventRef { 94 | // Construct the library's MouseEvent 95 | let mouse_event = match event_type { 96 | CGEventType::LeftMouseDown => Some(MouseEvent::Press(MouseButton::Left)), 97 | CGEventType::LeftMouseUp => Some(MouseEvent::Release(MouseButton::Left)), 98 | CGEventType::RightMouseDown => Some(MouseEvent::Press(MouseButton::Right)), 99 | CGEventType::RightMouseUp => Some(MouseEvent::Release(MouseButton::Right)), 100 | CGEventType::OtherMouseDown => Some(MouseEvent::Press(MouseButton::Middle)), 101 | CGEventType::OtherMouseUp => Some(MouseEvent::Release(MouseButton::Middle)), 102 | CGEventType::MouseMoved => { 103 | let point = CGEventGetLocation(cg_event); 104 | Some(MouseEvent::AbsoluteMove(point.x as i32, point.y as i32)) 105 | } 106 | CGEventType::ScrollWheel => { 107 | // CGEventField::scrollWheelEventPointDeltaAxis1 = 96 108 | // CGEventField::scrollWheelEventPointDeltaAxis2 = 97 109 | let delta_y = CGEventGetIntegerValueField(cg_event, 96); 110 | let delta_x = CGEventGetIntegerValueField(cg_event, 97); 111 | if delta_y > 0 { 112 | Some(MouseEvent::Scroll(ScrollDirection::Up, delta_y as u32)) 113 | } else if delta_y < 0 { 114 | Some(MouseEvent::Scroll( 115 | ScrollDirection::Down, 116 | delta_y.unsigned_abs() as u32, 117 | )) 118 | } else if delta_x < 0 { 119 | Some(MouseEvent::Scroll( 120 | ScrollDirection::Right, 121 | delta_x.unsigned_abs() as u32, 122 | )) 123 | } else if delta_x > 0 { 124 | Some(MouseEvent::Scroll(ScrollDirection::Left, delta_x as u32)) 125 | } else { 126 | // Probably axis3 wheel scrolled 127 | None 128 | } 129 | } 130 | _ => None, 131 | }; 132 | 133 | match (mouse_event, &mut CALLBACKS) { 134 | (Some(event), Some(callbacks)) => { 135 | for callback in callbacks.lock().unwrap().values() { 136 | callback(&event); 137 | } 138 | } 139 | _ => {} 140 | } 141 | 142 | cg_event 143 | } 144 | 145 | unsafe { 146 | // Create the mouse listener hook 147 | TAP_EVENT_REF = Some(CGEventTapCreate( 148 | CGEventTapLocation::CGHIDEventTap, 149 | CGEventTapPlacement::HeadInsertEventTap, 150 | CGEventTapOption::ListenOnly as u32, 151 | (1 << CGEventType::LeftMouseDown as u64) 152 | + (1 << CGEventType::LeftMouseUp as u64) 153 | + (1 << CGEventType::RightMouseDown as u64) 154 | + (1 << CGEventType::RightMouseUp as u64) 155 | + (1 << CGEventType::OtherMouseDown as u64) 156 | + (1 << CGEventType::OtherMouseUp as u64) 157 | + (1 << CGEventType::MouseMoved as u64) 158 | + (1 << CGEventType::ScrollWheel as u64), 159 | Some(mouse_on_event_callback), 160 | null_mut(), 161 | )); 162 | 163 | let loop_source = 164 | CFMachPortCreateRunLoopSource(null_mut(), TAP_EVENT_REF.unwrap(), 0); 165 | let current_loop = CFRunLoopGetCurrent(); 166 | CFRunLoopAddSource(current_loop, loop_source, kCFRunLoopDefaultMode); 167 | CGEventTapEnable(TAP_EVENT_REF.unwrap(), true); 168 | CFRunLoopRun(); 169 | } 170 | }); 171 | 172 | Ok(()) 173 | } 174 | } 175 | 176 | impl Default for DarwinMouseManager { 177 | fn default() -> Self { 178 | Self::new() 179 | } 180 | } 181 | 182 | impl Drop for DarwinMouseManager { 183 | fn drop(&mut self) { 184 | unsafe { 185 | match TAP_EVENT_REF { 186 | Some(event_ref) => { 187 | // Release the tap event 188 | CFRelease(event_ref); 189 | TAP_EVENT_REF = None; 190 | } 191 | None => {} 192 | } 193 | } 194 | } 195 | } 196 | 197 | impl MouseActions for DarwinMouseManager { 198 | fn move_to(&self, x: i32, y: i32) -> Result<(), Error> { 199 | let cg_point = CGPoint { 200 | x: x as f64, 201 | y: y as f64, 202 | }; 203 | unsafe { 204 | let result = CGWarpMouseCursorPosition(cg_point); 205 | if result != CGError::Success { 206 | return Err(Error::CustomError( 207 | "Failed to move the mouse, CGError is not Success".to_string(), 208 | )); 209 | } 210 | }; 211 | 212 | Ok(()) 213 | } 214 | 215 | fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<(), Error> { 216 | let (x, y) = self.get_position()?; 217 | self.move_to(x + x_offset, y + y_offset) 218 | } 219 | 220 | fn get_position(&self) -> Result<(i32, i32), Error> { 221 | unsafe { 222 | let event = CGEventCreate(null_mut()); 223 | if event == null_mut() { 224 | return Err(Error::CGCouldNotCreateEvent); 225 | } 226 | let cursor = CGEventGetLocation(event); 227 | CFRelease(event as CFTypeRef); 228 | return Ok((cursor.x as i32, cursor.y as i32)); 229 | } 230 | } 231 | 232 | fn press_button(&self, button: MouseButton) -> Result<(), Error> { 233 | let (event_type, mouse_button) = match button { 234 | MouseButton::Left => (CGEventType::LeftMouseDown, CGMouseButton::Left), 235 | MouseButton::Middle => (CGEventType::OtherMouseDown, CGMouseButton::Center), 236 | MouseButton::Right => (CGEventType::RightMouseDown, CGMouseButton::Right), 237 | }; 238 | self.create_mouse_event(event_type, mouse_button)?; 239 | Ok(()) 240 | } 241 | 242 | fn release_button(&self, button: MouseButton) -> Result<(), Error> { 243 | let (event_type, mouse_button) = match button { 244 | MouseButton::Left => (CGEventType::LeftMouseUp, CGMouseButton::Left), 245 | MouseButton::Middle => (CGEventType::OtherMouseUp, CGMouseButton::Center), 246 | MouseButton::Right => (CGEventType::RightMouseUp, CGMouseButton::Right), 247 | }; 248 | self.create_mouse_event(event_type, mouse_button) 249 | } 250 | 251 | fn click_button(&self, button: MouseButton) -> Result<(), Error> { 252 | self.press_button(button)?; 253 | self.release_button(button) 254 | } 255 | 256 | fn scroll_wheel( 257 | &self, 258 | direction: ScrollDirection, 259 | scroll_unit: ScrollUnit, 260 | distance: u32, 261 | ) -> Result<(), Error> { 262 | let distance = match direction { 263 | ScrollDirection::Up | ScrollDirection::Left => distance as c_int, 264 | ScrollDirection::Down | ScrollDirection::Right => -(distance as c_int), 265 | }; 266 | self.create_scroll_wheel_event(distance, scroll_unit, direction) 267 | } 268 | 269 | fn hook(&mut self, callback: Box) -> Result { 270 | if !self.is_listening { 271 | self.start_listener()?; 272 | self.is_listening = true; 273 | } 274 | 275 | let id = self.callback_counter; 276 | unsafe { 277 | match &mut CALLBACKS { 278 | Some(callbacks) => { 279 | callbacks.lock().unwrap().insert(id, callback); 280 | } 281 | None => { 282 | initialize_callbacks(); 283 | return self.hook(callback); 284 | } 285 | } 286 | } 287 | self.callback_counter += 1; 288 | Ok(id) 289 | } 290 | 291 | fn unhook(&mut self, callback_id: CallbackId) -> Result<(), Error> { 292 | unsafe { 293 | match &mut CALLBACKS { 294 | Some(callbacks) => match callbacks.lock().unwrap().remove(&callback_id) { 295 | Some(_) => Ok(()), 296 | None => Err(Error::UnhookFailed), 297 | }, 298 | None => { 299 | initialize_callbacks(); 300 | self.unhook(callback_id) 301 | } 302 | } 303 | } 304 | } 305 | 306 | fn unhook_all(&mut self) -> Result<(), Error> { 307 | unsafe { 308 | match &mut CALLBACKS { 309 | Some(callbacks) => { 310 | callbacks.lock().unwrap().clear(); 311 | } 312 | None => { 313 | initialize_callbacks(); 314 | return self.unhook_all(); 315 | } 316 | } 317 | } 318 | Ok(()) 319 | } 320 | } 321 | 322 | fn initialize_callbacks() { 323 | unsafe { 324 | match CALLBACKS { 325 | Some(_) => {} 326 | None => { 327 | CALLBACKS = Some(Mutex::new(HashMap::new())); 328 | } 329 | } 330 | } 331 | } 332 | 333 | /// CoreGraphics type definitions 334 | #[allow(dead_code)] 335 | #[derive(PartialEq, Eq)] 336 | #[repr(C)] 337 | enum CGError { 338 | CannotComplete = 1004, 339 | Failure = 1000, 340 | IllegalArgument = 1001, 341 | InvalidConnection = 1002, 342 | InvalidContext = 1003, 343 | InvalidOperation = 1010, 344 | NoneAvailable = 1011, 345 | NotImplemented = 1006, 346 | RangeCheck = 1007, 347 | Success = 0, 348 | TypeCheck = 1008, 349 | } 350 | #[repr(C)] 351 | pub struct CGPoint { 352 | x: c_double, 353 | y: c_double, 354 | } 355 | enum CGEventSource {} 356 | enum CGEvent {} 357 | type CGEventSourceRef = *mut CGEventSource; 358 | type CGEventRef = *mut CGEvent; 359 | type CFTypeRef = *const c_void; 360 | type CGEventMask = c_ulong; 361 | 362 | #[repr(C)] 363 | enum CGEventType { 364 | LeftMouseDown = 1, 365 | LeftMouseUp = 2, 366 | RightMouseDown = 3, 367 | RightMouseUp = 4, 368 | MouseMoved = 5, 369 | _LeftMouseDragged = 6, 370 | _RightMouseDragged = 7, 371 | ScrollWheel = 22, 372 | OtherMouseDown = 25, 373 | OtherMouseUp = 26, 374 | _OtherMouseDragged = 27, 375 | } 376 | 377 | #[repr(C)] 378 | enum CGMouseButton { 379 | Left = 0, 380 | Right = 1, 381 | Center = 2, 382 | } 383 | 384 | #[repr(C)] 385 | enum CGEventTapLocation { 386 | CGHIDEventTap = 0, 387 | _CGSessionEventTap = 1, 388 | _CGAnnotatedSessionEventTap = 2, 389 | } 390 | 391 | #[repr(C)] 392 | enum CGScrollEventUnit { 393 | Pixel = 0, 394 | Line = 1, 395 | } 396 | 397 | #[repr(C)] 398 | enum CGEventTapPlacement { 399 | HeadInsertEventTap = 0, 400 | _TailAppendEventTap = 1, 401 | } 402 | 403 | #[repr(C)] 404 | enum CGEventTapOption { 405 | _Default = 0, 406 | ListenOnly = 1, 407 | } 408 | 409 | type CGEventTapCallback = Option< 410 | unsafe extern "C" fn( 411 | proxy: *const c_void, 412 | event_type: CGEventType, 413 | cg_event: CGEventRef, 414 | user_info: *mut c_void, 415 | ) -> CGEventRef, 416 | >; 417 | 418 | #[link(name = "CoreGraphics", kind = "framework")] 419 | extern "C" { 420 | fn CGWarpMouseCursorPosition(new_cursor_position: CGPoint) -> CGError; 421 | fn CGEventCreate(source: CGEventSourceRef) -> CGEventRef; 422 | fn CGEventGetLocation(event: CGEventRef) -> CGPoint; 423 | fn CGEventCreateMouseEvent( 424 | source: CGEventSourceRef, 425 | mouse_type: CGEventType, 426 | mouse_cursor_position: CGPoint, 427 | mouse_button: CGMouseButton, 428 | ) -> CGEventRef; 429 | fn CGEventCreateScrollWheelEvent( 430 | source: CGEventSourceRef, 431 | units: CGScrollEventUnit, 432 | // Number of scroll directions/wheels, maximum is 3 433 | wheel_count: c_int, 434 | // Vertical wheel movement distance 435 | wheel1: c_int, 436 | // Horizontal wheel movement distance 437 | wheel2: c_int, 438 | ) -> CGEventRef; 439 | fn CGEventPost(tap: CGEventTapLocation, event: CGEventRef); 440 | fn CGEventTapCreate( 441 | tap: CGEventTapLocation, 442 | place: CGEventTapPlacement, 443 | options: c_uint, 444 | eventsOfInterest: CGEventMask, 445 | callback: CGEventTapCallback, 446 | refcon: *mut c_void, 447 | ) -> CFTypeRef; 448 | fn CGEventTapEnable(tap: *const c_void, enable: bool); 449 | fn CGEventGetIntegerValueField(event: CGEventRef, field: c_uint) -> c_long; 450 | } 451 | #[link(name = "CoreFoundation", kind = "framework")] 452 | extern "C" { 453 | static kCFRunLoopDefaultMode: *const c_void; 454 | 455 | fn CFRelease(cf: CFTypeRef); 456 | fn CFMachPortCreateRunLoopSource( 457 | allocator: *mut c_void, 458 | tap: *const c_void, 459 | order: c_ulong, 460 | ) -> *mut c_void; 461 | fn CFRunLoopGetCurrent() -> *mut c_void; 462 | fn CFRunLoopAddSource(rl: *mut c_void, source: *mut c_void, mode: *const c_void); 463 | fn CFRunLoopRun(); 464 | } 465 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug, PartialEq, Eq)] 4 | pub enum Error { 5 | #[error("this function is not implemented for the current platform")] 6 | NotImplemented, 7 | #[error("failed while trying to write to a file")] 8 | WriteFailed, 9 | #[error("failed while trying to unhook a callback, make sure the id is correct")] 10 | UnhookFailed, 11 | #[error("the pointer is not on the same screen as the specified window")] 12 | X11PointerWindowMismatch, 13 | #[error("failed to send input, the input was already blocked by another thread")] 14 | InputIsBlocked, 15 | #[error("CoreGraphics: failed to create mouse event")] 16 | CGCouldNotCreateEvent, 17 | #[error("permission denied for this operation, plese try as super user")] 18 | PermissionDenied, 19 | #[error("Error: `{0}`")] 20 | CustomError(String), 21 | } 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any( 2 | target_os = "linux", 3 | target_os = "dragonfly", 4 | target_os = "freebsd", 5 | target_os = "netbsd", 6 | target_os = "openbsd" 7 | ))] 8 | pub mod nix; 9 | 10 | #[cfg(target_vendor = "apple")] 11 | pub mod darwin; 12 | 13 | #[cfg(target_os = "windows")] 14 | pub mod windows; 15 | 16 | /// The `Mouse` struct that implements the `MouseActions` 17 | /// 18 | /// # Example usage 19 | /// 20 | /// ```rust,no_run 21 | /// use std::thread; 22 | /// use std::time::Duration; 23 | /// 24 | /// use mouce::{Mouse, MouseActions}; 25 | /// 26 | /// fn main() { 27 | /// let mouse_manager = Mouse::new(); 28 | /// 29 | /// let mut x = 0; 30 | /// while x < 1920 { 31 | /// let _ = mouse_manager.move_to(x, 540); 32 | /// x += 1; 33 | /// thread::sleep(Duration::from_millis(2)); 34 | /// } 35 | /// } 36 | /// ``` 37 | #[derive(Clone)] 38 | pub struct Mouse { 39 | #[cfg(all( 40 | any( 41 | target_os = "linux", 42 | target_os = "dragonfly", 43 | target_os = "freebsd", 44 | target_os = "netbsd", 45 | target_os = "openbsd" 46 | ), 47 | feature = "x11" 48 | ))] 49 | inner: crate::nix::x11::X11MouseManager, 50 | #[cfg(all( 51 | any( 52 | target_os = "linux", 53 | target_os = "dragonfly", 54 | target_os = "freebsd", 55 | target_os = "netbsd", 56 | target_os = "openbsd" 57 | ), 58 | not(feature = "x11") 59 | ))] 60 | inner: crate::nix::uinput::UInputMouseManager, 61 | #[cfg(target_vendor = "apple")] 62 | inner: crate::darwin::DarwinMouseManager, 63 | #[cfg(target_os = "windows")] 64 | inner: crate::windows::WindowsMouseManager, 65 | } 66 | 67 | impl Mouse { 68 | pub fn new() -> Self { 69 | #[cfg(all( 70 | any( 71 | target_os = "linux", 72 | target_os = "dragonfly", 73 | target_os = "freebsd", 74 | target_os = "netbsd", 75 | target_os = "openbsd" 76 | ), 77 | feature = "x11" 78 | ))] 79 | let inner = crate::nix::x11::X11MouseManager::new(); 80 | #[cfg(all( 81 | any( 82 | target_os = "linux", 83 | target_os = "dragonfly", 84 | target_os = "freebsd", 85 | target_os = "netbsd", 86 | target_os = "openbsd" 87 | ), 88 | not(feature = "x11") 89 | ))] 90 | let inner = crate::nix::uinput::UInputMouseManager::new(); 91 | #[cfg(target_vendor = "apple")] 92 | let inner = crate::darwin::DarwinMouseManager::new(); 93 | #[cfg(target_os = "windows")] 94 | let inner = crate::windows::WindowsMouseManager::new(); 95 | 96 | Self { inner } 97 | } 98 | } 99 | 100 | impl Default for Mouse { 101 | fn default() -> Self { 102 | Self::new() 103 | } 104 | } 105 | 106 | impl MouseActions for Mouse { 107 | fn move_to(&self, x: i32, y: i32) -> Result<(), error::Error> { 108 | self.inner.move_to(x, y) 109 | } 110 | 111 | fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<(), error::Error> { 112 | self.inner.move_relative(x_offset, y_offset) 113 | } 114 | 115 | fn get_position(&self) -> Result<(i32, i32), error::Error> { 116 | self.inner.get_position() 117 | } 118 | 119 | fn press_button(&self, button: common::MouseButton) -> Result<(), error::Error> { 120 | self.inner.press_button(button) 121 | } 122 | 123 | fn release_button(&self, button: common::MouseButton) -> Result<(), error::Error> { 124 | self.inner.release_button(button) 125 | } 126 | 127 | fn click_button(&self, button: common::MouseButton) -> Result<(), error::Error> { 128 | self.inner.click_button(button) 129 | } 130 | 131 | fn scroll_wheel( 132 | &self, 133 | direction: common::ScrollDirection, 134 | scroll_unit: common::ScrollUnit, 135 | distance: u32, 136 | ) -> Result<(), error::Error> { 137 | self.inner.scroll_wheel(direction, scroll_unit, distance) 138 | } 139 | 140 | fn hook( 141 | &mut self, 142 | callback: Box, 143 | ) -> Result { 144 | self.inner.hook(callback) 145 | } 146 | 147 | fn unhook(&mut self, callback_id: common::CallbackId) -> Result<(), error::Error> { 148 | self.inner.unhook(callback_id) 149 | } 150 | 151 | fn unhook_all(&mut self) -> Result<(), error::Error> { 152 | self.inner.unhook_all() 153 | } 154 | } 155 | 156 | pub mod common; 157 | pub mod error; 158 | 159 | pub use common::MouseActions; 160 | 161 | #[cfg(test)] 162 | mod tests { 163 | use crate::Mouse; 164 | 165 | #[test] 166 | fn supported_platform() { 167 | // Mouse should be visible and successfully return a new instance 168 | // if the current platform is supported 169 | Mouse::new(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/nix/mod.rs: -------------------------------------------------------------------------------- 1 | /// 2 | /// This module contains the mouse action functions 3 | /// for the unix-like systems 4 | /// 5 | use crate::common::{CallbackId, MouseButton, MouseEvent, ScrollDirection}; 6 | use crate::error::Error; 7 | use crate::nix::uinput::{ 8 | InputEvent, TimeVal, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, EV_KEY, EV_REL, REL_HWHEEL, REL_WHEEL, 9 | REL_X, REL_Y, 10 | }; 11 | use glob::glob; 12 | use std::collections::HashMap; 13 | use std::fs::File; 14 | use std::mem::size_of; 15 | use std::os::unix::io::AsRawFd; 16 | use std::sync::mpsc; 17 | use std::sync::{Arc, Mutex}; 18 | use std::thread; 19 | 20 | #[cfg(feature = "x11")] 21 | use std::{process::Command, str::from_utf8}; 22 | #[cfg(feature = "x11")] 23 | pub mod x11; 24 | 25 | pub mod uinput; 26 | 27 | type Callbacks = Arc>>>; 28 | 29 | /// Start the event listener for nix systems 30 | fn start_nix_listener(callbacks: &Callbacks) -> Result<(), Error> { 31 | let (tx, rx) = mpsc::channel(); 32 | 33 | let mut previous_paths = vec![]; 34 | // Read all the mouse events listed under /dev/input/by-id and 35 | // /dev/input/by-path. These directories are collections of symlinks 36 | // to /dev/input/event* 37 | // 38 | // I am only interested in the ones that end with `-event-mouse` 39 | for file in glob("/dev/input/by-id/*-event-mouse") 40 | .expect("Failed to read by-id glob pattern") 41 | .chain( 42 | glob("/dev/input/by-path/*-event-mouse").expect("Failed to read by-path glob pattern"), 43 | ) 44 | { 45 | let mut file = file.expect("Failed because of an IO error"); 46 | 47 | // Get the link if it exists 48 | if let Ok(rel_path) = file.read_link() { 49 | if rel_path.is_absolute() { 50 | file = rel_path; 51 | } else { 52 | // Remove the file name from the path buffer, leaving us with path to directory 53 | file.pop(); 54 | // Push the relative path of the link (e.g. `../event8`) 55 | file.push(rel_path); 56 | // Get the absolute path to final path 57 | file = std::fs::canonicalize(file) 58 | .expect("Can't get absolute path to linked device file"); 59 | } 60 | } 61 | 62 | let path = file.display().to_string(); 63 | 64 | if previous_paths.contains(&path) { 65 | continue; 66 | } 67 | 68 | previous_paths.push(path.clone()); 69 | 70 | let event = match File::options().read(true).open(path) { 71 | Ok(file) => file, 72 | Err(_) => return Err(Error::PermissionDenied), 73 | }; 74 | 75 | // Create a thread for this mouse-event file 76 | let tx = tx.clone(); 77 | thread::spawn(move || loop { 78 | let mut buffer = InputEvent { 79 | time: TimeVal { 80 | tv_sec: 0, 81 | tv_usec: 0, 82 | }, 83 | r#type: 0, 84 | code: 0, 85 | value: 0, 86 | }; 87 | unsafe { 88 | read(event.as_raw_fd(), &mut buffer, size_of::()); 89 | } 90 | tx.send(buffer).unwrap(); 91 | }); 92 | } 93 | 94 | let callbacks = callbacks.clone(); 95 | // Create a thread for handling the callbacks 96 | thread::spawn(move || { 97 | for received in rx { 98 | // Construct the library's MouseEvent 99 | let r#type = received.r#type as i32; 100 | let code = received.code as i32; 101 | let val = received.value; 102 | 103 | let mouse_event = if r#type == EV_KEY { 104 | let button = if code == BTN_LEFT { 105 | MouseButton::Left 106 | } else if code == BTN_RIGHT { 107 | MouseButton::Right 108 | } else if code == BTN_MIDDLE { 109 | MouseButton::Middle 110 | } else { 111 | // Ignore the unknown mouse buttons 112 | continue; 113 | }; 114 | 115 | if received.value == 1 { 116 | MouseEvent::Press(button) 117 | } else { 118 | MouseEvent::Release(button) 119 | } 120 | } else if r#type == EV_REL { 121 | let code = received.code as u32; 122 | if code == REL_WHEEL { 123 | MouseEvent::Scroll( 124 | if received.value > 0 { 125 | ScrollDirection::Up 126 | } else { 127 | ScrollDirection::Down 128 | }, 129 | received.value.unsigned_abs(), 130 | ) 131 | } else if code == REL_HWHEEL { 132 | MouseEvent::Scroll( 133 | if received.value > 0 { 134 | ScrollDirection::Right 135 | } else { 136 | ScrollDirection::Left 137 | }, 138 | received.value.unsigned_abs(), 139 | ) 140 | } else if code == REL_X { 141 | MouseEvent::RelativeMove(val, 0) 142 | } else if code == REL_Y { 143 | MouseEvent::RelativeMove(0, val) 144 | } else { 145 | continue; 146 | } 147 | } else { 148 | // Ignore other unknown events 149 | continue; 150 | }; 151 | 152 | // Invoke all given callbacks with the constructed mouse event 153 | for callback in callbacks.lock().unwrap().values() { 154 | callback(&mouse_event); 155 | } 156 | } 157 | }); 158 | 159 | Ok(()) 160 | } 161 | 162 | // Legacy function to check if x11 is available, it was used to fallback to uinput if 163 | // X11 was not available, this feature is not included anymore but perhaps can be reimplemented 164 | // in the build.rs to determine if x11 is enabled but not available in compile time 165 | #[cfg(feature = "x11")] 166 | fn is_x11() -> bool { 167 | // Try to verify x11 using loginctl 168 | let loginctl_output = Command::new("sh") 169 | .arg("-c") 170 | .arg("loginctl show-session $(loginctl | awk '/tty/ {print $1}') -p Type --value") 171 | .output(); 172 | 173 | if let Ok(out) = loginctl_output { 174 | if from_utf8(&out.stdout).unwrap().trim().to_lowercase() == "x11" { 175 | return true; 176 | } 177 | } 178 | 179 | // If loginctl fails try to read the environment variable $XDG_SESSION_TYPE 180 | if let Ok(session_type) = std::env::var("XDG_SESSION_TYPE") { 181 | if session_type.trim().to_lowercase() == "x11" { 182 | return true; 183 | } 184 | } 185 | 186 | false 187 | } 188 | 189 | extern "C" { 190 | fn read(fd: i32, buf: *mut InputEvent, count: usize) -> i32; 191 | } 192 | -------------------------------------------------------------------------------- /src/nix/uinput.rs: -------------------------------------------------------------------------------- 1 | /// 2 | /// This module contains the mouse action functions 3 | /// for the linux systems that uses uinput 4 | /// 5 | /// - Unsupported mouse actions 6 | /// - get_position is not available on uinput 7 | /// 8 | use crate::common::{ 9 | CallbackId, MouseActions, MouseButton, MouseEvent, ScrollDirection, ScrollUnit, 10 | }; 11 | use crate::error::Error; 12 | use crate::nix::Callbacks; 13 | use std::collections::HashMap; 14 | use std::fs::File; 15 | use std::mem::size_of; 16 | use std::os::fd::RawFd; 17 | use std::os::raw::{c_char, c_int, c_long, c_uint, c_ulong, c_ushort}; 18 | use std::os::unix::prelude::AsRawFd; 19 | use std::sync::{Arc, Mutex}; 20 | use std::thread; 21 | use std::time::Duration; 22 | 23 | const UINPUT_MAX_NAME_SIZE: usize = 80; 24 | 25 | #[derive(Clone)] 26 | pub struct UInputMouseManager { 27 | uinput_file: Arc>, 28 | callbacks: Callbacks, 29 | callback_counter: CallbackId, 30 | is_listening: bool, 31 | } 32 | 33 | impl UInputMouseManager { 34 | pub fn new() -> Self { 35 | let manager = UInputMouseManager { 36 | uinput_file: Arc::new(Mutex::new( 37 | File::options() 38 | .write(true) 39 | .open("/dev/uinput") 40 | .expect("uinput file can not be opened"), 41 | )), 42 | callbacks: Arc::new(Mutex::new(HashMap::new())), 43 | callback_counter: 0, 44 | is_listening: false, 45 | }; 46 | let fd = manager.uinput_file_raw_fd(); 47 | unsafe { 48 | // For press events (also needed for mouse movement) 49 | ioctl(fd, UI_SET_EVBIT, EV_KEY); 50 | ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); 51 | ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT); 52 | ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE); 53 | 54 | // For mouse movement 55 | ioctl(fd, UI_SET_EVBIT, EV_REL); 56 | ioctl(fd, UI_SET_RELBIT, REL_X); 57 | ioctl(fd, UI_SET_RELBIT, REL_Y); 58 | ioctl(fd, UI_SET_RELBIT, REL_WHEEL); 59 | ioctl(fd, UI_SET_RELBIT, REL_HWHEEL); 60 | } 61 | 62 | let mut usetup = UInputSetup { 63 | id: InputId { 64 | bustype: BUS_USB, 65 | // Random vendor and product 66 | vendor: 0x2222, 67 | product: 0x3333, 68 | version: 0, 69 | }, 70 | name: [0; UINPUT_MAX_NAME_SIZE], 71 | ff_effects_max: 0, 72 | }; 73 | 74 | let mut device_bytes: Vec = "mouce-library-fake-mouse" 75 | .chars() 76 | .map(|ch| ch as c_char) 77 | .collect(); 78 | 79 | // Fill the rest of the name buffer with empty chars 80 | for _ in 0..UINPUT_MAX_NAME_SIZE - device_bytes.len() { 81 | device_bytes.push('\0' as c_char); 82 | } 83 | 84 | usetup.name.copy_from_slice(&device_bytes); 85 | 86 | unsafe { 87 | ioctl(fd, UI_DEV_SETUP, &usetup); 88 | ioctl(fd, UI_DEV_CREATE); 89 | } 90 | 91 | // On UI_DEV_CREATE the kernel will create the device node for this 92 | // device. We are inserting a pause here so that userspace has time 93 | // to detect, initialize the new device, and can start listening to 94 | // the event, otherwise it will not notice the event we are about to send. 95 | thread::sleep(Duration::from_millis(300)); 96 | 97 | manager 98 | } 99 | 100 | fn uinput_file_raw_fd(&self) -> RawFd { 101 | self.uinput_file 102 | .lock() 103 | .expect("uinput file lock is poisoned") 104 | .as_raw_fd() 105 | } 106 | 107 | /// Write the given event to the uinput file 108 | fn emit(&self, r#type: c_int, code: c_int, value: c_int) -> Result<(), Error> { 109 | let mut event = InputEvent { 110 | time: TimeVal { 111 | tv_sec: 0, 112 | tv_usec: 0, 113 | }, 114 | r#type: r#type as u16, 115 | code: code as u16, 116 | value, 117 | }; 118 | let fd = self.uinput_file_raw_fd(); 119 | 120 | unsafe { 121 | let count = size_of::(); 122 | let written_bytes = write(fd, &mut event, count); 123 | if written_bytes == -1 || written_bytes != count as c_long { 124 | return Err(Error::WriteFailed); 125 | } 126 | } 127 | 128 | Ok(()) 129 | } 130 | 131 | /// Syncronize the device 132 | fn syncronize(&self) -> Result<(), Error> { 133 | self.emit(EV_SYN, SYN_REPORT, 0)?; 134 | // Give uinput some time to update the mouse location, 135 | // otherwise it fails to move the mouse on release mode 136 | // A delay of 1 milliseconds seems to be enough for it 137 | thread::sleep(Duration::from_millis(1)); 138 | Ok(()) 139 | } 140 | 141 | /// Move the mouse relative to the current position 142 | fn move_relative(&self, x: i32, y: i32) -> Result<(), Error> { 143 | self.emit(EV_REL, REL_X as i32, x)?; 144 | self.emit(EV_REL, REL_Y as i32, y)?; 145 | self.syncronize() 146 | } 147 | } 148 | 149 | impl Default for UInputMouseManager { 150 | fn default() -> Self { 151 | Self::new() 152 | } 153 | } 154 | 155 | impl Drop for UInputMouseManager { 156 | fn drop(&mut self) { 157 | let fd = self.uinput_file_raw_fd(); 158 | unsafe { 159 | // Destroy the device, the file is closed automatically by the File module 160 | ioctl(fd, UI_DEV_DESTROY as c_ulong); 161 | } 162 | } 163 | } 164 | 165 | impl MouseActions for UInputMouseManager { 166 | fn move_to(&self, x: i32, y: i32) -> Result<(), Error> { 167 | // For some reason, absolute mouse move events are not working on uinput 168 | // (as I understand those events are intended for touch events) 169 | // 170 | // As a work around solution; first set the mouse to top left, then 171 | // call relative move function to simulate an absolute move event 172 | self.move_relative(i32::MIN, i32::MIN)?; 173 | self.move_relative(x, y) 174 | } 175 | 176 | fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<(), Error> { 177 | self.move_relative(x_offset, y_offset) 178 | } 179 | 180 | fn get_position(&self) -> Result<(i32, i32), Error> { 181 | // uinput does not let us get the current position of the mouse 182 | Err(Error::NotImplemented) 183 | } 184 | 185 | fn press_button(&self, button: MouseButton) -> Result<(), Error> { 186 | let btn = match button { 187 | MouseButton::Left => BTN_LEFT, 188 | MouseButton::Right => BTN_RIGHT, 189 | MouseButton::Middle => BTN_MIDDLE, 190 | }; 191 | self.emit(EV_KEY, btn, 1)?; 192 | self.syncronize() 193 | } 194 | 195 | fn release_button(&self, button: MouseButton) -> Result<(), Error> { 196 | let btn = match button { 197 | MouseButton::Left => BTN_LEFT, 198 | MouseButton::Right => BTN_RIGHT, 199 | MouseButton::Middle => BTN_MIDDLE, 200 | }; 201 | self.emit(EV_KEY, btn, 0)?; 202 | self.syncronize() 203 | } 204 | 205 | fn click_button(&self, button: MouseButton) -> Result<(), Error> { 206 | self.press_button(button)?; 207 | self.release_button(button) 208 | } 209 | 210 | fn scroll_wheel( 211 | &self, 212 | direction: ScrollDirection, 213 | scroll_unit: ScrollUnit, 214 | distance: u32, 215 | ) -> Result<(), Error> { 216 | match scroll_unit { 217 | ScrollUnit::Pixel => Err(Error::NotImplemented), 218 | ScrollUnit::Line => { 219 | let (scroll_dir, scroll_value) = match direction { 220 | ScrollDirection::Up => (REL_WHEEL, distance as c_int), 221 | ScrollDirection::Down => (REL_WHEEL, -(distance as c_int)), 222 | ScrollDirection::Left => (REL_HWHEEL, -(distance as c_int)), 223 | ScrollDirection::Right => (REL_HWHEEL, distance as c_int), 224 | }; 225 | self.emit(EV_REL, scroll_dir as c_int, scroll_value)?; 226 | self.syncronize() 227 | } 228 | } 229 | } 230 | 231 | fn hook(&mut self, callback: Box) -> Result { 232 | if !self.is_listening { 233 | super::start_nix_listener(&self.callbacks)?; 234 | self.is_listening = true; 235 | } 236 | 237 | let id = self.callback_counter; 238 | self.callbacks.lock().unwrap().insert(id, callback); 239 | self.callback_counter += 1; 240 | Ok(id) 241 | } 242 | 243 | fn unhook(&mut self, callback_id: CallbackId) -> Result<(), Error> { 244 | match self.callbacks.lock().unwrap().remove(&callback_id) { 245 | Some(_) => Ok(()), 246 | None => Err(Error::UnhookFailed), 247 | } 248 | } 249 | 250 | fn unhook_all(&mut self) -> Result<(), Error> { 251 | self.callbacks.lock().unwrap().clear(); 252 | Ok(()) 253 | } 254 | } 255 | 256 | /// ioctl and uinput definitions 257 | const UI_SET_EVBIT: c_ulong = 1074025828; 258 | const UI_SET_KEYBIT: c_ulong = 1074025829; 259 | const UI_SET_RELBIT: c_ulong = 1074025830; 260 | const UI_DEV_SETUP: c_ulong = 1079792899; 261 | const UI_DEV_CREATE: c_ulong = 21761; 262 | const UI_DEV_DESTROY: c_uint = 21762; 263 | 264 | pub const EV_KEY: c_int = 0x01; 265 | pub const EV_REL: c_int = 0x02; 266 | pub const REL_X: c_uint = 0x00; 267 | pub const REL_Y: c_uint = 0x01; 268 | pub const REL_WHEEL: c_uint = 0x08; 269 | pub const REL_HWHEEL: c_uint = 0x06; 270 | pub const BTN_LEFT: c_int = 0x110; 271 | pub const BTN_RIGHT: c_int = 0x111; 272 | pub const BTN_MIDDLE: c_int = 0x112; 273 | const SYN_REPORT: c_int = 0x00; 274 | const EV_SYN: c_int = 0x00; 275 | const BUS_USB: c_ushort = 0x03; 276 | 277 | /// uinput types 278 | #[repr(C)] 279 | struct UInputSetup { 280 | id: InputId, 281 | name: [c_char; UINPUT_MAX_NAME_SIZE], 282 | ff_effects_max: c_ulong, 283 | } 284 | 285 | #[repr(C)] 286 | struct InputId { 287 | bustype: c_ushort, 288 | vendor: c_ushort, 289 | product: c_ushort, 290 | version: c_ushort, 291 | } 292 | 293 | #[repr(C)] 294 | pub struct InputEvent { 295 | pub time: TimeVal, 296 | pub r#type: u16, 297 | pub code: u16, 298 | pub value: c_int, 299 | } 300 | 301 | #[repr(C)] 302 | pub struct TimeVal { 303 | pub tv_sec: c_ulong, 304 | pub tv_usec: c_ulong, 305 | } 306 | 307 | extern "C" { 308 | fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int; 309 | fn write(fd: c_int, buf: *mut InputEvent, count: usize) -> c_long; 310 | } 311 | -------------------------------------------------------------------------------- /src/nix/x11.rs: -------------------------------------------------------------------------------- 1 | /// 2 | /// This module contains the mouse action functions 3 | /// for the unix-like systems that use X11 4 | /// 5 | use crate::common::{ 6 | CallbackId, MouseActions, MouseButton, MouseEvent, ScrollDirection, ScrollUnit, 7 | }; 8 | use crate::error::Error; 9 | use crate::nix::Callbacks; 10 | use std::collections::HashMap; 11 | use std::os::raw::{c_char, c_int, c_uint, c_ulong}; 12 | use std::sync::{Arc, Mutex}; 13 | 14 | #[derive(Clone)] 15 | pub struct X11MouseManager { 16 | display: *mut Display, 17 | window: Window, 18 | callbacks: Callbacks, 19 | callback_counter: CallbackId, 20 | is_listening: bool, 21 | } 22 | 23 | unsafe impl Send for X11MouseManager {} 24 | 25 | impl X11MouseManager { 26 | pub fn new() -> Self { 27 | unsafe { 28 | let display = XOpenDisplay(&0); 29 | let window = XDefaultRootWindow(display); 30 | X11MouseManager { 31 | display, 32 | window, 33 | callbacks: Arc::new(Mutex::new(HashMap::new())), 34 | callback_counter: 0, 35 | is_listening: false, 36 | } 37 | } 38 | } 39 | 40 | fn button_event(&self, button: &MouseButton, is_press: bool) -> Result<(), Error> { 41 | let btn = match button { 42 | MouseButton::Left => 1, 43 | MouseButton::Middle => 2, 44 | MouseButton::Right => 3, 45 | }; 46 | unsafe { 47 | XTestFakeButtonEvent(self.display, btn, is_press, 0); 48 | XFlush(self.display); 49 | } 50 | Ok(()) 51 | } 52 | } 53 | 54 | impl Default for X11MouseManager { 55 | fn default() -> Self { 56 | Self::new() 57 | } 58 | } 59 | 60 | impl MouseActions for X11MouseManager { 61 | fn move_to(&self, x: i32, y: i32) -> Result<(), Error> { 62 | unsafe { 63 | XWarpPointer(self.display, 0, self.window, 0, 0, 0, 0, x, y); 64 | XFlush(self.display); 65 | } 66 | Ok(()) 67 | } 68 | 69 | fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<(), Error> { 70 | let (x, y) = self.get_position()?; 71 | self.move_to(x + x_offset, y + y_offset) 72 | } 73 | 74 | fn get_position(&self) -> Result<(i32, i32), Error> { 75 | let mut x = 0; 76 | let mut y = 0; 77 | let mut void = 0; 78 | let mut mask = 0; 79 | 80 | unsafe { 81 | let out = XQueryPointer( 82 | self.display, 83 | self.window, 84 | &mut void, 85 | &mut void, 86 | &mut x, 87 | &mut y, 88 | &mut x, 89 | &mut y, 90 | &mut mask, 91 | ); 92 | 93 | // If XQueryPointer returns False (which is an enum value that corresponds to 0) 94 | // that means the pointer is not on the same screen as the specified window 95 | if out == 0 { 96 | return Err(Error::X11PointerWindowMismatch); 97 | } 98 | } 99 | 100 | Ok((x, y)) 101 | } 102 | 103 | fn press_button(&self, button: MouseButton) -> Result<(), Error> { 104 | self.button_event(&button, true) 105 | } 106 | 107 | fn release_button(&self, button: MouseButton) -> Result<(), Error> { 108 | self.button_event(&button, false) 109 | } 110 | 111 | fn click_button(&self, button: MouseButton) -> Result<(), Error> { 112 | self.press_button(button)?; 113 | self.release_button(button) 114 | } 115 | 116 | fn scroll_wheel( 117 | &self, 118 | direction: ScrollDirection, 119 | scroll_unit: ScrollUnit, 120 | distance: u32, 121 | ) -> Result<(), Error> { 122 | match scroll_unit { 123 | ScrollUnit::Pixel => Err(Error::NotImplemented), 124 | ScrollUnit::Line => { 125 | let btn = match direction { 126 | ScrollDirection::Up => 4, 127 | ScrollDirection::Down => 5, 128 | ScrollDirection::Left => 6, 129 | ScrollDirection::Right => 7, 130 | }; 131 | for _ in 0..distance { 132 | unsafe { 133 | XTestFakeButtonEvent(self.display, btn, true, 0); 134 | XTestFakeButtonEvent(self.display, btn, false, 0); 135 | XFlush(self.display); 136 | } 137 | } 138 | Ok(()) 139 | } 140 | } 141 | } 142 | 143 | fn hook(&mut self, callback: Box) -> Result { 144 | if !self.is_listening { 145 | super::start_nix_listener(&self.callbacks)?; 146 | self.is_listening = true; 147 | } 148 | 149 | let id = self.callback_counter; 150 | self.callbacks.lock().unwrap().insert(id, callback); 151 | self.callback_counter += 1; 152 | Ok(id) 153 | } 154 | 155 | fn unhook(&mut self, callback_id: CallbackId) -> Result<(), Error> { 156 | match self.callbacks.lock().unwrap().remove(&callback_id) { 157 | Some(_) => Ok(()), 158 | None => Err(Error::UnhookFailed), 159 | } 160 | } 161 | 162 | fn unhook_all(&mut self) -> Result<(), Error> { 163 | self.callbacks.lock().unwrap().clear(); 164 | Ok(()) 165 | } 166 | } 167 | 168 | /// Xlib type definitions 169 | enum _XDisplay {} 170 | type Display = _XDisplay; 171 | type Window = c_ulong; 172 | 173 | // Xlib function definitions 174 | #[link(name = "X11")] 175 | extern "C" { 176 | fn XOpenDisplay(display: *const c_char) -> *mut Display; 177 | fn XDefaultRootWindow(display: *mut Display) -> Window; 178 | fn XWarpPointer( 179 | display: *mut Display, 180 | src_w: Window, 181 | dest_w: Window, 182 | srx_x: c_int, 183 | src_y: c_int, 184 | src_width: c_uint, 185 | src_height: c_uint, 186 | dest_x: c_int, 187 | dest_y: c_int, 188 | ) -> c_int; 189 | 190 | fn XFlush(display: *mut Display) -> c_int; 191 | fn XQueryPointer( 192 | display: *mut Display, 193 | window: Window, 194 | root_return: *mut Window, 195 | child_return: *mut Window, 196 | root_x_return: *mut c_int, 197 | root_y_return: *mut c_int, 198 | win_x_return: *mut c_int, 199 | win_y_return: *mut c_int, 200 | mask_return: *mut c_uint, 201 | ) -> c_int; 202 | } 203 | 204 | // XTest function definitions 205 | #[link(name = "Xtst")] 206 | extern "C" { 207 | fn XTestFakeButtonEvent( 208 | dpy: *mut Display, 209 | button: c_uint, 210 | is_press: bool, 211 | delay: c_ulong, 212 | ) -> c_int; 213 | } 214 | -------------------------------------------------------------------------------- /src/windows.rs: -------------------------------------------------------------------------------- 1 | /// 2 | /// This module contains the mouse action functions 3 | /// for the windows opearting system 4 | /// Uses the User32 system library 5 | /// 6 | use crate::common::{ 7 | CallbackId, MouseActions, MouseButton, MouseEvent, ScrollDirection, ScrollUnit, 8 | }; 9 | use crate::error::Error; 10 | use std::collections::HashMap; 11 | use std::mem::size_of; 12 | use std::os::raw::{c_int, c_long, c_short, c_uint, c_ulong, c_ushort}; 13 | use std::ptr::null_mut; 14 | use std::sync::Mutex; 15 | use std::thread; 16 | 17 | static mut HOOK: HHook = null_mut(); 18 | static mut CALLBACKS: Option>>> = None; 19 | 20 | #[derive(Clone)] 21 | pub struct WindowsMouseManager { 22 | callback_counter: CallbackId, 23 | is_listening: bool, 24 | } 25 | 26 | impl WindowsMouseManager { 27 | pub fn new() -> Self { 28 | WindowsMouseManager { 29 | callback_counter: 0, 30 | is_listening: false, 31 | } 32 | } 33 | 34 | fn send_input(&self, event: WindowsMouseEvent, mouse_data: i32) -> Result<(), Error> { 35 | let (x, y) = self.get_position_raw()?; 36 | let mut input = Input { 37 | r#type: INPUT_MOUSE, 38 | mi: MouseInput { 39 | dx: x, 40 | dy: y, 41 | mouse_data, 42 | dw_flags: event as DWord, 43 | time: 0, 44 | dw_extra_info: unsafe { GetMessageExtraInfo() as *mut c_ulong }, 45 | }, 46 | }; 47 | 48 | unsafe { 49 | let result = SendInput(1, &mut input, size_of::() as i32); 50 | // If the function returns 0, it means the input was blocked by another thread 51 | if result == 0 { 52 | return Err(Error::InputIsBlocked); 53 | } 54 | } 55 | Ok(()) 56 | } 57 | 58 | fn start_listener(&mut self) -> Result<(), Error> { 59 | thread::spawn(move || { 60 | unsafe extern "system" fn low_level_mouse_handler( 61 | code: c_int, 62 | param: WParam, 63 | lpdata: LParam, 64 | ) -> LResult { 65 | // Construct the library's MouseEvent 66 | let w_param = param as u32; 67 | 68 | let mouse_event = match w_param { 69 | WM_MOUSEMOVE => { 70 | let (x, y) = get_point(lpdata); 71 | Some(MouseEvent::AbsoluteMove( 72 | x.try_into().expect("Can't fit i64 into i32"), 73 | y.try_into().expect("Can't fit i64 into i32"), 74 | )) 75 | } 76 | WM_LBUTTONDOWN => Some(MouseEvent::Press(MouseButton::Left)), 77 | WM_MBUTTONDOWN => Some(MouseEvent::Press(MouseButton::Middle)), 78 | WM_RBUTTONDOWN => Some(MouseEvent::Press(MouseButton::Right)), 79 | WM_LBUTTONUP => Some(MouseEvent::Release(MouseButton::Left)), 80 | WM_MBUTTONUP => Some(MouseEvent::Release(MouseButton::Middle)), 81 | WM_RBUTTONUP => Some(MouseEvent::Release(MouseButton::Right)), 82 | WM_MOUSEWHEEL => { 83 | let delta = get_delta(lpdata) / WHEEL_DELTA as u16; 84 | let amount = get_scroll_amount(lpdata); 85 | 86 | match delta { 87 | 1 => Some(MouseEvent::Scroll(ScrollDirection::Up, amount)), 88 | _ => Some(MouseEvent::Scroll(ScrollDirection::Down, amount)), 89 | } 90 | } 91 | WM_MOUSEHWHEEL => { 92 | let delta = get_delta(lpdata) / WHEEL_DELTA as u16; 93 | let amount = get_scroll_amount(lpdata); 94 | 95 | match delta { 96 | 1 => Some(MouseEvent::Scroll(ScrollDirection::Right, amount)), 97 | _ => Some(MouseEvent::Scroll(ScrollDirection::Left, amount)), 98 | } 99 | } 100 | _ => None, 101 | }; 102 | 103 | match (mouse_event, &mut CALLBACKS) { 104 | (Some(event), Some(callbacks)) => { 105 | for callback in callbacks.lock().unwrap().values() { 106 | callback(&event); 107 | } 108 | } 109 | _ => {} 110 | } 111 | 112 | CallNextHookEx(HOOK, code, param, lpdata) 113 | } 114 | unsafe { 115 | HOOK = SetWindowsHookExA(WH_MOUSE_LL, Some(low_level_mouse_handler), null_mut(), 0); 116 | GetMessageA(null_mut(), null_mut(), 0, 0); 117 | } 118 | }); 119 | 120 | Ok(()) 121 | } 122 | 123 | // Return the mouse position (c_long, c_long), but it does not directly 124 | // comply with mouce interface, so we first fetch the positions here 125 | // then try to convert it to (i32, i32) within the trait implementation 126 | fn get_position_raw(&self) -> Result<(c_long, c_long), Error> { 127 | let mut out = Point { x: 0, y: 0 }; 128 | unsafe { 129 | let result = GetCursorPos(&mut out); 130 | if result == 0 { 131 | return Err(Error::CustomError( 132 | "failed to get the cursor position".to_string(), 133 | )); 134 | } 135 | } 136 | Ok((out.x, out.y)) 137 | } 138 | } 139 | 140 | impl Default for WindowsMouseManager { 141 | fn default() -> Self { 142 | Self::new() 143 | } 144 | } 145 | 146 | impl Drop for WindowsMouseManager { 147 | fn drop(&mut self) { 148 | unsafe { 149 | if HOOK.is_null() { 150 | // Remove the procedure installed in the hook chain 151 | UnhookWindowsHookEx(HOOK); 152 | } 153 | } 154 | } 155 | } 156 | 157 | impl MouseActions for WindowsMouseManager { 158 | fn move_to(&self, x: i32, y: i32) -> Result<(), Error> { 159 | unsafe { 160 | let result = SetCursorPos(x as c_int, y as c_int); 161 | if result == 0 { 162 | return Err(Error::CustomError( 163 | "failed to set the cursor position".to_string(), 164 | )); 165 | } 166 | } 167 | Ok(()) 168 | } 169 | 170 | fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<(), Error> { 171 | let (x, y) = self.get_position()?; 172 | self.move_to(x + x_offset, y + y_offset) 173 | } 174 | 175 | fn get_position(&self) -> Result<(i32, i32), Error> { 176 | match self.get_position_raw() { 177 | Ok((x, y)) => Ok(( 178 | x.try_into().expect("Can't fit i64 into i32"), 179 | y.try_into().expect("Can't fit i64 into i32"), 180 | )), 181 | Err(e) => Err(e), 182 | } 183 | } 184 | 185 | fn press_button(&self, button: MouseButton) -> Result<(), Error> { 186 | let event = match button { 187 | MouseButton::Left => WindowsMouseEvent::LeftDown, 188 | MouseButton::Middle => WindowsMouseEvent::MiddleDown, 189 | MouseButton::Right => WindowsMouseEvent::RightDown, 190 | }; 191 | 192 | self.send_input(event, 0) 193 | } 194 | 195 | fn release_button(&self, button: MouseButton) -> Result<(), Error> { 196 | let event = match button { 197 | MouseButton::Left => WindowsMouseEvent::LeftUp, 198 | MouseButton::Middle => WindowsMouseEvent::MiddleUp, 199 | MouseButton::Right => WindowsMouseEvent::RightUp, 200 | }; 201 | 202 | self.send_input(event, 0) 203 | } 204 | 205 | fn click_button(&self, button: MouseButton) -> Result<(), Error> { 206 | self.press_button(button)?; 207 | self.release_button(button) 208 | } 209 | 210 | fn scroll_wheel( 211 | &self, 212 | direction: ScrollDirection, 213 | scroll_unit: ScrollUnit, 214 | distance: u32, 215 | ) -> Result<(), Error> { 216 | let scroll_distance_in_pixels = match scroll_unit { 217 | ScrollUnit::Pixel => distance, 218 | ScrollUnit::Line => distance * WHEEL_DELTA as u32, 219 | }; 220 | 221 | let (event, scroll_distance) = match direction { 222 | ScrollDirection::Up => (WindowsMouseEvent::Wheel, scroll_distance_in_pixels as i32), 223 | ScrollDirection::Down => ( 224 | WindowsMouseEvent::Wheel, 225 | -(scroll_distance_in_pixels as i32), 226 | ), 227 | ScrollDirection::Right => (WindowsMouseEvent::HWheel, scroll_distance_in_pixels as i32), 228 | ScrollDirection::Left => ( 229 | WindowsMouseEvent::HWheel, 230 | -(scroll_distance_in_pixels as i32), 231 | ), 232 | }; 233 | self.send_input(event, scroll_distance) 234 | } 235 | 236 | /// On windows, the mouse movement events are reported in AbsoluteMove event 237 | /// using the physical coordinates rather than the logical pixel coordinates 238 | /// So the returned coordinates will correspond to the native resolution rather than 239 | /// the logical pixel space 240 | fn hook(&mut self, callback: Box) -> Result { 241 | if !self.is_listening { 242 | self.start_listener()?; 243 | self.is_listening = true; 244 | } 245 | 246 | let id = self.callback_counter; 247 | unsafe { 248 | match &mut CALLBACKS { 249 | Some(callbacks) => { 250 | callbacks.lock().unwrap().insert(id, callback); 251 | } 252 | None => { 253 | initialize_callbacks(); 254 | return self.hook(callback); 255 | } 256 | } 257 | } 258 | self.callback_counter += 1; 259 | Ok(id) 260 | } 261 | 262 | fn unhook(&mut self, callback_id: CallbackId) -> Result<(), Error> { 263 | unsafe { 264 | match &mut CALLBACKS { 265 | Some(callbacks) => match callbacks.lock().unwrap().remove(&callback_id) { 266 | Some(_) => Ok(()), 267 | None => Err(Error::UnhookFailed), 268 | }, 269 | None => { 270 | initialize_callbacks(); 271 | self.unhook(callback_id) 272 | } 273 | } 274 | } 275 | } 276 | 277 | fn unhook_all(&mut self) -> Result<(), Error> { 278 | unsafe { 279 | match &mut CALLBACKS { 280 | Some(callbacks) => { 281 | callbacks.lock().unwrap().clear(); 282 | } 283 | None => { 284 | initialize_callbacks(); 285 | return self.unhook_all(); 286 | } 287 | } 288 | } 289 | Ok(()) 290 | } 291 | } 292 | 293 | fn initialize_callbacks() { 294 | unsafe { 295 | match CALLBACKS { 296 | Some(_) => {} 297 | None => { 298 | CALLBACKS = Some(Mutex::new(HashMap::new())); 299 | } 300 | } 301 | } 302 | } 303 | 304 | unsafe fn get_point(lpdata: LParam) -> (c_long, c_long) { 305 | let mouse = *(lpdata as *const MSLLHookStruct); 306 | (mouse.pt.x, mouse.pt.y) 307 | } 308 | 309 | unsafe fn get_delta(lpdata: LParam) -> Word { 310 | let mouse = *(lpdata as *const MSLLHookStruct); 311 | ((mouse.mouse_data >> 16) & 0xffff) as Word 312 | } 313 | 314 | unsafe fn get_scroll_amount(lpdata: LParam) -> u32 { 315 | let mouse = *(lpdata as *const MSLLHookStruct); 316 | let signed_raw_amount = ((mouse.mouse_data as DWord >> 16) & 0xffff) as c_short; 317 | // Calculate the normalized and unsigned scroll amount 318 | let normalized = signed_raw_amount.unsigned_abs() / WHEEL_DELTA as u16; 319 | normalized as u32 320 | } 321 | 322 | /// User32 type definitions 323 | type LParam = *mut c_long; 324 | type LPInput = *mut Input; 325 | type DWord = c_ulong; 326 | type LResult = *mut c_int; 327 | type WParam = usize; 328 | type HHook = *mut Hhook__; 329 | type HInstance = *mut HInstance__; 330 | type HookProc = 331 | Option LResult>; 332 | type LPMsg = *mut Msg; 333 | type HWND = *mut HWND__; 334 | type Word = c_ushort; 335 | const WM_MOUSEMOVE: c_uint = 0x0200; 336 | const WM_LBUTTONDOWN: c_uint = 0x0201; 337 | const WM_LBUTTONUP: c_uint = 0x0202; 338 | const WM_RBUTTONDOWN: c_uint = 0x0204; 339 | const WM_RBUTTONUP: c_uint = 0x0205; 340 | const WM_MBUTTONDOWN: c_uint = 0x0207; 341 | const WM_MBUTTONUP: c_uint = 0x0208; 342 | const WM_MOUSEWHEEL: c_uint = 0x020A; 343 | const WM_MOUSEHWHEEL: c_uint = 0x020E; 344 | const WHEEL_DELTA: c_short = 120; 345 | const WH_MOUSE_LL: c_int = 14; 346 | enum Hhook__ {} 347 | enum HInstance__ {} 348 | enum HWND__ {} 349 | const INPUT_MOUSE: DWord = 0; 350 | #[repr(C)] 351 | struct MouseInput { 352 | dx: c_long, 353 | dy: c_long, 354 | mouse_data: c_int, 355 | dw_flags: DWord, 356 | time: DWord, 357 | dw_extra_info: *mut c_ulong, 358 | } 359 | #[repr(C)] 360 | struct Input { 361 | r#type: DWord, 362 | mi: MouseInput, 363 | } 364 | #[repr(C)] 365 | #[derive(Clone, Copy)] 366 | struct Point { 367 | x: c_long, 368 | y: c_long, 369 | } 370 | #[repr(C)] 371 | enum WindowsMouseEvent { 372 | LeftDown = 0x0002, 373 | LeftUp = 0x0004, 374 | RightDown = 0x0008, 375 | RightUp = 0x0010, 376 | MiddleDown = 0x0020, 377 | MiddleUp = 0x0040, 378 | Wheel = 0x0800, 379 | HWheel = 0x01000, 380 | } 381 | 382 | #[repr(C)] 383 | struct Msg { 384 | hwnd: HWND, 385 | message: c_uint, 386 | w_param: WParam, 387 | l_param: LParam, 388 | time: DWord, 389 | pt: Point, 390 | } 391 | 392 | #[repr(C)] 393 | #[derive(Clone, Copy)] 394 | struct MSLLHookStruct { 395 | pt: Point, 396 | mouse_data: DWord, 397 | flags: DWord, 398 | time: DWord, 399 | dw_extra_info: usize, 400 | } 401 | 402 | // User32 function definitions 403 | #[link(name = "user32")] 404 | extern "system" { 405 | fn SetCursorPos(x: c_int, y: c_int) -> c_int; 406 | fn GetCursorPos(lp_point: *mut Point) -> c_int; 407 | fn SendInput(c_inputs: c_uint, p_inputs: LPInput, cb_size: c_int) -> c_uint; 408 | fn GetMessageExtraInfo() -> LParam; 409 | fn SetWindowsHookExA( 410 | idHook: c_int, 411 | lpfn: HookProc, 412 | hmod: HInstance, 413 | dwThreadId: DWord, 414 | ) -> HHook; 415 | fn CallNextHookEx(hhk: HHook, n_code: c_int, w_param: WParam, l_param: LParam) -> LResult; 416 | fn GetMessageA( 417 | lp_msg: LPMsg, 418 | h_wnd: HWND, 419 | w_msg_filter_min: c_uint, 420 | w_msg_filter_max: c_uint, 421 | ) -> bool; 422 | fn UnhookWindowsHookEx(hhk: HHook) -> bool; 423 | } 424 | --------------------------------------------------------------------------------