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,
)
}