feat(detect): implement modifier state synchronization on Wayland

This commit is contained in:
Federico Terzi 2021-07-30 19:31:33 +02:00
parent 08e86c7e35
commit 33b9012802
6 changed files with 563 additions and 3 deletions

197
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<String, u32>) {
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 {

View File

@ -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<Device>,
hotkeys: Vec<HotKey>,
@ -65,11 +80,20 @@ pub struct EVDEVSource {
_context: LazyCell<Context>,
_keymap: LazyCell<Keymap>,
_hotkey_filter: RefCell<HotKeyFilter>,
_modifiers_map: HashMap<String, u32>,
}
#[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()

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#[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<Option<ModifiersState>> {
// Fallback for non-wayland systems
Ok(None)
}

View File

@ -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<Option<super::ModifiersState>> {
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<WEvent>` 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::<Option<WEvent>>::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::<FallbackFrame, _>(surface, None, dimensions, move |evt, mut dispatch_data| {
let next_action = dispatch_data.get::<Option<WEvent>>().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<RefCell<Option<super::ModifiersState>>>,
) {
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(())
}