espanso/espanso-engine/src/process/middleware/matcher.rs

258 lines
7.3 KiB
Rust

/*
* 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 log::trace;
use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
};
use super::super::Middleware;
use crate::event::{
input::{Key, Status},
internal::{DetectedMatch, MatchesDetectedEvent},
Event, EventType,
};
pub trait Matcher<'a, State> {
fn process(
&'a self,
prev_state: Option<&State>,
event: &MatcherEvent,
) -> (State, Vec<MatchResult>);
}
#[derive(Debug)]
pub enum MatcherEvent {
Key { key: Key, chars: Option<String> },
VirtualSeparator,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatchResult {
pub id: i32,
pub trigger: String,
pub left_separator: Option<String>,
pub right_separator: Option<String>,
pub args: HashMap<String, String>,
}
pub trait MatcherMiddlewareConfigProvider {
fn max_history_size(&self) -> usize;
}
pub trait ModifierStateProvider {
fn get_modifier_state(&self) -> ModifierState;
}
#[derive(Debug, Clone)]
pub struct ModifierState {
pub is_ctrl_down: bool,
pub is_alt_down: bool,
pub is_meta_down: bool,
}
pub struct MatcherMiddleware<'a, State> {
matchers: &'a [&'a dyn Matcher<'a, State>],
matcher_states: RefCell<VecDeque<Vec<State>>>,
max_history_size: usize,
modifier_status_provider: &'a dyn ModifierStateProvider,
}
impl<'a, State> MatcherMiddleware<'a, State> {
pub fn new(
matchers: &'a [&'a dyn Matcher<'a, State>],
options_provider: &'a dyn MatcherMiddlewareConfigProvider,
modifier_status_provider: &'a dyn ModifierStateProvider,
) -> Self {
let max_history_size = options_provider.max_history_size();
Self {
matchers,
matcher_states: RefCell::new(VecDeque::new()),
max_history_size,
modifier_status_provider,
}
}
}
impl<'a, State> Middleware for MatcherMiddleware<'a, State> {
fn name(&self) -> &'static str {
"matcher"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
if is_event_of_interest(&event.etype) {
let mut matcher_states = self.matcher_states.borrow_mut();
let prev_states = if !matcher_states.is_empty() {
matcher_states.get(matcher_states.len() - 1)
} else {
None
};
if let EventType::Keyboard(keyboard_event) = &event.etype {
// Backspace handling
if keyboard_event.key == Key::Backspace {
trace!("popping the last matcher state");
matcher_states.pop_back();
return event;
}
// We need to filter out some keyboard events if they are generated
// while some modifier keys are pressed, otherwise we could have
// wrong matches being detected.
// See: https://github.com/federico-terzi/espanso/issues/725
if should_skip_key_event_due_to_modifier_press(
&self.modifier_status_provider.get_modifier_state(),
) {
trace!("skipping keyboard event because incompatible modifiers are pressed");
return event;
}
}
// Some keys (such as the arrow keys) and mouse clicks prevent espanso from building
// an accurate key buffer, so we need to invalidate it.
if is_invalidating_event(&event.etype) {
trace!("invalidating event detected, clearing matching state");
matcher_states.clear();
return event;
}
let mut all_results = Vec::new();
if let Some(matcher_event) = convert_to_matcher_event(&event.etype) {
let mut new_states = Vec::new();
for (i, matcher) in self.matchers.iter().enumerate() {
let prev_state = prev_states.and_then(|states| states.get(i));
let (state, results) = matcher.process(prev_state, &matcher_event);
all_results.extend(results);
new_states.push(state);
}
matcher_states.push_back(new_states);
if matcher_states.len() > self.max_history_size {
matcher_states.pop_front();
}
if !all_results.is_empty() {
return Event::caused_by(
event.source_id,
EventType::MatchesDetected(MatchesDetectedEvent {
matches: all_results
.into_iter()
.map(|result| DetectedMatch {
id: result.id,
trigger: Some(result.trigger),
right_separator: result.right_separator,
left_separator: result.left_separator,
args: result.args,
})
.collect(),
is_search: false,
}),
);
}
}
}
event
}
}
fn is_event_of_interest(event_type: &EventType) -> bool {
match event_type {
EventType::Keyboard(keyboard_event) => {
if keyboard_event.status != Status::Pressed {
// Skip non-press events
false
} else {
// Skip linux Keyboard (XKB) Extension function and modifier keys
// In hex, they have the byte 3 = 0xfe
// See list in "keysymdef.h" file
if cfg!(target_os = "linux") {
if let Key::Other(raw_code) = &keyboard_event.key {
if (65025..=65276).contains(raw_code) {
return false;
}
}
}
// Skip modifier keys
!matches!(
keyboard_event.key,
Key::Alt | Key::Shift | Key::CapsLock | Key::Meta | Key::NumLock | Key::Control
)
}
}
EventType::Mouse(mouse_event) => mouse_event.status == Status::Pressed,
EventType::MatchInjected => true,
_ => false,
}
}
fn convert_to_matcher_event(event_type: &EventType) -> Option<MatcherEvent> {
match event_type {
EventType::Keyboard(keyboard_event) => Some(MatcherEvent::Key {
key: keyboard_event.key.clone(),
chars: keyboard_event.value.clone(),
}),
EventType::Mouse(_) => Some(MatcherEvent::VirtualSeparator),
EventType::MatchInjected => Some(MatcherEvent::VirtualSeparator),
_ => None,
}
}
fn is_invalidating_event(event_type: &EventType) -> bool {
match event_type {
EventType::Keyboard(keyboard_event) => matches!(
keyboard_event.key,
Key::ArrowDown
| Key::ArrowLeft
| Key::ArrowRight
| Key::ArrowUp
| Key::End
| Key::Home
| Key::PageDown
| Key::PageUp
| Key::Escape
),
EventType::Mouse(_) => true,
_ => false,
}
}
fn should_skip_key_event_due_to_modifier_press(modifier_state: &ModifierState) -> bool {
if cfg!(target_os = "macos") {
modifier_state.is_meta_down
} else if cfg!(target_os = "windows") {
modifier_state.is_alt_down
} else if cfg!(target_os = "linux") {
modifier_state.is_alt_down || modifier_state.is_meta_down
} else {
unreachable!()
}
}
// TODO: test