From 3aea65de729361de70d9ba64b0699832beb5abee Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 28 Jul 2021 22:37:46 +0200 Subject: [PATCH] feat(core): implement KeyStateProvider for wayland injector --- .../src/cli/worker/engine/funnel/key_state.rs | 132 ++++++++++++++++++ espanso/src/cli/worker/engine/funnel/mod.rs | 119 ++++++++++------ espanso/src/cli/worker/engine/mod.rs | 5 +- 3 files changed, 212 insertions(+), 44 deletions(-) create mode 100644 espanso/src/cli/worker/engine/funnel/key_state.rs diff --git a/espanso/src/cli/worker/engine/funnel/key_state.rs b/espanso/src/cli/worker/engine/funnel/key_state.rs new file mode 100644 index 0000000..f352219 --- /dev/null +++ b/espanso/src/cli/worker/engine/funnel/key_state.rs @@ -0,0 +1,132 @@ +/* + * 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 . + */ + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; + +use espanso_inject::KeyboardStateProvider; +use log::warn; + +/// This duration represents the maximum length for which a pressed key +/// event is considered valid. This is useful when the "release" event is +/// lost for whatever reason, so that espanso becomes eventually consistent +/// after a while. +const KEY_PRESS_EVENT_INVALIDATION_TIMEOUT: Duration = Duration::from_secs(3); + +#[derive(Clone)] +pub struct KeyStateStore { + state: Arc>, +} + +impl KeyStateStore { + pub fn new() -> Self { + Self { + state: Arc::new(Mutex::new(KeyState::default())), + } + } + + pub fn is_key_pressed(&self, key_code: u32) -> bool { + let mut state = self.state.lock().expect("unable to obtain modifier state"); + + if let Some(status) = state.keys.get_mut(&key_code) { + if status.is_outdated() { + warn!( + "detected outdated key records for {:?}, releasing the state", + key_code + ); + status.release(); + } + + status.is_pressed() + } else { + false + } + } + + pub fn update_state(&self, key_code: u32, is_pressed: bool) { + let mut state = self.state.lock().expect("unable to obtain key state"); + if let Some(status) = state.keys.get_mut(&key_code) { + if is_pressed { + status.press(); + } else { + status.release(); + } + } else { + state.keys.insert(key_code, KeyStatus::new(is_pressed)); + } + } +} + +struct KeyState { + keys: HashMap, +} + +impl Default for KeyState { + fn default() -> Self { + Self { + keys: HashMap::new(), + } + } +} + +struct KeyStatus { + pressed_at: Option, +} + +impl KeyStatus { + fn new(is_pressed: bool) -> Self { + Self { + pressed_at: if is_pressed { + Some(Instant::now()) + } else { + None + }, + } + } + + fn is_pressed(&self) -> bool { + self.pressed_at.is_some() + } + + fn is_outdated(&self) -> bool { + let now = Instant::now(); + if let Some(pressed_at) = self.pressed_at { + now.duration_since(pressed_at) > KEY_PRESS_EVENT_INVALIDATION_TIMEOUT + } else { + false + } + } + + fn release(&mut self) { + self.pressed_at = None + } + + fn press(&mut self) { + self.pressed_at = Some(Instant::now()); + } +} + +impl KeyboardStateProvider for KeyStateStore { + fn is_key_pressed(&self, code: u32) -> bool { + self.is_key_pressed(code) + } +} diff --git a/espanso/src/cli/worker/engine/funnel/mod.rs b/espanso/src/cli/worker/engine/funnel/mod.rs index 2276a30..98a92a4 100644 --- a/espanso/src/cli/worker/engine/funnel/mod.rs +++ b/espanso/src/cli/worker/engine/funnel/mod.rs @@ -18,69 +18,99 @@ */ use anyhow::Result; -use espanso_detect::{SourceCreationOptions, event::{InputEvent, KeyboardEvent, Status}}; -use log::{error}; +use espanso_detect::{ + event::{InputEvent, KeyboardEvent, Status}, + SourceCreationOptions, +}; +use log::error; use thiserror::Error; use detect::DetectSource; -use self::{modifier::{Modifier, ModifierStateStore}, sequencer::Sequencer}; +use self::{ + key_state::KeyStateStore, + modifier::{Modifier, ModifierStateStore}, + sequencer::Sequencer, +}; pub mod detect; pub mod exit; +pub mod key_state; pub mod modifier; pub mod secure_input; pub mod sequencer; pub mod ui; -pub fn init_and_spawn(source_options: SourceCreationOptions) -> Result<(DetectSource, ModifierStateStore, Sequencer)> { +pub fn init_and_spawn( + source_options: SourceCreationOptions, +) -> Result<( + DetectSource, + ModifierStateStore, + Sequencer, + Option, +)> { let (sender, receiver) = crossbeam::channel::unbounded(); let (init_tx, init_rx) = crossbeam::channel::unbounded(); let modifier_state_store = ModifierStateStore::new(); + let key_state_store = if source_options.use_evdev { + Some(KeyStateStore::new()) + } else { + None + }; let sequencer = Sequencer::new(); - let state_store_clone = modifier_state_store.clone(); + let modifier_state_store_clone = modifier_state_store.clone(); let sequencer_clone = sequencer.clone(); + let key_state_store_clone = key_state_store.clone(); if let Err(error) = std::thread::Builder::new() .name("detect thread".to_string()) - .spawn( - move || match espanso_detect::get_source(source_options) { - Ok(mut source) => { - if source.initialize().is_err() { - init_tx - .send(false) - .expect("unable to send to the init_tx channel"); - } else { - init_tx - .send(true) - .expect("unable to send to the init_tx channel"); - - source - .eventloop(Box::new(move |event| { - // Update the modifiers state - if let Some((modifier, is_pressed)) = get_modifier_status(&event) { - state_store_clone.update_state(modifier, is_pressed); - } - - // Generate a monotonically increasing id for the current event - let source_id = sequencer_clone.next_id(); - - sender - .send((event, source_id)) - .expect("unable to send to the source channel"); - })) - .expect("detect eventloop crashed"); - } - } - Err(error) => { - error!("cannot initialize event source: {:?}", error); + .spawn(move || match espanso_detect::get_source(source_options) { + Ok(mut source) => { + if source.initialize().is_err() { init_tx .send(false) .expect("unable to send to the init_tx channel"); + } else { + init_tx + .send(true) + .expect("unable to send to the init_tx channel"); + + source + .eventloop(Box::new(move |event| { + println!("key -> {:?}", event); + // Update the modifiers state + if let Some((modifier, is_pressed)) = get_modifier_status(&event) { + modifier_state_store_clone.update_state(modifier, is_pressed); + } + + // Update the key state (if needed) + if let Some(key_state_store) = &key_state_store_clone { + if let InputEvent::Keyboard(keyboard_event) = &event { + key_state_store.update_state( + keyboard_event.code, + keyboard_event.status == Status::Pressed, + ); + } + } + + // Generate a monotonically increasing id for the current event + let source_id = sequencer_clone.next_id(); + + sender + .send((event, source_id)) + .expect("unable to send to the source channel"); + })) + .expect("detect eventloop crashed"); } - }, - ) + } + Err(error) => { + error!("cannot initialize event source: {:?}", error); + init_tx + .send(false) + .expect("unable to send to the init_tx channel"); + } + }) { error!("detection thread initialization failed: {:?}", error); return Err(DetectSourceError::ThreadInitFailed.into()); @@ -94,7 +124,12 @@ pub fn init_and_spawn(source_options: SourceCreationOptions) -> Result<(DetectSo return Err(DetectSourceError::InitFailed.into()); } - Ok((DetectSource { receiver }, modifier_state_store, sequencer)) + Ok(( + DetectSource { receiver }, + modifier_state_store, + sequencer, + key_state_store, + )) } #[derive(Error, Debug)] @@ -118,10 +153,10 @@ fn get_modifier_status(event: &InputEvent) -> Option<(Modifier, bool)> { let is_pressed = *status == Status::Pressed; match key { espanso_detect::event::Key::Alt => Some((Modifier::Alt, is_pressed)), - espanso_detect::event::Key::Control => Some((Modifier::Ctrl, is_pressed)), - espanso_detect::event::Key::Meta => Some((Modifier::Meta, is_pressed)), + espanso_detect::event::Key::Control => Some((Modifier::Ctrl, is_pressed)), + espanso_detect::event::Key::Meta => Some((Modifier::Meta, is_pressed)), espanso_detect::event::Key::Shift => Some((Modifier::Shift, is_pressed)), - _ => None + _ => None, } } _ => None, diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index a881a46..51b14dc 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -23,7 +23,7 @@ use anyhow::Result; use crossbeam::channel::Receiver; use espanso_config::{config::ConfigStore, matches::store::MatchStore}; use espanso_detect::SourceCreationOptions; -use espanso_inject::InjectorCreationOptions; +use espanso_inject::{InjectorCreationOptions, KeyboardStateProvider}; use espanso_path::Paths; use espanso_ui::{event::UIEvent, UIRemote}; use log::{debug, error, info, warn}; @@ -92,7 +92,7 @@ pub fn initialize_and_spawn( let has_granted_capabilities = grant_linux_capabilities(use_evdev_backend); // TODO: pass all the options - let (detect_source, modifier_state_store, sequencer) = + let (detect_source, modifier_state_store, sequencer, key_state_store) = super::engine::funnel::init_and_spawn(SourceCreationOptions { use_evdev: use_evdev_backend, ..Default::default() @@ -134,6 +134,7 @@ pub fn initialize_and_spawn( let injector = espanso_inject::get_injector(InjectorCreationOptions { use_evdev: use_evdev_backend, + keyboard_state_provider: key_state_store.map(|store| Box::new(store) as Box), ..Default::default() }) .expect("failed to initialize injector module"); // TODO: handle the options