diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3572afe3..598c9d37 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,17 +19,35 @@ jobs: rust: - stable - nightly - - "1.71.0" + - "1.75.0" platform: - ubuntu-latest - windows-latest - macos-latest features: - - default - - default,serde - - wayland - - xdo - - x11rb + - "--all-features" + - "--features default" + - "--features libei" + - "--features wayland" + - "--features xdo" + - "--features x11rb" + exclude: + - platform: windows-latest + features: "--features libei" + - platform: windows-latest + features: "--features wayland" + - platform: windows-latest + features: "--features xdo" + - platform: windows-latest + features: "--features x11rb" + - platform: macos-latest + features: "--features libei" + - platform: macos-latest + features: "--features wayland" + - platform: macos-latest + features: "--features xdo" + - platform: macos-latest + features: "--features x11rb" runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 @@ -44,46 +62,34 @@ jobs: run: cargo fmt -- --check - name: Check clippy lints - run: cargo clippy --all-targets --all-features -- -D clippy::pedantic + run: cargo clippy --no-default-features ${{ matrix.features }} -- -D clippy::pedantic - name: Check clippy lints for the examples - run: cargo clippy --all-targets --all-features --examples -- -D clippy::pedantic + run: cargo clippy --no-default-features ${{ matrix.features }} --examples -- -D clippy::pedantic - name: Build the code - run: cargo build --no-default-features --features ${{ matrix.features }} - - name: Build the code with all features enabled - run: cargo build --all-features + run: cargo build --no-default-features ${{ matrix.features }} - name: Build the docs - run: cargo doc --no-deps --no-default-features --features ${{ matrix.features }} - - name: Build the docs with all features enabled - run: cargo doc --no-deps --all-features + run: cargo doc --no-deps --no-default-features ${{ matrix.features }} - name: Build the examples - run: cargo check --examples --no-default-features --features ${{ matrix.features }} - - name: Build the examples with all features enabled - run: cargo check --examples --all-features + run: cargo check --examples --no-default-features ${{ matrix.features }} - name: Build the examples in release mode - run: cargo check --release --examples --no-default-features --features ${{ matrix.features }} - - name: Build the examples in release mode with all features enabled - run: cargo check --release --examples --all-features + run: cargo check --release --examples --no-default-features ${{ matrix.features }} - name: Test the code - run: cargo test --no-default-features --features ${{ matrix.features }} - - name: Test the code with all features enabled - run: cargo test --all-features + run: cargo test --no-default-features ${{ matrix.features }} - name: Test the code in release mode - run: cargo test --release --no-default-features --features ${{ matrix.features }} - - name: Test the code in release mode with all features enabled - run: cargo test --release --all-features + run: cargo test --release --no-default-features ${{ matrix.features }} - name: Setup headless display for integration tests if: runner.os == 'Linux' # The integration tests only work on Linux right now uses: ./.github/actions/headless_display - name: Run integration tests - if: runner.os == 'Linux' # The integration tests only work on Linux right now - run: cargo test --all-features -- --include-ignored --nocapture + if: runner.os == 'Linux' && matrix.features == 'xdo' # The integration tests only work on Linux and x11 right now + run: cargo test --no-default-features ${{ matrix.features }} -- --include-ignored --nocapture - name: Install Miri if: matrix.rust == 'nightly' # Miri only works on Nightly @@ -94,4 +100,4 @@ jobs: cargo miri setup - name: Run Miri to check unsafe code if: matrix.rust == 'nightly' # Miri only works on Nightly - run: cargo miri test --verbose --all-features \ No newline at end of file + run: cargo miri test --verbose --no-default-features ${{ matrix.features }} \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index 4b833018..6cf773dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ ## Changed - All: A new Enigo struct is now always created with some settings -- Rust: MSRV is 1.71 +- Rust: MSRV is 1.75 - All held keys are released when Enigo is dropped - win: Don't panic if it was not possible to move the mouse - All: Never panic! All functions return Results now @@ -20,6 +20,7 @@ - DSL: The DSL was removed and replaced with the `Agent` trait. Activate the `serde` feature to use it. Have a look at the `serde` example to get an idea how to use it ## Added +- Linux: Partial support for `libei` was added. Use the experimental feature `libei` to test it. This works on GNOME 46 and above. Entering text often simulates the wrong characters. - Linux: Support X11 without `xdotools`. Use the experimental feature `x11rb` to test it - Linux: Partial support for Wayland was added. Use the experimental feature `wayland` to test it. Only the virtual_keyboard and input_method protocol can be used. This is not going to work on GNOME, but should work for example with phosh - Linux: Added `MicMute` key to enter `XF86_AudioMicMute` keysym diff --git a/Cargo.toml b/Cargo.toml index 20630f88..c40f0755 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ authors = [ "Dustin Bensing ", ] edition = "2021" -rust-version = "1.71" +rust-version = "1.75" description = "Cross-platform (Linux, Windows & macOS) library to simulate keyboard and mouse events" documentation = "https://docs.rs/enigo/" homepage = "https://github.com/enigo-rs/enigo" @@ -27,7 +27,8 @@ exclude = [".github", "examples", ".gitignore", "rustfmt.toml"] all-features = true [features] -default = ["xdo"] +default = ["libei"] +libei = ["dep:reis", "dep:ashpd", "dep:pollster", "dep:once_cell"] serde = ["dep:serde"] wayland = [ "dep:wayland-client", @@ -59,6 +60,10 @@ foreign-types-shared = "0.3" [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] libc = "0.2" +reis = { version = "0.2.0", optional = true } +ashpd = { version = "0.8.1", optional = true } +pollster = { version = "0.3.0", optional = true } +once_cell = { version = "1.19.0", optional = true } wayland-protocols-misc = { version = "0.2", features = [ "client", ], optional = true } diff --git a/README.md b/README.md index 2636db55..8c2be95b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Docs](https://docs.rs/enigo/badge.svg)](https://docs.rs/enigo) [![Dependency status](https://deps.rs/repo/github/enigo-rs/enigo/status.svg)](https://deps.rs/repo/github/enigo-rs/enigo) -![Rust version](https://img.shields.io/badge/rust--version-1.71+-brightgreen.svg) +![Rust version](https://img.shields.io/badge/rust--version-1.75+-brightgreen.svg) [![Crates.io](https://img.shields.io/crates/v/enigo.svg)](https://crates.io/crates/enigo) # enigo diff --git a/src/keycodes.rs b/src/keycodes.rs index eb2a2054..4019eafa 100644 --- a/src/keycodes.rs +++ b/src/keycodes.rs @@ -1028,7 +1028,7 @@ impl TryFrom for windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY { } #[cfg(all(unix, not(target_os = "macos")))] -#[cfg(any(feature = "wayland", feature = "x11rb"))] +#[cfg(any(feature = "wayland", feature = "x11rb", feature = "libei"))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(crate) enum Modifier { @@ -1043,7 +1043,7 @@ pub(crate) enum Modifier { } #[cfg(all(unix, not(target_os = "macos")))] -#[cfg(any(feature = "wayland", feature = "x11rb"))] +#[cfg(any(feature = "wayland", feature = "x11rb", feature = "libei"))] impl Modifier { /// Returns the bitflag of the modifier that is usually associated with it /// on Linux @@ -1079,7 +1079,7 @@ impl Modifier { } #[cfg(all(unix, not(target_os = "macos")))] -#[cfg(any(feature = "wayland", feature = "x11rb"))] +#[cfg(any(feature = "wayland", feature = "x11rb", feature = "libei"))] /// Converts a Key to a modifier impl TryFrom for Modifier { type Error = &'static str; @@ -1101,5 +1101,5 @@ impl TryFrom for Modifier { } #[cfg(all(unix, not(target_os = "macos")))] -#[cfg(any(feature = "wayland", feature = "x11rb"))] +#[cfg(any(feature = "wayland", feature = "x11rb", feature = "libei"))] pub(crate) type ModifierBitflag = u32; diff --git a/src/linux/libei.rs b/src/linux/libei.rs new file mode 100644 index 00000000..19d5f3f2 --- /dev/null +++ b/src/linux/libei.rs @@ -0,0 +1,724 @@ +use ashpd::desktop::remote_desktop::RemoteDesktop; +use log::{debug, error, trace, warn}; +use pollster::FutureExt as _; +use reis::{ + ei::{self, Connection}, + handshake::HandshakeResp, + PendingRequestResult, +}; +use std::{collections::HashMap, os::unix::net::UnixStream, time::Instant}; +use xkbcommon::xkb; + +use crate::{ + Axis, Button, Coordinate, Direction, InputError, InputResult, Key, Keyboard, Mouse, NewConError, +}; +pub type Keycode = u32; + +static INTERFACES: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + let mut m = HashMap::new(); + m.insert("ei_button", 1); + m.insert("ei_callback", 1); + m.insert("ei_connection", 1); + m.insert("ei_device", 2); + m.insert("ei_keyboard", 1); + m.insert("ei_pingpong", 1); + m.insert("ei_pointer", 1); + m.insert("ei_pointer_absolute", 1); + m.insert("ei_scroll", 1); + m.insert("ei_seat", 1); + m + }); + +#[derive(Debug, Default, PartialEq, Clone)] +struct SeatData { + name: Option, + capabilities: HashMap, +} + +#[derive(Debug, Default, PartialEq, Copy, Clone)] +enum DeviceState { + #[default] + Paused, + Resumed, + Emulating, +} + +#[derive(Debug, Default, PartialEq, Copy, Clone)] +struct DeviceRegion { + offset_x: u32, // region x offset in logical pixels + offset_y: u32, // region y offset in logical pixels + width: u32, // region width in logical pixels + height: u32, // region height in logical pixels + scale: f32, // the physical scale for this region +} + +#[derive(Debug, Default, PartialEq, Clone)] +struct DeviceData { + name: Option, + device_type: Option, + interfaces: HashMap, + state: DeviceState, + dimensions: Option<(u32, u32)>, // width, height + regions: Vec, +} + +impl DeviceData { + fn interface(&self) -> Option { + self.interfaces.get(T::NAME)?.clone().downcast() + } +} + +/// The main struct for handling the event emitting +#[derive(Clone)] +pub struct Con { + // XXX best way to handle data associated with object? + // TODO: Release seat when dropped, so compositor knows it wont be used anymore + seats: HashMap, + // XXX association with seat? + // TODO: Release device when dropped, so compositor knows it wont be used anymore + devices: HashMap, + keyboards: HashMap, + /// `None` if there was no disconnect + disconnect: Option<(ei::connection::DisconnectReason, String)>, + sequence: u32, + last_serial: u32, + context: ei::Context, + connection: Connection, + time_created: Instant, +} + +// This is safe, we have a unique pointer. +// TODO: use Unique once stable. +unsafe impl Send for Con {} + +impl Con { + async fn open_connection() -> ei::Context { + use ashpd::desktop::remote_desktop::DeviceType; + + trace!("open_connection"); + if let Some(context) = ei::Context::connect_to_env().unwrap() { + trace!("done open_connection after connect_to_env"); + context + } else { + debug!("Unable to find ei socket. Trying xdg desktop portal."); + let remote_desktop = RemoteDesktop::new().await.unwrap(); + trace!("New desktop"); + + // device_bitmask |= DeviceType::Touchscreen; + let session = remote_desktop.create_session().await.unwrap(); + remote_desktop + .select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer) // TODO: Add DeviceType::Touchscreen once we support it in enigo + .await + .unwrap(); + trace!("new session"); + remote_desktop + .start(&session, &ashpd::WindowIdentifier::default()) + .await + .unwrap(); + trace!("start session"); + // This is needed so there is no zbus error + std::thread::sleep(std::time::Duration::from_millis(10)); + let fd = remote_desktop.connect_to_eis(&session).await.unwrap(); + let stream = UnixStream::from(fd); + stream.set_nonblocking(true).unwrap(); // TODO: Check if this is a good idea + trace!("done open_connection"); + ei::Context::new(stream).unwrap() + } + } + + #[allow(clippy::unnecessary_wraps)] + /// Create a new Enigo instance + pub fn new() -> Result { + debug!("using libei"); + + let libei_name = "enigo"; + + let seats = HashMap::new(); + let devices = HashMap::new(); + let keyboards = HashMap::new(); + let disconnect = None; + let sequence = 0; + let time_created = Instant::now(); + + let context = Self::open_connection().block_on(); + let HandshakeResp { + connection, + serial, + negotiated_interfaces, + } = reis::handshake::ei_handshake_blocking( + &context, + libei_name, + ei::handshake::ContextType::Sender, + &INTERFACES, + ) + .unwrap(); + + trace!("main: handshake"); + + context + .flush() + .map_err(|_| NewConError::EstablishCon("unable to flush the libei context"))?; + trace!("main: flushed"); + + let mut con = Self { + seats, + devices, + keyboards, + disconnect, + sequence, + last_serial: serial.wrapping_add(1), + context, + connection, + time_created, + }; + + con.update(libei_name) + .map_err(|_| NewConError::EstablishCon("unable to update the libei connection"))?; + + for (device, device_data) in con.devices.iter_mut().filter(|(_, ref device_data)| { + device_data.device_type == Some(reis::ei::device::DeviceType::Virtual) + && device_data.state == DeviceState::Resumed + // TODO: Should all devices start emulating? + // && device_data.interface::().is_some() + }) { + println!("Start emulating"); + device.start_emulating(con.sequence, con.last_serial); + con.sequence = con.sequence.wrapping_add(1); + device_data.state = DeviceState::Emulating; + } + + con.update(libei_name) + .map_err(|_| NewConError::EstablishCon("unable to update the libei connection"))?; + + Ok(con) + } + + #[allow(clippy::too_many_lines)] + fn update(&mut self, libei_name: &str) -> InputResult<()> { + // TODO: Don't blindly do it 50 times but check if it is needed + for _ in 0..50 { + debug!("update"); + if self.context.read().is_err() { + error!("err reading"); + return Err(InputError::Simulate("Failed to update libei context")); + } + + while let Some(result) = self.context.pending_event() { + trace!("found pending_event"); + + let request = match result { + PendingRequestResult::Request(request) => request, + PendingRequestResult::ParseError(msg) => { + todo!() + } + PendingRequestResult::InvalidObject(object_id) => { + // TODO + error!("invalid object with id {object_id}"); + continue; + } + }; + + trace!("found request"); + match request { + ei::Event::Handshake(handshake, request) => match request { + ei::handshake::Event::HandshakeVersion { version: _ } => { + trace!("handshake version"); + handshake.handshake_version(1); + handshake.name(libei_name); + handshake.context_type(ei::handshake::ContextType::Sender); + for (interface, version) in INTERFACES.iter() { + handshake.interface_version(interface, *version); + } + handshake.finish(); + } + ei::handshake::Event::InterfaceVersion { name, version } => { + // TODO: Use the interface versions + trace!("Received: interface {name}, version {version}"); + } + ei::handshake::Event::Connection { + connection: _, + serial, + } => { + trace!("handshake connection"); + self.last_serial = serial; + self.sequence = serial; + } + _ => { + warn!("handshake else"); + } + }, + ei::Event::Connection(connection, request) => match request { + ei::connection::Event::Disconnected { + last_serial, + reason, + explanation, + } => { + self.seats.clear(); + self.seats.shrink_to_fit(); + self.devices.clear(); + self.devices.shrink_to_fit(); + self.keyboards.clear(); + self.keyboards.shrink_to_fit(); + self.disconnect = Some((reason, explanation)); + self.sequence = 0; + self.last_serial = last_serial; + } + ei::connection::Event::Seat { seat } => { + trace!("connection seat"); + self.seats.insert(seat, SeatData::default()); + } + ei::connection::Event::InvalidObject { + last_serial, + invalid_id, + } => { + // TODO: Try to recover? + error!("the serial {last_serial} contained an invalid object with the id {invalid_id}"); + } + ei::connection::Event::Ping { ping } => { + debug!("ping"); + ping.done(0); + } + _ => { + warn!("Unknown connection event"); + } + }, + ei::Event::Seat(seat, request) => { + trace!("connection seat"); + let data = self.seats.get_mut(&seat).unwrap(); + match request { + ei::seat::Event::Destroyed { serial } => { + debug!("seat was destroyed"); + self.seats.remove(&seat); + } + ei::seat::Event::Name { name } => { + data.name = Some(name); + } + ei::seat::Event::Capability { mask, interface } => { + data.capabilities.insert(interface, mask); + } + ei::seat::Event::Done => { + let mut bitmask = 0; + if let Some(bits) = data.capabilities.get("ei_button") { + bitmask |= bits; + } + if let Some(bits) = data.capabilities.get("ei_keyboard") { + bitmask |= bits; + } + if let Some(bits) = data.capabilities.get("ei_pointer") { + bitmask |= bits; + } + if let Some(bits) = data.capabilities.get("ei_pointer_absolute") { + bitmask |= bits; + } + if let Some(bits) = data.capabilities.get("ei_scroll") { + bitmask |= bits; + } + if let Some(bits) = data.capabilities.get("ei_touchscreen") { + bitmask |= bits; + } + + seat.bind(bitmask); + trace!("done binding to seat"); + } + ei::seat::Event::Device { device } => { + self.devices.insert(device, DeviceData::default()); + } + _ => { + warn!("Unknown seat event"); + } + } + } + ei::Event::Device(device, request) => { + trace!("device event"); + let data = self.devices.get_mut(&device).unwrap(); + match request { + ei::device::Event::Destroyed { serial } => { + debug!("device was destroyed"); + self.devices.remove(&device); + } + ei::device::Event::Name { name } => { + trace!("device name"); + data.name = Some(name); + } + ei::device::Event::DeviceType { device_type } => { + trace!("device type"); + data.device_type = Some(device_type); + } + ei::device::Event::Dimensions { width, height } => { + trace!("device type"); + data.dimensions = Some((width, height)); + } + ei::device::Event::Region { + offset_x, + offset_y, + width, + hight: height, + scale, + } => { + trace!("device type"); + data.regions.push(DeviceRegion { + offset_x, + offset_y, + width, + height, + scale, + }); + } + ei::device::Event::Interface { object } => { + trace!("device interface"); + data.interfaces + .insert(object.interface().to_string(), object); + } + ei::device::Event::Done => { + trace!("device done"); + } + ei::device::Event::Resumed { serial } => { + debug!("device resumed"); + self.last_serial = serial; + data.state = DeviceState::Resumed; + } + ei::device::Event::Paused { serial } => { + debug!("device paused"); + self.last_serial = serial; + data.state = DeviceState::Paused; + } + _ => { + warn!("device else"); + } + } + } + ei::Event::Keyboard(keyboard, request) => { + trace!("keyboard event"); + match request { + ei::keyboard::Event::Destroyed { serial } => { + debug!("keyboard was destroyed"); + self.keyboards.remove(&keyboard); + } + ei::keyboard::Event::Keymap { + keymap_type, + size, + keymap, + } => { + if keymap_type != ei::keyboard::KeymapType::Xkb { + error!("The keymap is of the wrong type"); + } + let context = xkb::Context::new(0); + self.keyboards.insert( + keyboard, + unsafe { + xkb::Keymap::new_from_fd( + &context, + keymap, + size as _, + xkb::KEYMAP_FORMAT_TEXT_V1, + 0, + ) + } + .unwrap() + .unwrap(), + ); + } + ei::keyboard::Event::Modifiers { + serial, + depressed, + locked, + latched, + group, + } => { // TODO: Handle updated modifiers + // Notification that the EIS + // implementation has changed modifier states + // on this device. Future ei_keyboard.key + // requests must take the new modifier state + // into account. + } + _ => {} + } + } + _ => { + warn!("else"); + } + } + } + + trace!("devices: {:?}", self.devices); + + if let Ok(()) = self.context.flush() { + trace!("flush success"); + } else { + error!("flush fail"); + } + + // This is needed so anything is typed + std::thread::sleep(std::time::Duration::from_millis(10)); + trace!("update flush"); + trace!("update done"); + } + Ok(()) + } +} + +impl Keyboard for Con { + fn fast_text(&mut self, text: &str) -> InputResult> { + warn!("fast text entry is not yet implemented with libei"); + // TODO: Add fast method + Ok(None) + } + + fn key(&mut self, key: Key, direction: Direction) -> InputResult<()> { + if let Some((device, device_data)) = self + .devices + .iter_mut() + .find(|(_, ref device_data)| device_data.interface::().is_some()) + { + if let Some((keyboard, keymap)) = self.keyboards.iter().next() { + let keycode = key_to_keycode(keymap, key)?; + + if direction == Direction::Press || direction == Direction::Click { + keyboard.key(keycode - 8, ei::keyboard::KeyState::Press); + } + if direction == Direction::Release || direction == Direction::Click { + keyboard.key(keycode - 8, ei::keyboard::KeyState::Released); + } + + let elapsed = self.time_created.elapsed().as_secs(); // Is seconds fine? + + device.frame(self.sequence, elapsed); + self.sequence = self.sequence.wrapping_add(1); + self.update("enigo").map_err(|_| { + InputError::Simulate("unable to update the libei connection to scroll") + })?; + } + } + Ok(()) + } + + fn raw(&mut self, keycode: u16, direction: Direction) -> InputResult<()> { + let keycode = keycode as u32; + + if let Some((device, device_data)) = self + .devices + .iter_mut() + .find(|(_, ref device_data)| device_data.interface::().is_some()) + { + let keyboard = device_data.interface::().unwrap(); + + if direction == Direction::Press || direction == Direction::Click { + keyboard.key(keycode - 8, ei::keyboard::KeyState::Press); + } + if direction == Direction::Release || direction == Direction::Click { + keyboard.key(keycode - 8, ei::keyboard::KeyState::Released); + } + + let elapsed = self.time_created.elapsed().as_secs(); // Is seconds fine? + + device.frame(self.sequence, elapsed); + self.sequence = self.sequence.wrapping_add(1); + self.update("enigo").map_err(|_| { + InputError::Simulate("unable to update the libei connection to scroll") + })?; + } + Ok(()) + } +} + +impl Mouse for Con { + fn button(&mut self, button: Button, direction: Direction) -> InputResult<()> { + if let Some((device, device_data)) = self + .devices + .iter_mut() + .find(|(_, ref device_data)| device_data.interface::().is_some()) + { + // Do nothing if one of the mouse scroll buttons was released + // Releasing one of the scroll mouse buttons has no effect + if direction == Direction::Release { + match button { + Button::Left + | Button::Right + | Button::Back + | Button::Forward + | Button::Middle => {} + Button::ScrollDown + | Button::ScrollUp + | Button::ScrollRight + | Button::ScrollLeft => return Ok(()), + } + }; + + let button = match button { + // Taken from /linux/input-event-codes.h + Button::Left => 0x110, + Button::Right => 0x111, + Button::Back => 0x116, + Button::Forward => 0x115, + Button::Middle => 0x112, + Button::ScrollDown => return self.scroll(1, Axis::Vertical), + Button::ScrollUp => return self.scroll(-1, Axis::Vertical), + Button::ScrollRight => return self.scroll(1, Axis::Horizontal), + Button::ScrollLeft => return self.scroll(-1, Axis::Horizontal), + }; + + let vp = device_data.interface::().unwrap(); + + if direction == Direction::Press || direction == Direction::Click { + trace!("vp.button({button}, ei::button::ButtonState::Pressed)"); + vp.button(button, ei::button::ButtonState::Press); + // self.update("enigo"); + let elapsed = self.time_created.elapsed().as_secs(); // Is seconds fine? + device.frame(self.sequence, elapsed); + self.sequence = self.sequence.wrapping_add(1); + } + + if direction == Direction::Release || direction == Direction::Click { + trace!("vp.button({button}, ei::button::ButtonState::Released)"); + vp.button(button, ei::button::ButtonState::Released); + // self.update("enigo"); + let elapsed = self.time_created.elapsed().as_secs(); // Is seconds fine? + device.frame(self.sequence, elapsed); + self.sequence = self.sequence.wrapping_add(1); + } + self.update("enigo").map_err(|_| { + InputError::Simulate("unable to update the libei connection to simulate a button") + })?; + } + Ok(()) + } + + fn move_mouse(&mut self, x: i32, y: i32, coordinate: Coordinate) -> InputResult<()> { + #[allow(clippy::cast_precision_loss)] + let (x, y) = (x as f32, y as f32); + match coordinate { + Coordinate::Rel => { + trace!("vp.motion_relative({x}, {y})"); + if let Some((device, device_data)) = self + .devices + .iter() + .find(|(_, device_data)| device_data.interface::().is_some()) + { + let vp = device_data.interface::().unwrap(); + vp.motion_relative(x, y); + + let elapsed = self.time_created.elapsed().as_secs(); // Is seconds fine? + + device.frame(self.sequence, elapsed); + self.sequence = self.sequence.wrapping_add(1); + + self.update("enigo").map_err(|_| { + InputError::Simulate( + "unable to update the libei connection to move the mouse", + ) + })?; + return Ok(()); + } + } + Coordinate::Abs => { + if x < 0.0 || y < 0.0 { + return Err(InputError::InvalidInput( + "the absolute coordinates cannot be negative", + )); + }; + trace!("vp.motion_absolute({x}, {y}, u32::MAX, u32::MAX)"); + if let Some((device, device_data)) = self.devices.iter().find(|(_, device_data)| { + device_data.interface::().is_some() + }) { + let vp = device_data.interface::().unwrap(); + vp.motion_absolute(x, y); + + let elapsed = self.time_created.elapsed().as_secs(); // Is seconds fine? + + device.frame(self.sequence, elapsed); + self.sequence = self.sequence.wrapping_add(1); + + self.update("enigo").map_err(|_| { + InputError::Simulate( + "unable to update the libei connection to move the mouse", + ) + })?; + return Ok(()); + } + } + }; + // TODO: Improve the error + Err(InputError::Simulate( + "None of the devices implements the move mouse interface so there is no way to move it", + )) + } + + fn scroll(&mut self, length: i32, axis: Axis) -> InputResult<()> { + #[allow(clippy::cast_precision_loss)] + let length = length as f32; + if let Some((device, device_data)) = self + .devices + .iter() + .find(|(_, device_data)| device_data.interface::().is_some()) + { + let (x, y) = match axis { + Axis::Horizontal => (length, 0.0), + Axis::Vertical => (0.0, length), + }; + trace!("vp.scroll({x}, {y})"); + let vp = device_data.interface::().unwrap(); + vp.scroll(x, y); + + let elapsed = self.time_created.elapsed().as_secs(); // Is seconds fine? + + device.frame(self.sequence, elapsed); + self.sequence = self.sequence.wrapping_add(1); + self.update("enigo").map_err(|_| { + InputError::Simulate("unable to update the libei connection to scroll") + })?; + return Ok(()); + } + Err(InputError::Simulate( + "None of the devices implements the Scroll interface so there is no way to scroll", + )) + } + + fn main_display(&self) -> InputResult<(i32, i32)> { + // TODO Implement this + error!("You tried to get the dimensions of the main display. I don't know how this is possible under Wayland. Let me know if there is a new protocol"); + Err(InputError::Simulate("Not implemented yet")) + } + + fn location(&self) -> InputResult<(i32, i32)> { + // TODO Implement this + error!("You tried to get the mouse location. I don't know how this is possible under Wayland. Let me know if there is a new protocol"); + Err(InputError::Simulate("Not implemented yet")) + } +} + +impl Drop for Con { + fn drop(&mut self) { + // TODO: Is it needed to filter or can we just stop emulating on all devices?? + for (device, _) in self.devices.iter().filter(|(_, device_data)| { + device_data.device_type == Some(reis::ei::device::DeviceType::Virtual) + && device_data.state == DeviceState::Emulating + }) { + println!("DROPPED"); + device.stop_emulating(self.last_serial); + self.last_serial = self.last_serial.wrapping_add(1); + } + self.connection.disconnect(); // Let the server know we voluntarily disconnected + + let _ = self.context.flush(); // Ignore the errors if the connection was + // dropped + } +} + +fn key_to_keycode(keymap: &xkb::Keymap, key: Key) -> InputResult { + let all_keycodes = keymap.min_keycode().raw()..keymap.max_keycode().raw(); + + let keysym = xkb::Keysym::from(key); + let mut keycode = None; + 'outer: for i in all_keycodes.clone() { + for j in 0..=1 { + let syms = keymap.key_get_syms_by_level(xkb::Keycode::new(i), 0, j); + if syms.contains(&keysym) { + keycode = Some(i); + break 'outer; + } + } + } + // Panics if the keysym was not mapped + keycode.ok_or(crate::InputError::InvalidInput("Key is not mapped")) +} diff --git a/src/linux/mod.rs b/src/linux/mod.rs index c5b30311..add31aba 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -6,11 +6,19 @@ use crate::{ }; // If none of these features is enabled, there is no way to simulate input -#[cfg(not(any(feature = "wayland", feature = "x11rb", feature = "xdo")))] +#[cfg(not(any( + feature = "wayland", + feature = "x11rb", + feature = "xdo", + feature = "libei" +)))] compile_error!( - "either feature `wayland`, `x11rb` or `xdo` must be enabled for this crate when using linux" + "either feature `wayland`, `x11rb`, `xdo` or `libei` must be enabled for this crate when using linux" ); +#[cfg(feature = "libei")] +mod libei; + #[cfg(feature = "wayland")] mod wayland; #[cfg(any(feature = "x11rb", feature = "xdo"))] @@ -33,6 +41,8 @@ pub struct Enigo { wayland: Option, #[cfg(any(feature = "x11rb", feature = "xdo"))] x11: Option, + #[cfg(feature = "libei")] + libei: Option, } impl Enigo { @@ -90,6 +100,18 @@ impl Enigo { None } }; + #[cfg(feature = "libei")] + let libei = match libei::Con::new() { + Ok(con) => { + connection_established = true; + debug!("libei connection established"); + Some(con) + } + Err(e) => { + warn!("failed to establish libei connection: {e}"); + None + } + }; if !connection_established { error!("no successful connection"); return Err(NewConError::EstablishCon("no successful connection")); @@ -102,6 +124,8 @@ impl Enigo { wayland, #[cfg(any(feature = "x11rb", feature = "xdo"))] x11, + #[cfg(feature = "libei")] + libei, }) } @@ -138,6 +162,13 @@ impl Mouse for Enigo { fn button(&mut self, button: Button, direction: Direction) -> InputResult<()> { debug!("\x1b[93mbutton(button: {button:?}, direction: {direction:?})\x1b[0m"); let mut success = false; + #[cfg(feature = "libei")] + if let Some(con) = self.libei.as_mut() { + trace!("try sending button event via libei"); + con.button(button, direction)?; + debug!("sent button event via libei"); + success = true; + } #[cfg(feature = "wayland")] if let Some(con) = self.wayland.as_mut() { trace!("try sending button event via wayland"); @@ -163,6 +194,13 @@ impl Mouse for Enigo { fn move_mouse(&mut self, x: i32, y: i32, coordinate: Coordinate) -> InputResult<()> { debug!("\x1b[93mmove_mouse(x: {x:?}, y: {y:?}, coordinate:{coordinate:?})\x1b[0m"); let mut success = false; + #[cfg(feature = "libei")] + if let Some(con) = self.libei.as_mut() { + trace!("try moving the mouse via libei"); + con.move_mouse(x, y, coordinate)?; + debug!("moved the mouse via libei"); + success = true; + } #[cfg(feature = "wayland")] if let Some(con) = self.wayland.as_mut() { trace!("try moving the mouse via wayland"); @@ -188,6 +226,13 @@ impl Mouse for Enigo { fn scroll(&mut self, length: i32, axis: Axis) -> InputResult<()> { debug!("\x1b[93mscroll(length: {length:?}, axis: {axis:?})\x1b[0m"); let mut success = false; + #[cfg(feature = "libei")] + if let Some(con) = self.libei.as_mut() { + trace!("try scrolling via libei"); + con.scroll(length, axis)?; + debug!("scrolled via libei"); + success = true; + } #[cfg(feature = "wayland")] if let Some(con) = self.wayland.as_mut() { trace!("try scrolling via wayland"); @@ -212,6 +257,11 @@ impl Mouse for Enigo { fn main_display(&self) -> InputResult<(i32, i32)> { debug!("\x1b[93mmain_display()\x1b[0m"); + #[cfg(feature = "libeii")] + if let Some(con) = self.libei.as_ref() { + trace!("try getting the dimensions of the display via libei"); + return con.main_display(); + } #[cfg(feature = "wayland")] if let Some(con) = self.wayland.as_ref() { trace!("try getting the dimensions of the display via wayland"); @@ -227,6 +277,11 @@ impl Mouse for Enigo { fn location(&self) -> InputResult<(i32, i32)> { debug!("\x1b[93mlocation()\x1b[0m"); + #[cfg(feature = "libei")] + if let Some(con) = self.libei.as_ref() { + trace!("try getting the mouse location via libei"); + return con.location(); + } #[cfg(feature = "wayland")] if let Some(con) = self.wayland.as_ref() { trace!("try getting the mouse location via wayland"); @@ -244,6 +299,12 @@ impl Mouse for Enigo { impl Keyboard for Enigo { fn fast_text(&mut self, text: &str) -> InputResult> { debug!("\x1b[93mfast_text(text: {text})\x1b[0m"); + + #[cfg(feature = "libei")] + if let Some(con) = self.libei.as_mut() { + trace!("try entering text fast via libei"); + con.text(text)?; + } #[cfg(feature = "wayland")] if let Some(con) = self.wayland.as_mut() { trace!("try entering text fast via wayland"); @@ -266,6 +327,13 @@ impl Keyboard for Enigo { return Ok(()); } + #[cfg(feature = "libei")] + if let Some(con) = self.libei.as_mut() { + trace!("try entering the key via libei"); + con.key(key, direction)?; + debug!("entered the key via libei"); + } + #[cfg(feature = "wayland")] if let Some(con) = self.wayland.as_mut() { trace!("try entering the key via wayland"); @@ -298,6 +366,12 @@ impl Keyboard for Enigo { fn raw(&mut self, keycode: u16, direction: Direction) -> InputResult<()> { debug!("\x1b[93mraw(keycode: {keycode:?}, direction: {direction:?})\x1b[0m"); + #[cfg(feature = "libei")] + if let Some(con) = self.libei.as_mut() { + trace!("try entering the keycode via libei"); + con.raw(keycode, direction)?; + debug!("entered the keycode via libei"); + } #[cfg(feature = "wayland")] if let Some(con) = self.wayland.as_mut() { trace!("try entering the keycode via wayland"); diff --git a/src/linux/wayland.rs b/src/linux/wayland.rs index 65f79e58..322e8fb5 100644 --- a/src/linux/wayland.rs +++ b/src/linux/wayland.rs @@ -272,10 +272,10 @@ impl Con { fn raw(&mut self, keycode: Keycode, direction: Direction) -> InputResult<()> { // Apply the new keymap if there were any changes self.apply_keymap()?; - self.send_key_event(keycode.into(), direction)?; + self.send_key_event(keycode, direction)?; // Let the keymap know that the key was held/no longer held // This is important to avoid unmapping held keys - self.keymap.key(keycode.into(), direction); + self.keymap.key(keycode, direction); Ok(()) }