feat(core): implement delay-for-modifier-release mechanism

This commit is contained in:
Federico Terzi 2021-04-25 15:54:08 +02:00
parent 3dfde8e830
commit 7db39f4c74
8 changed files with 336 additions and 71 deletions

View File

@ -53,8 +53,8 @@ pub fn initialize_and_spawn(
let modulo_manager = ui::modulo::ModuloManager::new(); let modulo_manager = ui::modulo::ModuloManager::new();
let detect_source = super::engine::source::detect::init_and_spawn() let (detect_source, modifier_state_store) =
.expect("failed to initialize detector module"); super::engine::source::init_and_spawn().expect("failed to initialize detector module");
let sources: Vec<&dyn crate::engine::funnel::Source> = vec![&detect_source]; let sources: Vec<&dyn crate::engine::funnel::Source> = vec![&detect_source];
let funnel = crate::engine::funnel::default(&sources); let funnel = crate::engine::funnel::default(&sources);
@ -85,7 +85,8 @@ pub fn initialize_and_spawn(
&paths.packages, &paths.packages,
); );
let shell_extension = espanso_render::extension::shell::ShellExtension::new(&paths.config); 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 form_extension = espanso_render::extension::form::FormExtension::new(&form_adapter);
let renderer = espanso_render::create(vec![ let renderer = espanso_render::create(vec![
&clipboard_extension, &clipboard_extension,
@ -106,6 +107,7 @@ pub fn initialize_and_spawn(
&multiplexer, &multiplexer,
&renderer_adapter, &renderer_adapter,
&match_cache, &match_cache,
&modifier_state_store,
); );
let event_injector = let event_injector =

View File

@ -17,22 +17,19 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use anyhow::Result;
use crossbeam::channel::{Receiver, Select, SelectedOperation}; use crossbeam::channel::{Receiver, Select, SelectedOperation};
use espanso_detect::{event::InputEvent, Source}; use espanso_detect::{event::InputEvent};
use log::{error, trace};
use crate::engine::{ use crate::engine::{
event::{ event::{
input::{Key, KeyboardEvent, MouseButton, MouseEvent, Status, Variant}, input::{Key, KeyboardEvent, MouseButton, MouseEvent, Status, Variant},
Event, Event,
}, },
funnel, process, funnel
}; };
use thiserror::Error;
pub struct DetectSource { pub struct DetectSource {
receiver: Receiver<InputEvent>, pub receiver: Receiver<InputEvent>,
} }
impl<'a> funnel::Source<'a> for DetectSource { 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<DetectSource> {
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<espanso_detect::event::Key> for Key { impl From<espanso_detect::event::Key> for Key {
fn from(key: espanso_detect::event::Key) -> Self { fn from(key: espanso_detect::event::Key) -> Self {
match key { match key {

View File

@ -17,4 +17,105 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
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 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,
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Mutex<ModifiersState>>,
}
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<Instant>,
}
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()
}
}

View File

@ -22,6 +22,7 @@ use log::trace;
use super::{Event, MatchFilter, MatchInfoProvider, MatchSelector, Matcher, Middleware, Multiplexer, Processor, Renderer, middleware::{ use super::{Event, MatchFilter, MatchInfoProvider, MatchSelector, Matcher, Middleware, Multiplexer, Processor, Renderer, middleware::{
match_select::MatchSelectMiddleware, matcher::MatcherMiddleware, multiplex::MultiplexMiddleware, match_select::MatchSelectMiddleware, matcher::MatcherMiddleware, multiplex::MultiplexMiddleware,
render::RenderMiddleware, action::ActionMiddleware, cursor_hint::CursorHintMiddleware, cause::CauseCompensateMiddleware, render::RenderMiddleware, action::ActionMiddleware, cursor_hint::CursorHintMiddleware, cause::CauseCompensateMiddleware,
delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider},
}}; }};
use std::collections::VecDeque; use std::collections::VecDeque;
@ -38,6 +39,7 @@ impl<'a> DefaultProcessor<'a> {
multiplexer: &'a dyn Multiplexer, multiplexer: &'a dyn Multiplexer,
renderer: &'a dyn Renderer<'a>, renderer: &'a dyn Renderer<'a>,
match_info_provider: &'a dyn MatchInfoProvider, match_info_provider: &'a dyn MatchInfoProvider,
modifier_status_provider: &'a dyn ModifierStatusProvider,
) -> DefaultProcessor<'a> { ) -> DefaultProcessor<'a> {
Self { Self {
event_queue: VecDeque::new(), event_queue: VecDeque::new(),
@ -49,6 +51,7 @@ impl<'a> DefaultProcessor<'a> {
Box::new(RenderMiddleware::new(renderer)), Box::new(RenderMiddleware::new(renderer)),
Box::new(CursorHintMiddleware::new()), Box::new(CursorHintMiddleware::new()),
Box::new(ActionMiddleware::new(match_info_provider)), 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); current_queue.push_front(event);
}; };
trace!("--------------- new event -----------------");
for middleware in self.middleware.iter() { for middleware in self.middleware.iter() {
trace!("middleware '{}' received event: {:?}", middleware.name(), current_event); trace!("middleware '{}' received event: {:?}", middleware.name(), current_event);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
// 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

View File

@ -20,6 +20,7 @@
pub mod action; pub mod action;
pub mod cause; pub mod cause;
pub mod cursor_hint; pub mod cursor_hint;
pub mod delay_modifiers;
pub mod match_select; pub mod match_select;
pub mod matcher; pub mod matcher;
pub mod multiplex; pub mod multiplex;

View File

@ -93,6 +93,7 @@ pub enum RendererError {
} }
pub use middleware::action::MatchInfoProvider; pub use middleware::action::MatchInfoProvider;
pub use middleware::delay_modifiers::ModifierStatusProvider;
pub fn default<'a, MatcherState>( pub fn default<'a, MatcherState>(
matchers: &'a [&'a dyn Matcher<'a, MatcherState>], matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
@ -101,6 +102,7 @@ pub fn default<'a, MatcherState>(
multiplexer: &'a dyn Multiplexer, multiplexer: &'a dyn Multiplexer,
renderer: &'a dyn Renderer<'a>, renderer: &'a dyn Renderer<'a>,
match_info_provider: &'a dyn MatchInfoProvider, match_info_provider: &'a dyn MatchInfoProvider,
modifier_status_provider: &'a dyn ModifierStatusProvider,
) -> impl Processor + 'a { ) -> impl Processor + 'a {
default::DefaultProcessor::new( default::DefaultProcessor::new(
matchers, matchers,
@ -109,5 +111,6 @@ pub fn default<'a, MatcherState>(
multiplexer, multiplexer,
renderer, renderer,
match_info_provider, match_info_provider,
modifier_status_provider,
) )
} }