diff --git a/Cargo.lock b/Cargo.lock index 95e63e1..f190e6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "calloop" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0a1340115d6bd81e1066469091596a339f68878a2ce3c2f39e546607d22131" +dependencies = [ + "log", + "nix 0.19.1", +] + [[package]] name = "caps" version = "0.5.2" @@ -418,12 +428,27 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading", +] + [[package]] name = "downcast" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d" +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "dtoa" version = "0.4.7" @@ -580,6 +605,7 @@ dependencies = [ "log", "regex", "scopeguard", + "smithay-client-toolkit", "thiserror", "widestring", ] @@ -1031,6 +1057,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + [[package]] name = "linked-hash-map" version = "0.5.4" @@ -1131,6 +1167,15 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "memmap2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.1" @@ -1246,6 +1291,40 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nix" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" +dependencies = [ + "bitflags 1.2.1", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags 1.2.1", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1329,6 +1408,12 @@ dependencies = [ "objc", ] +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "opener" version = "0.5.0" @@ -1709,6 +1794,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1781,6 +1872,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "smithay-client-toolkit" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec783683499a2cfc85b6df3d04f83b1907b5cbd98a1aed44667dbdf1eac4e64c" +dependencies = [ + "bitflags 1.2.1", + "calloop", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix 0.20.0", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + [[package]] name = "squote" version = "0.1.2" @@ -2091,6 +2206,79 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wayland-client" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ab332350e502f159382201394a78e3cc12d0f04db863429260164ea40e0355" +dependencies = [ + "bitflags 1.2.1", + "downcast-rs", + "libc", + "nix 0.20.0", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21817947c7011bbd0a27e11b17b337bfd022e8544b071a2641232047966fbda" +dependencies = [ + "nix 0.20.0", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be610084edd1586d45e7bdd275fe345c7c1873598caa464c4fb835dee70fa65a" +dependencies = [ + "nix 0.20.0", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "286620ea4d803bacf61fa087a4242ee316693099ee5a140796aaba02b29f861f" +dependencies = [ + "bitflags 1.2.1", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce923eb2deb61de332d1f356ec7b6bf37094dc5573952e1c8936db03b54c03f1" +dependencies = [ + "proc-macro2", + "quote 1.0.9", + "xml-rs 0.8.3", +] + +[[package]] +name = "wayland-sys" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d841fca9aed7febf9bed2e9796c49bf58d4152ceda8ac949ebe00868d8f0feb8" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + [[package]] name = "widestring" version = "0.4.3" @@ -2272,6 +2460,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "xcursor" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9a231574ae78801646617cefd13bfe94be907c0e4fa979cfd8b770aa3c5d08" +dependencies = [ + "nom", +] + [[package]] name = "xml-rs" version = "0.6.1" diff --git a/espanso-detect/Cargo.toml b/espanso-detect/Cargo.toml index b043be7..9f510a1 100644 --- a/espanso-detect/Cargo.toml +++ b/espanso-detect/Cargo.toml @@ -6,9 +6,11 @@ edition = "2018" build="build.rs" [features] +# TODO: REMOVE!!! +default = ["wayland"] # If the wayland feature is enabled, all X11 dependencies will be dropped # and only EVDEV-based methods will be supported. -wayland = [] +wayland = ["sctk"] [dependencies] log = "0.4.14" @@ -24,6 +26,7 @@ widestring = "0.4.3" [target.'cfg(target_os="linux")'.dependencies] libc = "0.2.85" scopeguard = "1.1.0" +sctk = { package = "smithay-client-toolkit", version = "0.14.0", optional = true } [build-dependencies] cc = "1.0.66" diff --git a/espanso-detect/src/evdev/device.rs b/espanso-detect/src/evdev/device.rs index 1879a1d..18a09ce 100644 --- a/espanso-detect/src/evdev/device.rs +++ b/espanso-detect/src/evdev/device.rs @@ -5,6 +5,7 @@ use anyhow::Result; use libc::{input_event, size_t, ssize_t, EWOULDBLOCK, O_CLOEXEC, O_NONBLOCK, O_RDONLY}; use log::trace; use scopeguard::ScopeGuard; +use std::collections::HashMap; use std::os::unix::io::AsRawFd; use std::{ ffi::{c_void, CStr}, @@ -13,6 +14,7 @@ use std::{ use std::{fs::File, os::unix::fs::OpenOptionsExt}; use thiserror::Error; +use super::sync::ModifiersState; use super::{ ffi::{ is_keyboard_or_mouse, xkb_key_direction, xkb_keycode_t, xkb_keymap_key_repeats, xkb_state, @@ -185,6 +187,38 @@ impl Device { Some(RawInputEvent::Keyboard(event)) } + + pub fn update_key(&mut self, code: u32, pressed: bool) { + let direction = if pressed { + super::ffi::xkb_key_direction::DOWN + } else { + super::ffi::xkb_key_direction::UP + }; + unsafe { + xkb_state_update_key(self.get_state(), code, direction); + } + } + + pub fn update_modifier_state(&mut self, modifiers_state: &ModifiersState, modifiers_map: &HashMap) { + if modifiers_state.alt { + self.update_key(*modifiers_map.get("alt").expect("unable to find modifiers key in map"), true); + } + if modifiers_state.ctrl { + self.update_key(*modifiers_map.get("ctrl").expect("unable to find modifiers key in map"), true); + } + if modifiers_state.meta { + self.update_key(*modifiers_map.get("meta").expect("unable to find modifiers key in map"), true); + } + if modifiers_state.num_lock { + self.update_key(*modifiers_map.get("num_lock").expect("unable to find modifiers key in map"), true); + } + if modifiers_state.shift { + self.update_key(*modifiers_map.get("shift").expect("unable to find modifiers key in map"), true); + } + if modifiers_state.caps_lock { + self.update_key(*modifiers_map.get("caps_lock").expect("unable to find modifiers key in map"), true); + } + } } impl Drop for Device { diff --git a/espanso-detect/src/evdev/mod.rs b/espanso-detect/src/evdev/mod.rs index 6b26f67..6e4548a 100644 --- a/espanso-detect/src/evdev/mod.rs +++ b/espanso-detect/src/evdev/mod.rs @@ -26,8 +26,10 @@ mod ffi; mod hotkey; mod keymap; mod state; +mod sync; use std::cell::RefCell; +use std::collections::HashMap; use anyhow::Result; use context::Context; @@ -37,7 +39,7 @@ use lazycell::LazyCell; use libc::{ __errno_location, close, epoll_ctl, epoll_event, epoll_wait, EINTR, EPOLLIN, EPOLL_CTL_ADD, }; -use log::{error, trace}; +use log::{debug, error, info, trace}; use thiserror::Error; use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; @@ -57,6 +59,19 @@ const BTN_MIDDLE: u16 = 0x112; const BTN_SIDE: u16 = 0x113; const BTN_EXTRA: u16 = 0x114; +// Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB +// keycode set (where ESC is 9). +const EVDEV_OFFSET: u32 = 8; + +// List of modifier keycodes, as defined in the "input-event-codes.h" header +// TODO: create an option to override them if needed +const KEY_CTRL: u32 = 29; +const KEY_SHIFT: u32 = 42; +const KEY_ALT: u32 = 56; +const KEY_META: u32 = 125; +const KEY_CAPSLOCK: u32 = 58; +const KEY_NUMLOCK: u32 = 69; + pub struct EVDEVSource { devices: Vec, hotkeys: Vec, @@ -65,11 +80,20 @@ pub struct EVDEVSource { _context: LazyCell, _keymap: LazyCell, _hotkey_filter: RefCell, + _modifiers_map: HashMap, } #[allow(clippy::new_without_default)] impl EVDEVSource { pub fn new(options: SourceCreationOptions) -> EVDEVSource { + let mut modifiers_map = HashMap::new(); + modifiers_map.insert("ctrl".to_string(), KEY_CTRL + EVDEV_OFFSET); + modifiers_map.insert("shift".to_string(), KEY_SHIFT + EVDEV_OFFSET); + modifiers_map.insert("alt".to_string(), KEY_ALT + EVDEV_OFFSET); + modifiers_map.insert("meta".to_string(), KEY_META + EVDEV_OFFSET); + modifiers_map.insert("caps_lock".to_string(), KEY_CAPSLOCK + EVDEV_OFFSET); + modifiers_map.insert("num_lock".to_string(), KEY_NUMLOCK + EVDEV_OFFSET); + Self { devices: Vec::new(), hotkeys: options.hotkeys, @@ -77,6 +101,7 @@ impl EVDEVSource { _keymap: LazyCell::new(), _keyboard_rmlvo: options.evdev_keyboard_rmlvo, _hotkey_filter: RefCell::new(HotKeyFilter::new()), + _modifiers_map: modifiers_map, } } } @@ -103,8 +128,18 @@ impl Source for EVDEVSource { } } - // Initialize the hotkeys let state = State::new(&keymap)?; + + info!("Querying modifier status..."); + if let Some(modifiers_state) = sync::get_modifiers_state()? { + debug!("Updating device modifier state: {:?}", modifiers_state); + + for device in &mut self.devices { + device.update_modifier_state(&modifiers_state, &self._modifiers_map); + } + } + + // Initialize the hotkeys self ._hotkey_filter .borrow_mut() diff --git a/espanso-detect/src/evdev/sync/mod.rs b/espanso-detect/src/evdev/sync/mod.rs new file mode 100644 index 0000000..d42c9fc --- /dev/null +++ b/espanso-detect/src/evdev/sync/mod.rs @@ -0,0 +1,39 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#[derive(Debug, Clone, Copy)] +pub struct ModifiersState { + pub ctrl: bool, + pub alt: bool, + pub shift: bool, + pub caps_lock: bool, + pub meta: bool, + pub num_lock: bool, +} + +#[cfg(feature = "wayland")] +mod wayland; +#[cfg(feature = "wayland")] +pub use wayland::get_modifiers_state; + +#[cfg(not(feature = "wayland"))] +pub fn get_modifiers_state() -> anyhow::Result> { + // Fallback for non-wayland systems + Ok(None) +} \ No newline at end of file diff --git a/espanso-detect/src/evdev/sync/wayland.rs b/espanso-detect/src/evdev/sync/wayland.rs new file mode 100644 index 0000000..ed2f8d8 --- /dev/null +++ b/espanso-detect/src/evdev/sync/wayland.rs @@ -0,0 +1,252 @@ +// This module was implemented starting from this wonderful example: +// https://github.com/Smithay/client-toolkit/blob/master/examples/kbd_input.rs + +use std::cell::RefCell; +use std::cmp::min; +use std::rc::Rc; + +use anyhow::{Context, Result}; +use log::error; +use sctk::reexports::calloop; +use sctk::reexports::client::protocol::{wl_keyboard, wl_shm, wl_surface}; +use sctk::seat::keyboard::{map_keyboard_repeat, Event as KbEvent, RepeatKind}; +use sctk::shm::AutoMemPool; +use sctk::window::{Event as WEvent, FallbackFrame}; + +sctk::default_environment!(EspansoModifiersSync, desktop); + +pub fn get_modifiers_state() -> Result> { + let (env, display, queue) = sctk::new_default_environment!(EspansoModifiersSync, desktop) + .context("Unable to connect to a Wayland compositor")?; + + let result = Rc::new(RefCell::new(None)); + + /* + * Prepare a calloop event loop to handle key repetion + */ + // Here `Option` is the type of a global value that will be shared by + // all callbacks invoked by the event loop. + let mut event_loop = calloop::EventLoop::>::try_new().unwrap(); + + /* + * Create a buffer with window contents + */ + + let mut dimensions = (1u32, 1u32); + + /* + * Init wayland objects + */ + + let surface = env.create_surface().detach(); + + let mut window = env + .create_window::(surface, None, dimensions, move |evt, mut dispatch_data| { + let next_action = dispatch_data.get::>().unwrap(); + // Keep last event in priority order : Close > Configure > Refresh + let replace = match (&evt, &*next_action) { + (_, &None) + | (_, &Some(WEvent::Refresh)) + | (&WEvent::Configure { .. }, &Some(WEvent::Configure { .. })) + | (&WEvent::Close, _) => true, + _ => false, + }; + if replace { + *next_action = Some(evt); + } + }) + .context("Failed to create a window !")?; + + window.set_title("Espanso Sync Tool".to_string()); + + let mut pool = env + .create_auto_pool() + .context("Failed to create a memory pool !")?; + + /* + * Keyboard initialization + */ + + let mut seats = Vec::<( + String, + Option<(wl_keyboard::WlKeyboard, calloop::RegistrationToken)>, + )>::new(); + + // first process already existing seats + for seat in env.get_all_seats() { + if let Some((has_kbd, name)) = sctk::seat::with_seat_data(&seat, |seat_data| { + ( + seat_data.has_keyboard && !seat_data.defunct, + seat_data.name.clone(), + ) + }) { + if has_kbd { + let result_clone = result.clone(); + match map_keyboard_repeat( + event_loop.handle(), + &seat, + None, + RepeatKind::System, + move |event, _, _| keyboard_event_handler(event, &result_clone), + ) { + Ok((kbd, repeat_source)) => { + seats.push((name, Some((kbd, repeat_source)))); + } + Err(e) => { + error!("Failed to map keyboard on seat {} : {:?}.", name, e); + seats.push((name, None)); + } + } + } else { + seats.push((name, None)); + } + } + } + + // then setup a listener for changes + let loop_handle = event_loop.handle(); + let result_clone = result.clone(); + let _seat_listener = env.listen_for_seats(move |seat, seat_data, _| { + let result_clone = result_clone.clone(); + // find the seat in the vec of seats, or insert it if it is unknown + let idx = seats.iter().position(|(name, _)| name == &seat_data.name); + let idx = idx.unwrap_or_else(|| { + seats.push((seat_data.name.clone(), None)); + seats.len() - 1 + }); + + let (_, ref mut opt_kbd) = &mut seats[idx]; + // we should map a keyboard if the seat has the capability & is not defunct + if seat_data.has_keyboard && !seat_data.defunct { + if opt_kbd.is_none() { + // we should initalize a keyboard + match map_keyboard_repeat( + loop_handle.clone(), + &seat, + None, + RepeatKind::System, + move |event, _, _| keyboard_event_handler(event, &result_clone), + ) { + Ok((kbd, repeat_source)) => { + *opt_kbd = Some((kbd, repeat_source)); + } + Err(e) => { + eprintln!( + "Failed to map keyboard on seat {} : {:?}.", + seat_data.name, e + ) + } + } + } + } else { + if let Some((kbd, source)) = opt_kbd.take() { + // the keyboard has been removed, cleanup + kbd.release(); + loop_handle.remove(source); + } + } + }); + + if !env.get_shell().unwrap().needs_configure() { + // initial draw to bootstrap on wl_shell + redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw"); + window.refresh(); + } + + sctk::WaylandSource::new(queue) + .quick_insert(event_loop.handle()) + .unwrap(); + + let mut next_action = None; + + loop { + match next_action.take() { + Some(WEvent::Close) => break, + Some(WEvent::Refresh) => { + window.refresh(); + window.surface().commit(); + } + Some(WEvent::Configure { + new_size, + states: _, + }) => { + if let Some((w, h)) = new_size { + window.resize(w, h); + dimensions = (w, h) + } + window.refresh(); + redraw(&mut pool, window.surface(), dimensions).expect("Failed to draw"); + } + None => { + let result_clone= result.clone(); + let result_ref = result_clone.borrow(); + + if let Some(result) = &*result_ref { + return Ok(Some(result.clone())); + } + } + } + + // always flush the connection before going to sleep waiting for events + display.flush().unwrap(); + + event_loop + .dispatch( + Some(std::time::Duration::from_millis(10)), + &mut next_action, + ) + .unwrap(); + } + + Ok(None) +} + +fn keyboard_event_handler( + event: KbEvent, + result_clone: &Rc>>, +) { + if let KbEvent::Modifiers { modifiers } = event { + let mut result_mut = (**result_clone).borrow_mut(); + *result_mut = Some(super::ModifiersState { + ctrl: modifiers.ctrl, + alt: modifiers.alt, + shift: modifiers.shift, + caps_lock: modifiers.caps_lock, + meta: modifiers.logo, + num_lock: modifiers.num_lock, + }) + } +} + +fn redraw( + pool: &mut AutoMemPool, + surface: &wl_surface::WlSurface, + (buf_x, buf_y): (u32, u32), +) -> Result<(), ::std::io::Error> { + let (canvas, new_buffer) = pool.buffer( + buf_x as i32, + buf_y as i32, + 4 * buf_x as i32, + wl_shm::Format::Argb8888, + )?; + for (i, dst_pixel) in canvas.chunks_exact_mut(4).enumerate() { + let x = i as u32 % buf_x; + let y = i as u32 / buf_x; + let r: u32 = min(((buf_x - x) * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y); + let g: u32 = min((x * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y); + let b: u32 = min(((buf_x - x) * 0xFF) / buf_x, (y * 0xFF) / buf_y); + let pixel: [u8; 4] = ((0xFF << 24) + (r << 16) + (g << 8) + b).to_ne_bytes(); + dst_pixel[0] = pixel[0]; + dst_pixel[1] = pixel[1]; + dst_pixel[2] = pixel[2]; + dst_pixel[3] = pixel[3]; + } + surface.attach(Some(&new_buffer), 0, 0); + if surface.as_ref().version() >= 4 { + surface.damage_buffer(0, 0, buf_x as i32, buf_y as i32); + } else { + surface.damage(0, 0, buf_x as i32, buf_y as i32); + } + surface.commit(); + Ok(()) +}