feat(core): implement delay-for-modifier-release mechanism
This commit is contained in:
parent
3dfde8e830
commit
7db39f4c74
|
@ -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 =
|
||||
|
|
|
@ -17,22 +17,19 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<InputEvent>,
|
||||
pub receiver: Receiver<InputEvent>,
|
||||
}
|
||||
|
||||
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 {
|
||||
fn from(key: espanso_detect::event::Key) -> Self {
|
||||
match key {
|
||||
|
|
|
@ -17,4 +17,105 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod detect;
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
134
espanso/src/cli/worker/engine/source/modifier.rs
Normal file
134
espanso/src/cli/worker/engine/source/modifier.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
84
espanso/src/engine/process/middleware/delay_modifiers.rs
Normal file
84
espanso/src/engine/process/middleware/delay_modifiers.rs
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user