diff --git a/rdev/.github/workflows/rust.yml b/rdev/.github/workflows/rust.yml new file mode 100644 index 0000000..ec1b994 --- /dev/null +++ b/rdev/.github/workflows/rust.yml @@ -0,0 +1,48 @@ +name: build + +on: [push, pull_request] + +jobs: + build: + + runs-on: ${{matrix.os}} + env: + DISPLAY: ':99' + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + include: + - os: ubuntu-latest + headless: Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + - os: ubuntu-latest + dependencies: sudo apt-get install libxtst-dev libevdev-dev --assume-yes + - os: macos-latest + # TODO: We can't test this on github, we can't set accessibility yet. + test: cargo test --verbose --all-features -- --skip test_listen_and_simulate --skip test_grab + - os: ubuntu-latest + # TODO unstable_grab feature is not supported on Linux. + test: cargo test --verbose --features=serialize + - os: windows-latest + test: cargo test --verbose --all-features + + steps: + - uses: actions/checkout@v2 + - name: CargoFmt + run: rustup component add rustfmt + - name: Dependencies + run: ${{matrix.dependencies}} + - name: Setup headless environment + run: ${{matrix.headless}} + - name: Check formatting + run: | + rustup component add rustfmt + cargo fmt -- --check + - name: Build + run: cargo build --verbose + - name: Run tests + run: ${{matrix.test}} + - name: Linter + run: | + rustup component add clippy + cargo clippy --all-features --verbose -- -Dwarnings diff --git a/rdev/.gitignore b/rdev/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/rdev/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/rdev/.gitrepo b/rdev/.gitrepo new file mode 100644 index 0000000..a732b0d --- /dev/null +++ b/rdev/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme +; +[subrepo] + remote = https://github.com/Narsil/rdev + branch = master + commit = fd0ca9670f3de294e2de16fbe0398e1538af1782 + parent = a6b7bb3bb28fda03be6c50c261da1c46762388d8 + method = merge + cmdver = 0.4.1 diff --git a/rdev/Cargo.toml b/rdev/Cargo.toml new file mode 100644 index 0000000..79fd64c --- /dev/null +++ b/rdev/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "rdev" +version = "0.5.0" +authors = ["Nicolas Patry "] +edition = "2018" + +description = "Listen and send keyboard and mouse events on Windows, Linux and MacOS." +documentation = "https://docs.rs/rdev/" +homepage = "https://github.com/Narsil/rdev" +repository = "https://github.com/Narsil/rdev" +readme = "README.md" +keywords = ["input", "mouse", "testing", "keyboard", "automation"] +categories = ["development-tools::testing", "api-bindings", "hardware-support"] +license = "MIT" + +[dependencies] +serde = {version = "1.0", features = ["derive"], optional=true} +lazy_static = "1.4" + +[features] +serialize = ["serde"] +unstable_grab = ["evdev-rs", "epoll", "inotify"] + +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.22" +core-graphics = {version = "0.19.0", features = ["highsierra"]} +core-foundation = {version = "0.7"} +core-foundation-sys = {version = "0.7"} + + +[target.'cfg(target_os = "linux")'.dependencies] +libc = "0.2" +x11 = {version = "2.18", features = ["xlib", "xrecord", "xinput"]} +evdev-rs = {version = "0.4.0", optional=true} +epoll = {version = "4.1.0", optional=true} +inotify = {version = "0.8.2", default-features=false, optional=true} + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = ["winuser", "errhandlingapi", "processthreadsapi"] } + +[dev-dependencies] +serde_json = "1.0" +# Some tests interact with the real OS. We can't hit the OS in parallel +# because that leads to unexpected behavior and flaky tests, so we need +# to run thoses tests in sequence instead. +serial_test = "0.4" +tokio = {version = "1.5", features=["sync", "macros", "rt-multi-thread"]} + +[[example]] +name = "serialize" +required-features = ["serialize"] + +[[example]] +name = "grab" +required-features = ["unstable_grab"] + +[[example]] +name = "tokio_channel" +required-features = ["unstable_grab"] + +[[test]] +name = "grab" +path = "tests/grab.rs" +required-features = ["unstable_grab"] diff --git a/rdev/LICENSE b/rdev/LICENSE new file mode 100644 index 0000000..5be1138 --- /dev/null +++ b/rdev/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Nicolas Patry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/rdev/README.md b/rdev/README.md new file mode 100644 index 0000000..88c00a6 --- /dev/null +++ b/rdev/README.md @@ -0,0 +1,223 @@ +![](https://github.com/Narsil/rdev/workflows/build/badge.svg) +[![Crate](https://img.shields.io/crates/v/rdev.svg)](https://crates.io/crates/rdev) +[![API](https://docs.rs/rdev/badge.svg)](https://docs.rs/rdev) + +# rdev + +Simple library to listen and send events **globally** to keyboard and mouse on MacOS, Windows and Linux +(x11). + +You can also check out [Enigo](https://github.com/Enigo-rs/Enigo) which is another +crate which helped me write this one. + +This crate is so far a pet project for me to understand the rust ecosystem. + +## Listening to global events + +```rust +use rdev::{listen, Event}; + +// This will block. +if let Err(error) = listen(callback) { + println!("Error: {:?}", error) +} + +fn callback(event: Event) { + println!("My callback {:?}", event); + match event.name { + Some(string) => println!("User wrote {:?}", string), + None => (), + } +} +``` + +### OS Caveats: +When using the `listen` function, the following caveats apply: + +### Mac OS +The process running the blocking `listen` function (loop) needs to be the parent process (no fork before). +The process needs to be granted access to the Accessibility API (ie. if you're running your process +inside Terminal.app, then Terminal.app needs to be added in +System Preferences > Security & Privacy > Privacy > Accessibility) +If the process is not granted access to the Accessibility API, MacOS will silently ignore rdev's +`listen` calleback and will not trigger it with events. No error will be generated. + +### Linux +The `listen` function uses X11 APIs, and so will not work in Wayland or in the linux kernel virtual console + +## Sending some events + +```rust +use rdev::{simulate, Button, EventType, Key, SimulateError}; +use std::{thread, time}; + +fn send(event_type: &EventType) { + let delay = time::Duration::from_millis(20); + match simulate(event_type) { + Ok(()) => (), + Err(SimulateError) => { + println!("We could not send {:?}", event_type); + } + } + // Let ths OS catchup (at least MacOS) + thread::sleep(delay); +} + +send(&EventType::KeyPress(Key::KeyS)); +send(&EventType::KeyRelease(Key::KeyS)); + +send(&EventType::MouseMove { x: 0.0, y: 0.0 }); +send(&EventType::MouseMove { x: 400.0, y: 400.0 }); +send(&EventType::ButtonPress(Button::Left)); +send(&EventType::ButtonRelease(Button::Right)); +send(&EventType::Wheel { + delta_x: 0, + delta_y: 1, +}); +``` +## Main structs +### Event + +In order to detect what a user types, we need to plug to the OS level management +of keyboard state (modifiers like shift, ctrl, but also dead keys if they exist). + +`EventType` corresponds to a *physical* event, corresponding to QWERTY layout +`Event` corresponds to an actual event that was received and `Event.name` reflects +what key was interpreted by the OS at that time, it will respect the layout. + +```rust +/// When events arrive from the system we can add some information +/// time is when the event was received. +#[derive(Debug)] +pub struct Event { + pub time: SystemTime, + pub name: Option, + pub event_type: EventType, +} +``` + +Be careful, Event::name, might be None, but also String::from(""), and might contain +not displayable unicode characters. We send exactly what the OS sends us so do some sanity checking +before using it. +Caveat: Dead keys don't function yet on Linux + +### EventType + +In order to manage different OS, the current EventType choices is a mix&match +to account for all possible events. +There is a safe mechanism to detect events no matter what, which are the +Unknown() variant of the enum which will contain some OS specific value. +Also not that not all keys are mapped to an OS code, so simulate might fail if you +try to send an unmapped key. Sending Unknown() variants will always work (the OS might +still reject it). + +```rust +/// In order to manage different OS, the current EventType choices is a mix&match +/// to account for all possible events. +#[derive(Debug)] +pub enum EventType { + /// The keys correspond to a standard qwerty layout, they don't correspond + /// To the actual letter a user would use, that requires some layout logic to be added. + KeyPress(Key), + KeyRelease(Key), + /// Some mouse will have more than 3 buttons, these are not defined, and different OS will + /// give different Unknown code. + ButtonPress(Button), + ButtonRelease(Button), + /// Values in pixels + MouseMove { + x: f64, + y: f64, + }, + /// Note: On Linux, there is no actual delta the actual values are ignored for delta_x + /// and we only look at the sign of delta_y to simulate wheelup or wheeldown. + Wheel { + delta_x: i64, + delta_y: i64, + }, +} +``` + + +## Getting the main screen size + +```rust +use rdev::{display_size}; + +let (w, h) = display_size().unwrap(); +assert!(w > 0); +assert!(h > 0); +``` + +## Keyboard state + +We can define a dummy Keyboard, that we will use to detect +what kind of EventType trigger some String. We get the currently used +layout for now ! +Caveat : This is layout dependent. If your app needs to support +layout switching don't use this ! +Caveat: On Linux, the dead keys mechanism is not implemented. +Caveat: Only shift and dead keys are implemented, Alt+unicode code on windows +won't work. + +```rust +use rdev::{Keyboard, EventType, Key, KeyboardState}; + +let mut keyboard = Keyboard::new().unwrap(); +let string = keyboard.add(&EventType::KeyPress(Key::KeyS)); +// string == Some("s") +``` + +## Grabbing global events. (Requires `unstable_grab` feature) + +Installing this library with the `unstable_grab` feature adds the `grab` function +which hooks into the global input device event stream. +by suppling this function with a callback, you can intercept +all keyboard and mouse events before they are delivered to applications / window managers. +In the callback, returning None ignores the event and returning the event let's it pass. +There is no modification of the event possible here (yet). + +Note: the use of the word `unstable` here refers specifically to the fact that the `grab` API is unstable and subject to change + +```rust +#[cfg(feature = "unstable_grab")] +use rdev::{grab, Event, EventType, Key}; + +#[cfg(feature = "unstable_grab")] +let callback = |event: Event| -> Option { + if let EventType::KeyPress(Key::CapsLock) = event.event_type { + println!("Consuming and cancelling CapsLock"); + None // CapsLock is now effectively disabled + } + else { Some(event) } +}; +// This will block. +#[cfg(feature = "unstable_grab")] +if let Err(error) = grab(callback) { + println!("Error: {:?}", error) +} +``` + +### OS Caveats: +When using the `listen` and/or `grab` functions, the following caveats apply: + +#### Mac OS +The process running the blocking `grab` function (loop) needs to be the parent process (no fork before). +The process needs to be granted access to the Accessibility API (ie. if you're running your process +inside Terminal.app, then Terminal.app needs to be added in +System Preferences > Security & Privacy > Privacy > Accessibility) +If the process is not granted access to the Accessibility API, the `grab` call will fail with an +EventTapError (at least in MacOS 10.15, possibly other versions as well) + +#### Linux +The `grab` function use the `evdev` library to intercept events, so they will work with both X11 and Wayland +In order for this to work, the process runnign the `listen` or `grab` loop needs to either run as root (not recommended), +or run as a user who's a member of the `input` group (recommended) +Note: on some distros, the group name for evdev access is called `plugdev`, and on some systems, both groups can exist. +When in doubt, add your user to both groups if they exist. + +## Serialization + +Event data returned by the `listen` and `grab` functions can be serialized and de-serialized with +Serde if you install this library with the `serialize` feature. + diff --git a/rdev/README.tpl b/rdev/README.tpl new file mode 100644 index 0000000..9f9061e --- /dev/null +++ b/rdev/README.tpl @@ -0,0 +1,8 @@ +![](https://github.com/Narsil/rdev/workflows/build/badge.svg) +[![Crate](https://img.shields.io/crates/v/rdev.svg)](https://crates.io/crates/rdev) +[![API](https://docs.rs/rdev/badge.svg)](https://docs.rs/rdev) + +# {{crate}} + +{{readme}} + diff --git a/rdev/examples/channel.rs b/rdev/examples/channel.rs new file mode 100644 index 0000000..5f7158a --- /dev/null +++ b/rdev/examples/channel.rs @@ -0,0 +1,22 @@ +use rdev::listen; +use std::sync::mpsc::channel; +use std::thread; + +fn main() { + // spawn new thread because listen blocks + let (schan, rchan) = channel(); + let _listener = thread::spawn(move || { + listen(move |event| { + schan + .send(event) + .unwrap_or_else(|e| println!("Could not send event {:?}", e)); + }) + .expect("Could not listen"); + }); + + let mut events = Vec::new(); + for event in rchan.iter() { + println!("Received {:?}", event); + events.push(event); + } +} diff --git a/rdev/examples/display.rs b/rdev/examples/display.rs new file mode 100644 index 0000000..b51848a --- /dev/null +++ b/rdev/examples/display.rs @@ -0,0 +1,6 @@ +use rdev::display_size; +fn main() { + let (w, h) = display_size().unwrap(); + + println!("Your screen is {:?}x{:?}", w, h); +} diff --git a/rdev/examples/grab.rs b/rdev/examples/grab.rs new file mode 100644 index 0000000..6f7f04a --- /dev/null +++ b/rdev/examples/grab.rs @@ -0,0 +1,19 @@ +use rdev::{grab, Event, EventType, Key}; + +fn main() { + // This will block. + if let Err(error) = grab(callback) { + println!("Error: {:?}", error) + } +} + +fn callback(event: Event) -> Option { + println!("My callback {:?}", event); + match event.event_type { + EventType::KeyPress(Key::Tab) => { + println!("Cancelling tab !"); + None + } + _ => Some(event), + } +} diff --git a/rdev/examples/keyboard_state.rs b/rdev/examples/keyboard_state.rs new file mode 100644 index 0000000..833eee9 --- /dev/null +++ b/rdev/examples/keyboard_state.rs @@ -0,0 +1,18 @@ +use rdev::{EventType, Key, Keyboard, KeyboardState}; + +fn main() { + let mut keyboard = Keyboard::new().unwrap(); + let char_s = keyboard.add(&EventType::KeyPress(Key::KeyS)).unwrap(); + assert_eq!(char_s, "s".to_string()); + println!("Pressing S gives: {:?}", char_s); + let n = keyboard.add(&EventType::KeyRelease(Key::KeyS)); + assert_eq!(n, None); + + keyboard.add(&EventType::KeyPress(Key::ShiftLeft)); + let char_s = keyboard.add(&EventType::KeyPress(Key::KeyS)).unwrap(); + println!("Pressing Shift+S gives: {:?}", char_s); + assert_eq!(char_s, "S".to_string()); + let n = keyboard.add(&EventType::KeyRelease(Key::KeyS)); + assert_eq!(n, None); + keyboard.add(&EventType::KeyRelease(Key::ShiftLeft)); +} diff --git a/rdev/examples/listen.rs b/rdev/examples/listen.rs new file mode 100644 index 0000000..3785294 --- /dev/null +++ b/rdev/examples/listen.rs @@ -0,0 +1,12 @@ +use rdev::{listen, Event}; + +fn main() { + // This will block. + if let Err(error) = listen(callback) { + println!("Error: {:?}", error) + } +} + +fn callback(event: Event) { + println!("My callback {:?}", event); +} diff --git a/rdev/examples/serialize.rs b/rdev/examples/serialize.rs new file mode 100644 index 0000000..800fceb --- /dev/null +++ b/rdev/examples/serialize.rs @@ -0,0 +1,18 @@ +use rdev::{Event, EventType, Key}; +use std::time::SystemTime; + +fn main() { + let event = Event { + event_type: EventType::KeyPress(Key::KeyS), + time: SystemTime::now(), + name: Some(String::from("S")), + }; + + let serialized = serde_json::to_string(&event).unwrap(); + + let deserialized: Event = serde_json::from_str(&serialized).unwrap(); + + println!("Serialized event {:?}", serialized); + println!("Deserialized event {:?}", deserialized); + assert_eq!(event, deserialized); +} diff --git a/rdev/examples/simulate.rs b/rdev/examples/simulate.rs new file mode 100644 index 0000000..1f50f04 --- /dev/null +++ b/rdev/examples/simulate.rs @@ -0,0 +1,28 @@ +use rdev::{simulate, Button, EventType, Key, SimulateError}; +use std::{thread, time}; + +fn send(event_type: &EventType) { + let delay = time::Duration::from_millis(20); + match simulate(event_type) { + Ok(()) => (), + Err(SimulateError) => { + println!("We could not send {:?}", event_type); + } + } + // Let ths OS catchup (at least MacOS) + thread::sleep(delay); +} + +fn main() { + send(&EventType::KeyPress(Key::KeyS)); + send(&EventType::KeyRelease(Key::KeyS)); + + send(&EventType::MouseMove { x: 0.0, y: 0.0 }); + send(&EventType::MouseMove { x: 400.0, y: 400.0 }); + send(&EventType::ButtonPress(Button::Left)); + send(&EventType::ButtonRelease(Button::Right)); + send(&EventType::Wheel { + delta_x: 0, + delta_y: 1, + }); +} diff --git a/rdev/examples/tokio_channel.rs b/rdev/examples/tokio_channel.rs new file mode 100644 index 0000000..57b29cf --- /dev/null +++ b/rdev/examples/tokio_channel.rs @@ -0,0 +1,22 @@ +use rdev::listen; +use std::thread; +use tokio::sync::mpsc; + +#[tokio::main] +async fn main() { + // spawn new thread because listen blocks + let (schan, mut rchan) = mpsc::unbounded_channel(); + let _listener = thread::spawn(move || { + listen(move |event| { + schan + .send(event) + .unwrap_or_else(|e| println!("Could not send event {:?}", e)); + }) + .expect("Could not listen"); + }); + + loop { + let event = rchan.recv().await; + println!("Received {:?}", event); + } +} diff --git a/rdev/rustfmt.toml b/rdev/rustfmt.toml new file mode 100644 index 0000000..c51666e --- /dev/null +++ b/rdev/rustfmt.toml @@ -0,0 +1 @@ +edition = "2018" \ No newline at end of file diff --git a/rdev/src/lib.rs b/rdev/src/lib.rs new file mode 100644 index 0000000..4aee56d --- /dev/null +++ b/rdev/src/lib.rs @@ -0,0 +1,410 @@ +//! Simple library to listen and send events to keyboard and mouse on MacOS, Windows and Linux +//! (x11). +//! +//! You can also check out [Enigo](https://github.com/Enigo-rs/Enigo) which is another +//! crate which helped me write this one. +//! +//! This crate is so far a pet project for me to understand the rust ecosystem. +//! +//! # Listening to global events +//! +//! ```no_run +//! use rdev::{listen, Event}; +//! +//! // This will block. +//! if let Err(error) = listen(callback) { +//! println!("Error: {:?}", error) +//! } +//! +//! fn callback(event: Event) { +//! println!("My callback {:?}", event); +//! match event.name { +//! Some(string) => println!("User wrote {:?}", string), +//! None => (), +//! } +//! } +//! ``` +//! +//! ## OS Caveats: +//! When using the `listen` function, the following caveats apply: +//! +//! ## Mac OS +//! The process running the blocking `listen` function (loop) needs to be the parent process (no fork before). +//! The process needs to be granted access to the Accessibility API (ie. if you're running your process +//! inside Terminal.app, then Terminal.app needs to be added in +//! System Preferences > Security & Privacy > Privacy > Accessibility) +//! If the process is not granted access to the Accessibility API, MacOS will silently ignore rdev's +//! `listen` calleback and will not trigger it with events. No error will be generated. +//! +//! ## Linux +//! The `listen` function uses X11 APIs, and so will not work in Wayland or in the linux kernel virtual console +//! +//! # Sending some events +//! +//! ```no_run +//! use rdev::{simulate, Button, EventType, Key, SimulateError}; +//! use std::{thread, time}; +//! +//! fn send(event_type: &EventType) { +//! let delay = time::Duration::from_millis(20); +//! match simulate(event_type) { +//! Ok(()) => (), +//! Err(SimulateError) => { +//! println!("We could not send {:?}", event_type); +//! } +//! } +//! // Let ths OS catchup (at least MacOS) +//! thread::sleep(delay); +//! } +//! +//! send(&EventType::KeyPress(Key::KeyS)); +//! send(&EventType::KeyRelease(Key::KeyS)); +//! +//! send(&EventType::MouseMove { x: 0.0, y: 0.0 }); +//! send(&EventType::MouseMove { x: 400.0, y: 400.0 }); +//! send(&EventType::ButtonPress(Button::Left)); +//! send(&EventType::ButtonRelease(Button::Right)); +//! send(&EventType::Wheel { +//! delta_x: 0, +//! delta_y: 1, +//! }); +//! ``` +//! # Main structs +//! ## Event +//! +//! In order to detect what a user types, we need to plug to the OS level management +//! of keyboard state (modifiers like shift, ctrl, but also dead keys if they exist). +//! +//! `EventType` corresponds to a *physical* event, corresponding to QWERTY layout +//! `Event` corresponds to an actual event that was received and `Event.name` reflects +//! what key was interpreted by the OS at that time, it will respect the layout. +//! +//! ```no_run +//! # use crate::rdev::EventType; +//! # use std::time::SystemTime; +//! /// When events arrive from the system we can add some information +//! /// time is when the event was received. +//! #[derive(Debug)] +//! pub struct Event { +//! pub time: SystemTime, +//! pub name: Option, +//! pub event_type: EventType, +//! } +//! ``` +//! +//! Be careful, Event::name, might be None, but also String::from(""), and might contain +//! not displayable unicode characters. We send exactly what the OS sends us so do some sanity checking +//! before using it. +//! Caveat: Dead keys don't function yet on Linux +//! +//! ## EventType +//! +//! In order to manage different OS, the current EventType choices is a mix&match +//! to account for all possible events. +//! There is a safe mechanism to detect events no matter what, which are the +//! Unknown() variant of the enum which will contain some OS specific value. +//! Also not that not all keys are mapped to an OS code, so simulate might fail if you +//! try to send an unmapped key. Sending Unknown() variants will always work (the OS might +//! still reject it). +//! +//! ```no_run +//! # use crate::rdev::{Key, Button}; +//! /// In order to manage different OS, the current EventType choices is a mix&match +//! /// to account for all possible events. +//! #[derive(Debug)] +//! pub enum EventType { +//! /// The keys correspond to a standard qwerty layout, they don't correspond +//! /// To the actual letter a user would use, that requires some layout logic to be added. +//! KeyPress(Key), +//! KeyRelease(Key), +//! /// Some mouse will have more than 3 buttons, these are not defined, and different OS will +//! /// give different Unknown code. +//! ButtonPress(Button), +//! ButtonRelease(Button), +//! /// Values in pixels +//! MouseMove { +//! x: f64, +//! y: f64, +//! }, +//! /// Note: On Linux, there is no actual delta the actual values are ignored for delta_x +//! /// and we only look at the sign of delta_y to simulate wheelup or wheeldown. +//! Wheel { +//! delta_x: i64, +//! delta_y: i64, +//! }, +//! } +//! ``` +//! +//! +//! # Getting the main screen size +//! +//! ```no_run +//! use rdev::{display_size}; +//! +//! let (w, h) = display_size().unwrap(); +//! assert!(w > 0); +//! assert!(h > 0); +//! ``` +//! +//! # Keyboard state +//! +//! We can define a dummy Keyboard, that we will use to detect +//! what kind of EventType trigger some String. We get the currently used +//! layout for now ! +//! Caveat : This is layout dependent. If your app needs to support +//! layout switching don't use this ! +//! Caveat: On Linux, the dead keys mechanism is not implemented. +//! Caveat: Only shift and dead keys are implemented, Alt+unicode code on windows +//! won't work. +//! +//! ```no_run +//! use rdev::{Keyboard, EventType, Key, KeyboardState}; +//! +//! let mut keyboard = Keyboard::new().unwrap(); +//! let string = keyboard.add(&EventType::KeyPress(Key::KeyS)); +//! // string == Some("s") +//! ``` +//! +//! # Grabbing global events. (Requires `unstable_grab` feature) +//! +//! Installing this library with the `unstable_grab` feature adds the `grab` function +//! which hooks into the global input device event stream. +//! by suppling this function with a callback, you can intercept +//! all keyboard and mouse events before they are delivered to applications / window managers. +//! In the callback, returning None ignores the event and returning the event let's it pass. +//! There is no modification of the event possible here (yet). +//! +//! Note: the use of the word `unstable` here refers specifically to the fact that the `grab` API is unstable and subject to change +//! +//! ```no_run +//! #[cfg(feature = "unstable_grab")] +//! use rdev::{grab, Event, EventType, Key}; +//! +//! #[cfg(feature = "unstable_grab")] +//! let callback = |event: Event| -> Option { +//! if let EventType::KeyPress(Key::CapsLock) = event.event_type { +//! println!("Consuming and cancelling CapsLock"); +//! None // CapsLock is now effectively disabled +//! } +//! else { Some(event) } +//! }; +//! // This will block. +//! #[cfg(feature = "unstable_grab")] +//! if let Err(error) = grab(callback) { +//! println!("Error: {:?}", error) +//! } +//! ``` +//! +//! ## OS Caveats: +//! When using the `listen` and/or `grab` functions, the following caveats apply: +//! +//! ### Mac OS +//! The process running the blocking `grab` function (loop) needs to be the parent process (no fork before). +//! The process needs to be granted access to the Accessibility API (ie. if you're running your process +//! inside Terminal.app, then Terminal.app needs to be added in +//! System Preferences > Security & Privacy > Privacy > Accessibility) +//! If the process is not granted access to the Accessibility API, the `grab` call will fail with an +//! EventTapError (at least in MacOS 10.15, possibly other versions as well) +//! +//! ### Linux +//! The `grab` function use the `evdev` library to intercept events, so they will work with both X11 and Wayland +//! In order for this to work, the process runnign the `listen` or `grab` loop needs to either run as root (not recommended), +//! or run as a user who's a member of the `input` group (recommended) +//! Note: on some distros, the group name for evdev access is called `plugdev`, and on some systems, both groups can exist. +//! When in doubt, add your user to both groups if they exist. +//! +//! # Serialization +//! +//! Event data returned by the `listen` and `grab` functions can be serialized and de-serialized with +//! Serde if you install this library with the `serialize` feature. +mod rdev; +pub use crate::rdev::{ + Button, DisplayError, Event, EventType, GrabCallback, GrabError, Key, KeyboardState, + ListenError, SimulateError, +}; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub use crate::macos::Keyboard; +#[cfg(target_os = "macos")] +use crate::macos::{display_size as _display_size, listen as _listen, simulate as _simulate}; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +pub use crate::linux::Keyboard; +#[cfg(target_os = "linux")] +use crate::linux::{display_size as _display_size, listen as _listen, simulate as _simulate}; + +#[cfg(target_os = "windows")] +mod windows; +#[cfg(target_os = "windows")] +pub use crate::windows::Keyboard; +#[cfg(target_os = "windows")] +use crate::windows::{display_size as _display_size, listen as _listen, simulate as _simulate}; + +/// Listening to global events. Caveat: On MacOS, you require the listen +/// loop needs to be the primary app (no fork before) and need to have accessibility +/// settings enabled. +/// +/// ```no_run +/// use rdev::{listen, Event}; +/// +/// fn callback(event: Event) { +/// println!("My callback {:?}", event); +/// match event.name{ +/// Some(string) => println!("User wrote {:?}", string), +/// None => () +/// } +/// } +/// fn main(){ +/// // This will block. +/// if let Err(error) = listen(callback) { +/// println!("Error: {:?}", error) +/// } +/// } +/// ``` +pub fn listen(callback: T) -> Result<(), ListenError> +where + T: FnMut(Event) + 'static, +{ + _listen(callback) +} + +/// Sending some events +/// +/// ```no_run +/// use rdev::{simulate, Button, EventType, Key, SimulateError}; +/// use std::{thread, time}; +/// +/// fn send(event_type: &EventType) { +/// let delay = time::Duration::from_millis(20); +/// match simulate(event_type) { +/// Ok(()) => (), +/// Err(SimulateError) => { +/// println!("We could not send {:?}", event_type); +/// } +/// } +/// // Let ths OS catchup (at least MacOS) +/// thread::sleep(delay); +/// } +/// +/// fn my_shortcut() { +/// send(&EventType::KeyPress(Key::KeyS)); +/// send(&EventType::KeyRelease(Key::KeyS)); +/// +/// send(&EventType::MouseMove { x: 0.0, y: 0.0 }); +/// send(&EventType::MouseMove { x: 400.0, y: 400.0 }); +/// send(&EventType::ButtonPress(Button::Left)); +/// send(&EventType::ButtonRelease(Button::Right)); +/// send(&EventType::Wheel { +/// delta_x: 0, +/// delta_y: 1, +/// }); +/// } +/// ``` +pub fn simulate(event_type: &EventType) -> Result<(), SimulateError> { + _simulate(event_type) +} + +/// Returns the size in pixels of the main screen. +/// This is useful to use with x, y from MouseMove Event. +/// +/// ```no_run +/// use rdev::{display_size}; +/// +/// let (w, h) = display_size().unwrap(); +/// println!("My screen size : {:?}x{:?}", w, h); +/// ``` +pub fn display_size() -> Result<(u64, u64), DisplayError> { + _display_size() +} + +#[cfg(feature = "unstable_grab")] +#[cfg(target_os = "linux")] +pub use crate::linux::grab as _grab; +#[cfg(feature = "unstable_grab")] +#[cfg(target_os = "macos")] +pub use crate::macos::grab as _grab; +#[cfg(feature = "unstable_grab")] +#[cfg(target_os = "windows")] +pub use crate::windows::grab as _grab; +#[cfg(any(feature = "unstable_grab"))] +/// Grabbing global events. In the callback, returning None ignores the event +/// and returning the event let's it pass. There is no modification of the event +/// possible here. +/// Caveat: On MacOS, you require the grab +/// loop needs to be the primary app (no fork before) and need to have accessibility +/// settings enabled. +/// On Linux, you need rw access to evdev devices in /etc/input/ (usually group membership in `input` group is enough) +/// +/// ```no_run +/// use rdev::{grab, Event, EventType, Key}; +/// +/// fn callback(event: Event) -> Option { +/// println!("My callback {:?}", event); +/// match event.event_type{ +/// EventType::KeyPress(Key::Tab) => None, +/// _ => Some(event), +/// } +/// } +/// fn main(){ +/// // This will block. +/// if let Err(error) = grab(callback) { +/// println!("Error: {:?}", error) +/// } +/// } +/// ``` +#[cfg(any(feature = "unstable_grab"))] +pub fn grab(callback: T) -> Result<(), GrabError> +where + T: Fn(Event) -> Option + 'static, +{ + _grab(callback) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keyboard_state() { + // S + let mut keyboard = Keyboard::new().unwrap(); + let char_s = keyboard.add(&EventType::KeyPress(Key::KeyS)).unwrap(); + assert_eq!( + char_s, + "s".to_string(), + "This test should pass only on Qwerty layout !" + ); + let n = keyboard.add(&EventType::KeyRelease(Key::KeyS)); + assert_eq!(n, None); + + // Shift + S + keyboard.add(&EventType::KeyPress(Key::ShiftLeft)); + let char_s = keyboard.add(&EventType::KeyPress(Key::KeyS)).unwrap(); + assert_eq!(char_s, "S".to_string()); + let n = keyboard.add(&EventType::KeyRelease(Key::KeyS)); + assert_eq!(n, None); + keyboard.add(&EventType::KeyRelease(Key::ShiftLeft)); + + // Reset + keyboard.add(&EventType::KeyPress(Key::ShiftLeft)); + keyboard.reset(); + let char_s = keyboard.add(&EventType::KeyPress(Key::KeyS)).unwrap(); + assert_eq!(char_s, "s".to_string()); + let n = keyboard.add(&EventType::KeyRelease(Key::KeyS)); + assert_eq!(n, None); + keyboard.add(&EventType::KeyRelease(Key::ShiftLeft)); + + // UsIntl layout required + // let n = keyboard.add(&EventType::KeyPress(Key::Quote)); + // assert_eq!(n, Some("".to_string())); + // let m = keyboard.add(&EventType::KeyRelease(Key::Quote)); + // assert_eq!(m, None); + // let e = keyboard.add(&EventType::KeyPress(Key::KeyE)).unwrap(); + // assert_eq!(e, "é".to_string()); + // keyboard.add(&EventType::KeyRelease(Key::KeyE)); + } +} diff --git a/rdev/src/linux/common.rs b/rdev/src/linux/common.rs new file mode 100644 index 0000000..babd5e6 --- /dev/null +++ b/rdev/src/linux/common.rs @@ -0,0 +1,134 @@ +use crate::linux::keyboard::Keyboard; +use crate::linux::keycodes::key_from_code; +use crate::rdev::{Button, Event, EventType, KeyboardState}; +use std::convert::TryInto; +use std::os::raw::{c_int, c_uchar, c_uint}; +use std::ptr::null; +use std::time::SystemTime; +use x11::xlib; + +pub const TRUE: c_int = 1; +pub const FALSE: c_int = 0; + +// A global for the callbacks. +pub static mut KEYBOARD: Option = None; + +pub fn convert_event(code: c_uchar, type_: c_int, x: f64, y: f64) -> Option { + match type_ { + xlib::KeyPress => { + let key = key_from_code(code.into()); + Some(EventType::KeyPress(key)) + } + xlib::KeyRelease => { + let key = key_from_code(code.into()); + Some(EventType::KeyRelease(key)) + } + // Xlib does not implement wheel events left and right afaik. + // But MacOS does, so we need to acknowledge the larger event space. + xlib::ButtonPress => match code { + 1 => Some(EventType::ButtonPress(Button::Left)), + 2 => Some(EventType::ButtonPress(Button::Middle)), + 3 => Some(EventType::ButtonPress(Button::Right)), + 4 => Some(EventType::Wheel { + delta_y: 1, + delta_x: 0, + }), + 5 => Some(EventType::Wheel { + delta_y: -1, + delta_x: 0, + }), + #[allow(clippy::identity_conversion)] + code => Some(EventType::ButtonPress(Button::Unknown(code))), + }, + xlib::ButtonRelease => match code { + 1 => Some(EventType::ButtonRelease(Button::Left)), + 2 => Some(EventType::ButtonRelease(Button::Middle)), + 3 => Some(EventType::ButtonRelease(Button::Right)), + 4 | 5 => None, + #[allow(clippy::identity_conversion)] + _ => Some(EventType::ButtonRelease(Button::Unknown(code))), + }, + xlib::MotionNotify => Some(EventType::MouseMove { x, y }), + _ => None, + } +} + +pub fn convert( + keyboard: &mut Option, + code: c_uint, + type_: c_int, + x: f64, + y: f64, +) -> Option { + let event_type = convert_event(code as c_uchar, type_, x, y)?; + let kb: &mut Keyboard = (*keyboard).as_mut()?; + let name = kb.add(&event_type); + Some(Event { + event_type, + time: SystemTime::now(), + name, + }) +} + +pub struct Display { + display: *mut xlib::Display, +} + +impl Display { + pub fn new() -> Option { + unsafe { + let display = xlib::XOpenDisplay(null()); + if display.is_null() { + return None; + } + Some(Display { display }) + } + } + + pub fn get_size(&self) -> Option<(u64, u64)> { + unsafe { + let screen_ptr = xlib::XDefaultScreenOfDisplay(self.display); + if screen_ptr.is_null() { + return None; + } + let screen = *screen_ptr; + Some(( + screen.width.try_into().ok()?, + screen.height.try_into().ok()?, + )) + } + } + + #[cfg(feature = "unstable_grab")] + pub fn get_mouse_pos(&self) -> Option<(u64, u64)> { + unsafe { + let root_window = xlib::XRootWindow(self.display, 0); + let mut root_x = 0; + let mut root_y = 0; + let mut x = 0; + let mut y = 0; + let mut root = 0; + let mut child = 0; + let mut mask = 0; + let _screen_ptr = xlib::XQueryPointer( + self.display, + root_window, + &mut root, + &mut child, + &mut root_x, + &mut root_y, + &mut x, + &mut y, + &mut mask, + ); + Some((root_x.try_into().ok()?, root_y.try_into().ok()?)) + } + } +} +impl Drop for Display { + fn drop(&mut self) { + unsafe { + xlib::XCloseDisplay(self.display); + } + } +} diff --git a/rdev/src/linux/display.rs b/rdev/src/linux/display.rs new file mode 100644 index 0000000..d50e91f --- /dev/null +++ b/rdev/src/linux/display.rs @@ -0,0 +1,7 @@ +use crate::linux::common::Display; +use crate::rdev::DisplayError; + +pub fn display_size() -> Result<(u64, u64), DisplayError> { + let display = Display::new().ok_or(DisplayError::NoDisplay)?; + display.get_size().ok_or(DisplayError::NoDisplay) +} diff --git a/rdev/src/linux/grab.rs b/rdev/src/linux/grab.rs new file mode 100644 index 0000000..3ddefbb --- /dev/null +++ b/rdev/src/linux/grab.rs @@ -0,0 +1,529 @@ +use crate::linux::common::Display; +use crate::linux::keyboard::Keyboard; +use crate::rdev::{Button, Event, EventType, GrabError, Key, KeyboardState}; +use epoll::ControlOptions::{EPOLL_CTL_ADD, EPOLL_CTL_DEL}; +use evdev_rs::{ + enums::{EventCode, EV_KEY, EV_REL}, + Device, InputEvent, UInputDevice, +}; +use inotify::{Inotify, WatchMask}; +use std::ffi::{OsStr, OsString}; +use std::fs::{read_dir, File}; +use std::io; +use std::os::unix::{ + ffi::OsStrExt, + fs::FileTypeExt, + io::{AsRawFd, IntoRawFd, RawFd}, +}; +use std::path::Path; +use std::time::SystemTime; + +// TODO The x, y coordinates are currently wrong !! Is there mouse acceleration +// to take into account ?? + +macro_rules! convert_keys { + ($($ev_key:ident, $rdev_key:ident),*) => { + //TODO: make const when rust lang issue #49146 is fixed + #[allow(unreachable_patterns)] + fn evdev_key_to_rdev_key(key: &EV_KEY) -> Option { + match key { + $( + EV_KEY::$ev_key => Some(Key::$rdev_key), + )* + _ => None, + } + } + + // //TODO: make const when rust lang issue #49146 is fixed + // fn rdev_key_to_evdev_key(key: &Key) -> Option { + // match key { + // $( + // Key::$rdev_key => Some(EV_KEY::$ev_key), + // )* + // _ => None + // } + // } + }; +} + +macro_rules! convert_buttons { + ($($ev_key:ident, $rdev_key:ident),*) => { + //TODO: make const when rust lang issue #49146 is fixed + fn evdev_key_to_rdev_button(key: &EV_KEY) -> Option