diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index cb77ab1..f80d254 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -53,8 +53,8 @@ pub fn initialize_and_spawn( let modulo_manager = ui::modulo::ModuloManager::new(); - let detect_source = super::engine::source::detect::init_and_spawn() - .expect("failed to initialize detector module"); + let (detect_source, modifier_state_store) = + super::engine::source::init_and_spawn().expect("failed to initialize detector module"); let sources: Vec<&dyn crate::engine::funnel::Source> = vec![&detect_source]; let funnel = crate::engine::funnel::default(&sources); @@ -85,7 +85,8 @@ pub fn initialize_and_spawn( &paths.packages, ); let shell_extension = espanso_render::extension::shell::ShellExtension::new(&paths.config); - let form_adapter = ui::modulo::form::ModuloFormProviderAdapter::new(&modulo_manager, icon_paths.form_icon); + let form_adapter = + ui::modulo::form::ModuloFormProviderAdapter::new(&modulo_manager, icon_paths.form_icon); let form_extension = espanso_render::extension::form::FormExtension::new(&form_adapter); let renderer = espanso_render::create(vec![ &clipboard_extension, @@ -106,6 +107,7 @@ pub fn initialize_and_spawn( &multiplexer, &renderer_adapter, &match_cache, + &modifier_state_store, ); let event_injector = diff --git a/espanso/src/cli/worker/engine/source/detect.rs b/espanso/src/cli/worker/engine/source/detect.rs index ce056e9..7d931d5 100644 --- a/espanso/src/cli/worker/engine/source/detect.rs +++ b/espanso/src/cli/worker/engine/source/detect.rs @@ -17,22 +17,19 @@ * along with espanso. If not, see . */ -use anyhow::Result; use crossbeam::channel::{Receiver, Select, SelectedOperation}; -use espanso_detect::{event::InputEvent, Source}; -use log::{error, trace}; +use espanso_detect::{event::InputEvent}; use crate::engine::{ event::{ input::{Key, KeyboardEvent, MouseButton, MouseEvent, Status, Variant}, Event, }, - funnel, process, + funnel }; -use thiserror::Error; pub struct DetectSource { - receiver: Receiver, + pub receiver: Receiver, } impl<'a> funnel::Source<'a> for DetectSource { @@ -60,67 +57,6 @@ impl<'a> funnel::Source<'a> for DetectSource { } } -// TODO: pass options -pub fn init_and_spawn() -> Result { - let (sender, receiver) = crossbeam::channel::unbounded(); - let (init_tx, init_rx) = crossbeam::channel::unbounded(); - - if let Err(error) = std::thread::Builder::new() - .name("detect thread".to_string()) - .spawn( - move || match espanso_detect::get_source(Default::default()) { - 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| { - sender - .send(event) - .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()); - } - - // Wait for the initialization status - let has_initialized = init_rx - .recv() - .expect("unable to receive from the init_rx channel"); - if !has_initialized { - return Err(DetectSourceError::InitFailed.into()); - } - - Ok(DetectSource { receiver }) -} - -#[derive(Error, Debug)] -pub enum DetectSourceError { - #[error("detection thread initialization failed")] - ThreadInitFailed, - - #[error("detection source initialization failed")] - InitFailed, -} - impl From for Key { fn from(key: espanso_detect::event::Key) -> Self { match key { diff --git a/espanso/src/cli/worker/engine/source/mod.rs b/espanso/src/cli/worker/engine/source/mod.rs index e5f4092..788e985 100644 --- a/espanso/src/cli/worker/engine/source/mod.rs +++ b/espanso/src/cli/worker/engine/source/mod.rs @@ -17,4 +17,105 @@ * along with espanso. If not, see . */ -pub mod detect; \ No newline at end of file +use anyhow::Result; +use espanso_detect::event::{InputEvent, KeyboardEvent, Status}; +use log::{error}; +use thiserror::Error; + +use detect::DetectSource; + +use self::modifier::{Modifier, ModifierStateStore}; + +pub mod detect; +pub mod modifier; + +// TODO: pass options +pub fn init_and_spawn() -> Result<(DetectSource, ModifierStateStore)> { + let (sender, receiver) = crossbeam::channel::unbounded(); + let (init_tx, init_rx) = crossbeam::channel::unbounded(); + + let modifier_state_store = ModifierStateStore::new(); + + let state_store_clone = modifier_state_store.clone(); + if let Err(error) = std::thread::Builder::new() + .name("detect thread".to_string()) + .spawn( + move || match espanso_detect::get_source(Default::default()) { + 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); + } + + sender + .send(event) + .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()); + } + + // Wait for the initialization status + let has_initialized = init_rx + .recv() + .expect("unable to receive from the init_rx channel"); + if !has_initialized { + return Err(DetectSourceError::InitFailed.into()); + } + + Ok((DetectSource { receiver }, modifier_state_store)) +} + +#[derive(Error, Debug)] +pub enum DetectSourceError { + #[error("detection thread initialization failed")] + ThreadInitFailed, + + #[error("detection source initialization failed")] + InitFailed, +} + +fn get_modifier_status(event: &InputEvent) -> Option<(Modifier, bool)> { + match event { + InputEvent::Keyboard(KeyboardEvent { + key, + status, + value: _, + variant: _, + code: _, + }) => { + 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::Shift => Some((Modifier::Shift, is_pressed)), + _ => None + } + } + _ => None, + } +} diff --git a/espanso/src/cli/worker/engine/source/modifier.rs b/espanso/src/cli/worker/engine/source/modifier.rs new file mode 100644 index 0000000..02e40b4 --- /dev/null +++ b/espanso/src/cli/worker/engine/source/modifier.rs @@ -0,0 +1,134 @@ +/* + * 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::{ + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; + +use log::warn; + +use crate::engine::process::ModifierStatusProvider; + +// TODO: explain +const MAXIMUM_MODIFIERS_PRESS_TIME_RECORD: Duration = Duration::from_secs(30); + +#[derive(Debug, Hash, PartialEq, Eq)] +pub enum Modifier { + Ctrl, + Shift, + Alt, + Meta, +} + +#[derive(Clone)] +pub struct ModifierStateStore { + state: Arc>, +} + +impl ModifierStateStore { + pub fn new() -> Self { + Self { + state: Arc::new(Mutex::new(ModifiersState::default())), + } + } + + pub fn is_any_modifier_pressed(&self) -> bool { + let mut state = self.state.lock().expect("unable to obtain modifier state"); + let mut is_any_modifier_pressed = false; + for (modifier, status) in &mut state.modifiers { + if status.is_outdated() { + warn!( + "detected outdated modifier records for {:?}, releasing the state", + modifier + ); + status.release(); + } + + if status.is_pressed() { + is_any_modifier_pressed = true; + } + } + is_any_modifier_pressed + } + + pub fn update_state(&self, modifier: Modifier, is_pressed: bool) { + let mut state = self.state.lock().expect("unable to obtain modifier state"); + for (curr_modifier, status) in &mut state.modifiers { + if curr_modifier == &modifier { + if is_pressed { + status.press(); + } else { + status.release(); + } + break; + } + } + } +} + +struct ModifiersState { + modifiers: Vec<(Modifier, ModifierStatus)>, +} + +impl Default for ModifiersState { + fn default() -> Self { + Self { + modifiers: vec![ + (Modifier::Ctrl, ModifierStatus { pressed_at: None }), + (Modifier::Alt, ModifierStatus { pressed_at: None }), + (Modifier::Shift, ModifierStatus { pressed_at: None }), + (Modifier::Meta, ModifierStatus { pressed_at: None }), + ], + } + } +} + +struct ModifierStatus { + pressed_at: Option, +} + +impl ModifierStatus { + 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) > MAXIMUM_MODIFIERS_PRESS_TIME_RECORD + } else { + false + } + } + + fn release(&mut self) { + self.pressed_at = None + } + + fn press(&mut self) { + self.pressed_at = Some(Instant::now()); + } +} + +impl ModifierStatusProvider for ModifierStateStore { + fn is_any_modifier_pressed(&self) -> bool { + self.is_any_modifier_pressed() + } +} diff --git a/espanso/src/engine/process/default.rs b/espanso/src/engine/process/default.rs index 52bf18a..2bfbf3e 100644 --- a/espanso/src/engine/process/default.rs +++ b/espanso/src/engine/process/default.rs @@ -22,6 +22,7 @@ use log::trace; use super::{Event, MatchFilter, MatchInfoProvider, MatchSelector, Matcher, Middleware, Multiplexer, Processor, Renderer, middleware::{ match_select::MatchSelectMiddleware, matcher::MatcherMiddleware, multiplex::MultiplexMiddleware, render::RenderMiddleware, action::ActionMiddleware, cursor_hint::CursorHintMiddleware, cause::CauseCompensateMiddleware, + delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider}, }}; use std::collections::VecDeque; @@ -38,6 +39,7 @@ impl<'a> DefaultProcessor<'a> { multiplexer: &'a dyn Multiplexer, renderer: &'a dyn Renderer<'a>, match_info_provider: &'a dyn MatchInfoProvider, + modifier_status_provider: &'a dyn ModifierStatusProvider, ) -> DefaultProcessor<'a> { Self { event_queue: VecDeque::new(), @@ -49,6 +51,7 @@ impl<'a> DefaultProcessor<'a> { Box::new(RenderMiddleware::new(renderer)), Box::new(CursorHintMiddleware::new()), Box::new(ActionMiddleware::new(match_info_provider)), + Box::new(DelayForModifierReleaseMiddleware::new(modifier_status_provider)), ], } } @@ -63,6 +66,7 @@ impl<'a> DefaultProcessor<'a> { current_queue.push_front(event); }; + trace!("--------------- new event -----------------"); for middleware in self.middleware.iter() { trace!("middleware '{}' received event: {:?}", middleware.name(), current_event); diff --git a/espanso/src/engine/process/middleware/delay_modifiers.rs b/espanso/src/engine/process/middleware/delay_modifiers.rs new file mode 100644 index 0000000..f6977a4 --- /dev/null +++ b/espanso/src/engine/process/middleware/delay_modifiers.rs @@ -0,0 +1,84 @@ +/* + * 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 . + */ + +// TODO: explain why this is needed + +use std::{ + time::{Duration, Instant}, +}; + +use log::{trace, warn}; + +use super::super::Middleware; +use crate::engine::event::{ + Event, +}; + +// TODO: pass through config +const MODIFIER_DELAY_TIMEOUT: Duration = Duration::from_secs(3); + +pub trait ModifierStatusProvider { + fn is_any_modifier_pressed(&self) -> bool; +} + +pub struct DelayForModifierReleaseMiddleware<'a> { + provider: &'a dyn ModifierStatusProvider, +} + +impl <'a> DelayForModifierReleaseMiddleware<'a> { + pub fn new(provider: &'a dyn ModifierStatusProvider) -> Self { + Self { + provider + } + } +} + +impl <'a> Middleware for DelayForModifierReleaseMiddleware<'a> { + fn name(&self) -> &'static str { + "delay_modifiers" + } + + fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event { + if is_injection_event(&event) { + let start = Instant::now(); + while self.provider.is_any_modifier_pressed() { + if Instant::now().duration_since(start) > MODIFIER_DELAY_TIMEOUT { + warn!("injection delay has timed out, please release the modifier keys (SHIFT, CTRL, ALT, CMD) to trigger an expansion"); + break; + } + trace!("delaying injection event as some modifiers are pressed"); + std::thread::sleep(Duration::from_millis(100)); + } + } + + event + } +} + +fn is_injection_event(event: &Event) -> bool { + match event { + Event::TriggerCompensation(_) => true, + Event::CursorHintCompensation(_) => true, + Event::KeySequenceInject(_) => true, + Event::TextInject(_) => true, + _ => false, + } +} + +// TODO: test diff --git a/espanso/src/engine/process/middleware/mod.rs b/espanso/src/engine/process/middleware/mod.rs index 91d530f..ff26462 100644 --- a/espanso/src/engine/process/middleware/mod.rs +++ b/espanso/src/engine/process/middleware/mod.rs @@ -20,6 +20,7 @@ pub mod action; pub mod cause; pub mod cursor_hint; +pub mod delay_modifiers; pub mod match_select; pub mod matcher; pub mod multiplex; diff --git a/espanso/src/engine/process/mod.rs b/espanso/src/engine/process/mod.rs index 51b4370..e88c123 100644 --- a/espanso/src/engine/process/mod.rs +++ b/espanso/src/engine/process/mod.rs @@ -93,6 +93,7 @@ pub enum RendererError { } pub use middleware::action::MatchInfoProvider; +pub use middleware::delay_modifiers::ModifierStatusProvider; pub fn default<'a, MatcherState>( matchers: &'a [&'a dyn Matcher<'a, MatcherState>], @@ -101,6 +102,7 @@ pub fn default<'a, MatcherState>( multiplexer: &'a dyn Multiplexer, renderer: &'a dyn Renderer<'a>, match_info_provider: &'a dyn MatchInfoProvider, + modifier_status_provider: &'a dyn ModifierStatusProvider, ) -> impl Processor + 'a { default::DefaultProcessor::new( matchers, @@ -109,5 +111,6 @@ pub fn default<'a, MatcherState>( multiplexer, renderer, match_info_provider, + modifier_status_provider, ) }