feat(engine): move engine into separate module

This commit is contained in:
Federico Terzi 2021-08-14 11:07:43 +02:00
parent de8879e03c
commit 974e405b23
39 changed files with 3152 additions and 0 deletions

17
espanso-engine/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "espanso-engine"
version = "0.1.0"
authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
edition = "2018"
[dependencies]
log = "0.4.14"
anyhow = "1.0.38"
thiserror = "1.0.23"
crossbeam = "0.8.0"
markdown = "0.3.0"
html2text = "0.2.1"
[dev-dependencies]
tempdir = "0.3.7"

View File

@ -0,0 +1,78 @@
/*
* 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 super::{ContextMenuHandler, Event, IconHandler, ImageInjector, SecureInputManager};
use super::{Dispatcher, Executor, HtmlInjector, KeyInjector, ModeProvider, TextInjector};
pub struct DefaultDispatcher<'a> {
executors: Vec<Box<dyn Executor + 'a>>,
}
#[allow(clippy::too_many_arguments)]
impl<'a> DefaultDispatcher<'a> {
pub fn new(
event_injector: &'a dyn TextInjector,
clipboard_injector: &'a dyn TextInjector,
mode_provider: &'a dyn ModeProvider,
key_injector: &'a dyn KeyInjector,
html_injector: &'a dyn HtmlInjector,
image_injector: &'a dyn ImageInjector,
context_menu_handler: &'a dyn ContextMenuHandler,
icon_handler: &'a dyn IconHandler,
secure_input_manager: &'a dyn SecureInputManager,
) -> Self {
Self {
executors: vec![
Box::new(super::executor::text_inject::TextInjectExecutor::new(
event_injector,
clipboard_injector,
mode_provider,
)),
Box::new(super::executor::key_inject::KeyInjectExecutor::new(
key_injector,
)),
Box::new(super::executor::html_inject::HtmlInjectExecutor::new(
html_injector,
)),
Box::new(super::executor::image_inject::ImageInjectExecutor::new(
image_injector,
)),
Box::new(super::executor::context_menu::ContextMenuExecutor::new(
context_menu_handler,
)),
Box::new(super::executor::icon_update::IconUpdateExecutor::new(
icon_handler,
)),
Box::new(super::executor::secure_input::SecureInputExecutor::new(
secure_input_manager,
)),
],
}
}
}
impl<'a> Dispatcher for DefaultDispatcher<'a> {
fn dispatch(&self, event: Event) {
for executor in self.executors.iter() {
if executor.execute(&event) {
break;
}
}
}
}

View File

@ -0,0 +1,53 @@
/*
* 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 crate::event::{ui::MenuItem, EventType};
use crate::{dispatch::Executor, event::Event};
use anyhow::Result;
use log::error;
pub trait ContextMenuHandler {
fn show_context_menu(&self, items: &[MenuItem]) -> Result<()>;
}
pub struct ContextMenuExecutor<'a> {
handler: &'a dyn ContextMenuHandler,
}
impl<'a> ContextMenuExecutor<'a> {
pub fn new(handler: &'a dyn ContextMenuHandler) -> Self {
Self { handler }
}
}
impl<'a> Executor for ContextMenuExecutor<'a> {
fn execute(&self, event: &Event) -> bool {
if let EventType::ShowContextMenu(context_menu_event) = &event.etype {
if let Err(error) = self.handler.show_context_menu(&context_menu_event.items) {
error!("context menu handler reported an error: {:?}", error);
}
return true;
}
false
}
}
// TODO: test

View File

@ -0,0 +1,64 @@
/*
* 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 anyhow::Result;
use log::error;
use crate::{
dispatch::Executor,
event::{Event, EventType},
};
pub trait HtmlInjector {
fn inject_html(&self, html: &str, fallback: &str) -> Result<()>;
}
pub struct HtmlInjectExecutor<'a> {
injector: &'a dyn HtmlInjector,
}
impl<'a> HtmlInjectExecutor<'a> {
pub fn new(injector: &'a dyn HtmlInjector) -> Self {
Self { injector }
}
}
impl<'a> Executor for HtmlInjectExecutor<'a> {
fn execute(&self, event: &Event) -> bool {
if let EventType::HtmlInject(inject_event) = &event.etype {
// Render the text fallback for those applications that don't support HTML clipboard
let decorator = html2text::render::text_renderer::TrivialDecorator::new();
let fallback_text =
html2text::from_read_with_decorator(inject_event.html.as_bytes(), 1000000, decorator);
if let Err(error) = self
.injector
.inject_html(&inject_event.html, &fallback_text)
{
error!("html injector reported an error: {:?}", error);
}
return true;
}
false
}
}
// TODO: test

View File

@ -0,0 +1,56 @@
/*
* 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 anyhow::Result;
use log::error;
use crate::{
dispatch::Executor,
event::{ui::IconStatus, Event, EventType},
};
pub trait IconHandler {
fn update_icon(&self, status: &IconStatus) -> Result<()>;
}
pub struct IconUpdateExecutor<'a> {
handler: &'a dyn IconHandler,
}
impl<'a> IconUpdateExecutor<'a> {
pub fn new(handler: &'a dyn IconHandler) -> Self {
Self { handler }
}
}
impl<'a> Executor for IconUpdateExecutor<'a> {
fn execute(&self, event: &Event) -> bool {
if let EventType::IconStatusChange(m_event) = &event.etype {
if let Err(error) = self.handler.update_icon(&m_event.status) {
error!("icon handler reported an error: {:?}", error);
}
return true;
}
false
}
}
// TODO: test

View File

@ -0,0 +1,56 @@
/*
* 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 anyhow::Result;
use log::error;
use crate::{
dispatch::Executor,
event::{Event, EventType},
};
pub trait ImageInjector {
fn inject_image(&self, path: &str) -> Result<()>;
}
pub struct ImageInjectExecutor<'a> {
injector: &'a dyn ImageInjector,
}
impl<'a> ImageInjectExecutor<'a> {
pub fn new(injector: &'a dyn ImageInjector) -> Self {
Self { injector }
}
}
impl<'a> Executor for ImageInjectExecutor<'a> {
fn execute(&self, event: &Event) -> bool {
if let EventType::ImageInject(inject_event) = &event.etype {
if let Err(error) = self.injector.inject_image(&inject_event.image_path) {
error!("image injector reported an error: {:?}", error);
}
return true;
}
false
}
}
// TODO: test

View File

@ -0,0 +1,52 @@
/*
* 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 crate::{
dispatch::Executor,
event::{input::Key, Event, EventType},
};
use anyhow::Result;
use log::error;
pub trait KeyInjector {
fn inject_sequence(&self, keys: &[Key]) -> Result<()>;
}
pub struct KeyInjectExecutor<'a> {
injector: &'a dyn KeyInjector,
}
impl<'a> KeyInjectExecutor<'a> {
pub fn new(injector: &'a dyn KeyInjector) -> Self {
Self { injector }
}
}
impl<'a> Executor for KeyInjectExecutor<'a> {
fn execute(&self, event: &Event) -> bool {
if let EventType::KeySequenceInject(inject_event) = &event.etype {
if let Err(error) = self.injector.inject_sequence(&inject_event.keys) {
error!("key injector reported an error: {}", error);
}
return true;
}
false
}
}

View File

@ -0,0 +1,26 @@
/*
* 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/>.
*/
pub mod context_menu;
pub mod html_inject;
pub mod icon_update;
pub mod image_inject;
pub mod key_inject;
pub mod secure_input;
pub mod text_inject;

View File

@ -0,0 +1,59 @@
/*
* 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 anyhow::Result;
use log::error;
use crate::{
dispatch::Executor,
event::{Event, EventType},
};
pub trait SecureInputManager {
fn display_secure_input_troubleshoot(&self) -> Result<()>;
fn launch_secure_input_autofix(&self) -> Result<()>;
}
pub struct SecureInputExecutor<'a> {
manager: &'a dyn SecureInputManager,
}
impl<'a> SecureInputExecutor<'a> {
pub fn new(manager: &'a dyn SecureInputManager) -> Self {
Self { manager }
}
}
impl<'a> Executor for SecureInputExecutor<'a> {
fn execute(&self, event: &Event) -> bool {
if let EventType::DisplaySecureInputTroubleshoot = &event.etype {
if let Err(error) = self.manager.display_secure_input_troubleshoot() {
error!("unable to display secure input troubleshoot: {}", error);
}
return true;
} else if let EventType::LaunchSecureInputAutoFix = &event.etype {
if let Err(error) = self.manager.launch_secure_input_autofix() {
error!("unable to launch secure input autofix: {}", error);
}
return true;
}
false
}
}

View File

@ -0,0 +1,117 @@
/*
* 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 crate::{
dispatch::Executor,
event::{effect::TextInjectMode, Event, EventType},
};
use anyhow::Result;
use log::{error, trace};
pub trait TextInjector {
fn name(&self) -> &'static str;
fn inject_text(&self, text: &str) -> Result<()>;
}
pub trait ModeProvider {
fn active_mode(&self) -> Mode;
}
pub enum Mode {
Event,
Clipboard,
Auto {
// Maximum size after which the clipboard backend
// is used over the event one to speed up the injection.
clipboard_threshold: usize,
},
}
pub struct TextInjectExecutor<'a> {
event_injector: &'a dyn TextInjector,
clipboard_injector: &'a dyn TextInjector,
mode_provider: &'a dyn ModeProvider,
}
impl<'a> TextInjectExecutor<'a> {
pub fn new(
event_injector: &'a dyn TextInjector,
clipboard_injector: &'a dyn TextInjector,
mode_provider: &'a dyn ModeProvider,
) -> Self {
Self {
event_injector,
clipboard_injector,
mode_provider,
}
}
}
impl<'a> Executor for TextInjectExecutor<'a> {
fn execute(&self, event: &Event) -> bool {
if let EventType::TextInject(inject_event) = &event.etype {
let active_mode = self.mode_provider.active_mode();
let injector = if let Some(force_mode) = &inject_event.force_mode {
if let TextInjectMode::Keys = force_mode {
self.event_injector
} else {
self.clipboard_injector
}
} else if let Mode::Clipboard = active_mode {
self.clipboard_injector
} else if let Mode::Event = active_mode {
self.event_injector
} else if let Mode::Auto {
clipboard_threshold,
} = active_mode
{
if inject_event.text.chars().count() > clipboard_threshold {
self.clipboard_injector
} else if cfg!(target_os = "linux") {
if inject_event.text.chars().all(|c| c.is_ascii()) {
self.event_injector
} else {
self.clipboard_injector
}
} else {
self.event_injector
}
} else {
self.event_injector
};
trace!("using injector: {}", injector.name());
if let Err(error) = injector.inject_text(&inject_event.text) {
error!(
"text injector ({}) reported an error: {:?}",
injector.name(),
error
);
}
return true;
}
false
}
}
// TODO: test

View File

@ -0,0 +1,65 @@
/*
* 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 crate::event::Event;
mod default;
mod executor;
pub trait Executor {
fn execute(&self, event: &Event) -> bool;
}
pub trait Dispatcher {
fn dispatch(&self, event: Event);
}
// Re-export dependency injection entities
pub use executor::context_menu::ContextMenuHandler;
pub use executor::html_inject::HtmlInjector;
pub use executor::icon_update::IconHandler;
pub use executor::image_inject::ImageInjector;
pub use executor::key_inject::KeyInjector;
pub use executor::secure_input::SecureInputManager;
pub use executor::text_inject::{Mode, ModeProvider, TextInjector};
#[allow(clippy::too_many_arguments)]
pub fn default<'a>(
event_injector: &'a dyn TextInjector,
clipboard_injector: &'a dyn TextInjector,
mode_provider: &'a dyn ModeProvider,
key_injector: &'a dyn KeyInjector,
html_injector: &'a dyn HtmlInjector,
image_injector: &'a dyn ImageInjector,
context_menu_handler: &'a dyn ContextMenuHandler,
icon_handler: &'a dyn IconHandler,
secure_input_manager: &'a dyn SecureInputManager,
) -> impl Dispatcher + 'a {
default::DefaultDispatcher::new(
event_injector,
clipboard_injector,
mode_provider,
key_injector,
html_injector,
image_injector,
context_menu_handler,
icon_handler,
secure_input_manager,
)
}

View File

@ -0,0 +1,63 @@
/*
* 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 super::input::Key;
#[derive(Debug, Clone, PartialEq)]
pub struct TriggerCompensationEvent {
pub trigger: String,
pub left_separator: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CursorHintCompensationEvent {
pub cursor_hint_back_count: usize,
}
#[derive(Debug, Clone)]
pub struct TextInjectRequest {
pub text: String,
pub force_mode: Option<TextInjectMode>,
}
#[derive(Debug, Clone)]
pub struct MarkdownInjectRequest {
pub markdown: String,
}
#[derive(Debug, Clone)]
pub struct HtmlInjectRequest {
pub html: String,
}
#[derive(Debug, PartialEq, Clone)]
pub enum TextInjectMode {
Keys,
Clipboard,
}
#[derive(Debug, Clone)]
pub struct KeySequenceInjectRequest {
pub keys: Vec<Key>,
}
#[derive(Debug, Clone)]
pub struct ImageInjectRequest {
pub image_path: String,
}

View File

@ -0,0 +1,123 @@
/*
* 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/>.
*/
#[derive(Debug, PartialEq, Clone)]
pub enum Status {
Pressed,
Released,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Variant {
Left,
Right,
}
#[derive(Debug, PartialEq, Clone)]
pub struct KeyboardEvent {
pub key: Key,
pub value: Option<String>,
pub status: Status,
pub variant: Option<Variant>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MouseButton {
Left,
Right,
Middle,
Button1,
Button2,
Button3,
Button4,
Button5,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MouseEvent {
pub button: MouseButton,
pub status: Status,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Key {
// Modifiers
Alt,
CapsLock,
Control,
Meta,
NumLock,
Shift,
// Whitespace
Enter,
Tab,
Space,
// Navigation
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
End,
Home,
PageDown,
PageUp,
// UI
Escape,
// Editing keys
Backspace,
// Function keys
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
// Other keys, includes the raw code provided by the operating system
Other(i32),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ContextMenuClickedEvent {
pub context_item_id: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct HotKeyEvent {
pub hotkey_id: i32,
}

View File

@ -0,0 +1,91 @@
/*
* 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::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct MatchesDetectedEvent {
pub matches: Vec<DetectedMatch>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct DetectedMatch {
pub id: i32,
pub trigger: Option<String>,
pub left_separator: Option<String>,
pub right_separator: Option<String>,
pub args: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatchSelectedEvent {
pub chosen: DetectedMatch,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CauseCompensatedMatchEvent {
pub m: DetectedMatch,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RenderingRequestedEvent {
pub match_id: i32,
pub trigger: Option<String>,
pub left_separator: Option<String>,
pub right_separator: Option<String>,
pub trigger_args: HashMap<String, String>,
pub format: TextFormat,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TextFormat {
Plain,
Markdown,
Html,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImageRequestedEvent {
pub match_id: i32,
pub image_path: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImageResolvedEvent {
pub image_path: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RenderedEvent {
pub match_id: i32,
pub body: String,
pub format: TextFormat,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DiscardPreviousEvent {
// All Events with a source_id smaller than this one will be discarded
pub minimum_source_id: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SecureInputEnabledEvent {
pub app_name: String,
pub app_path: String,
}

View File

@ -0,0 +1,107 @@
/*
* 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/>.
*/
pub mod effect;
pub mod input;
pub mod internal;
pub mod ui;
pub type SourceId = u32;
#[derive(Debug, Clone)]
pub struct Event {
// The source id is a unique, monothonically increasing number
// that is given to each event by the source and is propagated
// to all consequential events.
// For example, if a keyboard event with source_id = 5 generates
// a detected match event, this event will have source_id = 5
pub source_id: SourceId,
pub etype: EventType,
}
impl Event {
pub fn caused_by(cause_id: SourceId, event_type: EventType) -> Event {
Event {
source_id: cause_id,
etype: event_type,
}
}
}
#[derive(Debug, Clone)]
#[allow(clippy::upper_case_acronyms)]
pub enum EventType {
NOOP,
ProcessingError(String),
ExitRequested(ExitMode),
Exit(ExitMode),
Heartbeat,
// Inputs
Keyboard(input::KeyboardEvent),
Mouse(input::MouseEvent),
HotKey(input::HotKeyEvent),
TrayIconClicked,
ContextMenuClicked(input::ContextMenuClickedEvent),
// Internal
MatchesDetected(internal::MatchesDetectedEvent),
MatchSelected(internal::MatchSelectedEvent),
CauseCompensatedMatch(internal::CauseCompensatedMatchEvent),
RenderingRequested(internal::RenderingRequestedEvent),
ImageRequested(internal::ImageRequestedEvent),
Rendered(internal::RenderedEvent),
ImageResolved(internal::ImageResolvedEvent),
MatchInjected,
DiscardPrevious(internal::DiscardPreviousEvent),
Disabled,
Enabled,
DisableRequest,
EnableRequest,
SecureInputEnabled(internal::SecureInputEnabledEvent),
SecureInputDisabled,
// Effects
TriggerCompensation(effect::TriggerCompensationEvent),
CursorHintCompensation(effect::CursorHintCompensationEvent),
KeySequenceInject(effect::KeySequenceInjectRequest),
TextInject(effect::TextInjectRequest),
MarkdownInject(effect::MarkdownInjectRequest),
HtmlInject(effect::HtmlInjectRequest),
ImageInject(effect::ImageInjectRequest),
// UI
ShowContextMenu(ui::ShowContextMenuEvent),
IconStatusChange(ui::IconStatusChangeEvent),
DisplaySecureInputTroubleshoot,
ShowSearchBar,
// Other
LaunchSecureInputAutoFix,
}
#[derive(Debug, Clone)]
pub enum ExitMode {
Exit,
ExitAllProcesses,
RestartWorker,
}

View File

@ -0,0 +1,54 @@
/*
* 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/>.
*/
#[derive(Debug, Clone, PartialEq)]
pub struct ShowContextMenuEvent {
pub items: Vec<MenuItem>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MenuItem {
Simple(SimpleMenuItem),
Sub(SubMenuItem),
Separator,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SimpleMenuItem {
pub id: u32,
pub label: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SubMenuItem {
pub label: String,
pub items: Vec<MenuItem>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct IconStatusChangeEvent {
pub status: IconStatus,
}
#[derive(Debug, Clone, PartialEq)]
pub enum IconStatus {
Enabled,
Disabled,
SecureInputDisabled,
}

View File

@ -0,0 +1,54 @@
/*
* 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 crossbeam::channel::Select;
use super::{Funnel, FunnelResult, Source};
pub struct DefaultFunnel<'a> {
sources: &'a [&'a dyn Source<'a>],
}
impl<'a> DefaultFunnel<'a> {
pub fn new(sources: &'a [&'a dyn Source<'a>]) -> Self {
Self { sources }
}
}
impl<'a> Funnel for DefaultFunnel<'a> {
fn receive(&self) -> FunnelResult {
let mut select = Select::new();
// First register all the sources to the select operation
for source in self.sources.iter() {
source.register(&mut select);
}
// Wait for the first source (blocking operation)
let op = select.select();
let source = self
.sources
.get(op.index())
.expect("invalid source index returned by select operation");
// Receive (and convert) the event
let event = source.receive(op);
FunnelResult::Event(event)
}
}

View File

@ -0,0 +1,44 @@
/*
* 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 crossbeam::{channel::Select, channel::SelectedOperation};
use self::default::DefaultFunnel;
use crate::event::Event;
mod default;
pub trait Source<'a> {
fn register(&'a self, select: &mut Select<'a>) -> usize;
fn receive(&'a self, op: SelectedOperation) -> Event;
}
pub trait Funnel {
fn receive(&self) -> FunnelResult;
}
pub enum FunnelResult {
Event(Event),
EndOfStream,
}
pub fn default<'a>(sources: &'a [&'a dyn Source<'a>]) -> impl Funnel + 'a {
DefaultFunnel::new(sources)
}

74
espanso-engine/src/lib.rs Normal file
View File

@ -0,0 +1,74 @@
/*
* 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::debug;
use self::{
dispatch::Dispatcher,
event::{Event, EventType, ExitMode},
funnel::{Funnel, FunnelResult},
process::Processor,
};
pub mod dispatch;
pub mod event;
pub mod funnel;
pub mod process;
pub struct Engine<'a> {
funnel: &'a dyn Funnel,
processor: &'a mut dyn Processor,
dispatcher: &'a dyn Dispatcher,
}
impl<'a> Engine<'a> {
pub fn new(
funnel: &'a dyn Funnel,
processor: &'a mut dyn Processor,
dispatcher: &'a dyn Dispatcher,
) -> Self {
Self {
funnel,
processor,
dispatcher,
}
}
pub fn run(&mut self) -> ExitMode {
loop {
match self.funnel.receive() {
FunnelResult::Event(event) => {
let processed_events = self.processor.process(event);
for event in processed_events {
if let EventType::Exit(mode) = &event.etype {
debug!("exit event received with mode {:?}, exiting engine", mode);
return mode.clone();
}
self.dispatcher.dispatch(event);
}
}
FunnelResult::EndOfStream => {
debug!("end of stream received");
return ExitMode::Exit;
}
}
}
}
}

View File

@ -0,0 +1,155 @@
/*
* 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 super::{
middleware::{
action::{ActionMiddleware, EventSequenceProvider},
cause::CauseCompensateMiddleware,
cursor_hint::CursorHintMiddleware,
delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider},
markdown::MarkdownMiddleware,
match_select::MatchSelectMiddleware,
matcher::MatcherMiddleware,
multiplex::MultiplexMiddleware,
past_discard::PastEventsDiscardMiddleware,
render::RenderMiddleware,
},
DisableOptions, MatchFilter, MatchInfoProvider, MatchProvider, MatchSelector, Matcher,
MatcherMiddlewareConfigProvider, Middleware, Multiplexer, PathProvider, Processor, Renderer,
};
use crate::{
event::{Event, EventType},
process::middleware::{
context_menu::ContextMenuMiddleware, disable::DisableMiddleware, exit::ExitMiddleware,
hotkey::HotKeyMiddleware, icon_status::IconStatusMiddleware,
image_resolve::ImageResolverMiddleware, search::SearchMiddleware,
},
};
use std::collections::VecDeque;
pub struct DefaultProcessor<'a> {
event_queue: VecDeque<Event>,
middleware: Vec<Box<dyn Middleware + 'a>>,
}
#[allow(clippy::too_many_arguments)]
impl<'a> DefaultProcessor<'a> {
pub fn new<MatcherState>(
matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
match_filter: &'a dyn MatchFilter,
match_selector: &'a dyn MatchSelector,
multiplexer: &'a dyn Multiplexer,
renderer: &'a dyn Renderer<'a>,
match_info_provider: &'a dyn MatchInfoProvider,
modifier_status_provider: &'a dyn ModifierStatusProvider,
event_sequence_provider: &'a dyn EventSequenceProvider,
path_provider: &'a dyn PathProvider,
disable_options: DisableOptions,
matcher_options_provider: &'a dyn MatcherMiddlewareConfigProvider,
match_provider: &'a dyn MatchProvider,
) -> DefaultProcessor<'a> {
Self {
event_queue: VecDeque::new(),
middleware: vec![
Box::new(PastEventsDiscardMiddleware::new()),
Box::new(DisableMiddleware::new(disable_options)),
Box::new(IconStatusMiddleware::new()),
Box::new(MatcherMiddleware::new(matchers, matcher_options_provider)),
Box::new(ContextMenuMiddleware::new()),
Box::new(HotKeyMiddleware::new()),
Box::new(MatchSelectMiddleware::new(match_filter, match_selector)),
Box::new(CauseCompensateMiddleware::new()),
Box::new(MultiplexMiddleware::new(multiplexer)),
Box::new(RenderMiddleware::new(renderer)),
Box::new(ImageResolverMiddleware::new(path_provider)),
Box::new(CursorHintMiddleware::new()),
Box::new(ExitMiddleware::new()),
Box::new(ActionMiddleware::new(
match_info_provider,
event_sequence_provider,
)),
Box::new(SearchMiddleware::new(match_provider)),
Box::new(MarkdownMiddleware::new()),
Box::new(DelayForModifierReleaseMiddleware::new(
modifier_status_provider,
)),
],
}
}
fn process_one(&mut self) -> Option<Event> {
if let Some(event) = self.event_queue.pop_back() {
let mut current_event = event;
let mut current_queue = VecDeque::new();
let mut dispatch = |event: Event| {
trace!("dispatched event: {:?}", event);
current_queue.push_front(event);
};
trace!("--------------- new event -----------------");
for middleware in self.middleware.iter() {
trace!(
"middleware '{}' received event: {:?}",
middleware.name(),
current_event
);
current_event = middleware.next(current_event, &mut dispatch);
trace!(
"middleware '{}' produced event: {:?}",
middleware.name(),
current_event
);
if let EventType::NOOP = current_event.etype {
trace!("interrupting chain as the event is NOOP");
break;
}
}
while let Some(event) = current_queue.pop_back() {
self.event_queue.push_front(event);
}
Some(current_event)
} else {
None
}
}
}
impl<'a> Processor for DefaultProcessor<'a> {
fn process(&mut self, event: Event) -> Vec<Event> {
self.event_queue.push_front(event);
let mut processed_events = Vec::new();
while !self.event_queue.is_empty() {
if let Some(event) = self.process_one() {
processed_events.push(event);
}
}
processed_events
}
}

View File

@ -0,0 +1,136 @@
/*
* 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 super::super::Middleware;
use crate::event::{
effect::{
HtmlInjectRequest, ImageInjectRequest, KeySequenceInjectRequest, MarkdownInjectRequest,
TextInjectMode, TextInjectRequest,
},
input::Key,
internal::{DiscardPreviousEvent, TextFormat},
Event, EventType,
};
pub trait MatchInfoProvider {
fn get_force_mode(&self, match_id: i32) -> Option<TextInjectMode>;
}
pub trait EventSequenceProvider {
fn get_next_id(&self) -> u32;
}
pub struct ActionMiddleware<'a> {
match_info_provider: &'a dyn MatchInfoProvider,
event_sequence_provider: &'a dyn EventSequenceProvider,
}
impl<'a> ActionMiddleware<'a> {
pub fn new(
match_info_provider: &'a dyn MatchInfoProvider,
event_sequence_provider: &'a dyn EventSequenceProvider,
) -> Self {
Self {
match_info_provider,
event_sequence_provider,
}
}
}
impl<'a> Middleware for ActionMiddleware<'a> {
fn name(&self) -> &'static str {
"action"
}
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
match &event.etype {
EventType::Rendered(_) | EventType::ImageResolved(_) => {
dispatch(Event::caused_by(event.source_id, EventType::MatchInjected));
dispatch(Event::caused_by(
event.source_id,
EventType::DiscardPrevious(DiscardPreviousEvent {
minimum_source_id: self.event_sequence_provider.get_next_id(),
}),
));
match &event.etype {
EventType::Rendered(m_event) => Event::caused_by(
event.source_id,
match m_event.format {
TextFormat::Plain => EventType::TextInject(TextInjectRequest {
text: m_event.body.clone(),
force_mode: self.match_info_provider.get_force_mode(m_event.match_id),
}),
TextFormat::Html => EventType::HtmlInject(HtmlInjectRequest {
html: m_event.body.clone(),
}),
TextFormat::Markdown => EventType::MarkdownInject(MarkdownInjectRequest {
markdown: m_event.body.clone(),
}),
},
),
EventType::ImageResolved(m_event) => Event::caused_by(
event.source_id,
EventType::ImageInject(ImageInjectRequest {
image_path: m_event.image_path.clone(),
}),
),
_ => unreachable!(),
}
}
EventType::CursorHintCompensation(m_event) => {
dispatch(Event::caused_by(
event.source_id,
EventType::DiscardPrevious(DiscardPreviousEvent {
minimum_source_id: self.event_sequence_provider.get_next_id(),
}),
));
Event::caused_by(
event.source_id,
EventType::KeySequenceInject(KeySequenceInjectRequest {
keys: (0..m_event.cursor_hint_back_count)
.map(|_| Key::ArrowLeft)
.collect(),
}),
)
}
EventType::TriggerCompensation(m_event) => {
let mut backspace_count = m_event.trigger.chars().count();
// We want to preserve the left separator if present
if let Some(left_separator) = &m_event.left_separator {
backspace_count -= left_separator.chars().count();
}
Event::caused_by(
event.source_id,
EventType::KeySequenceInject(KeySequenceInjectRequest {
keys: (0..backspace_count).map(|_| Key::Backspace).collect(),
}),
)
}
_ => event,
}
// TODO: handle images
}
}
// TODO: test

View File

@ -0,0 +1,58 @@
/*
* 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 super::super::Middleware;
use crate::{event::{Event, EventType, effect::TriggerCompensationEvent, internal::CauseCompensatedMatchEvent}};
pub struct CauseCompensateMiddleware {}
impl CauseCompensateMiddleware {
pub fn new() -> Self {
Self {}
}
}
impl Middleware for CauseCompensateMiddleware {
fn name(&self) -> &'static str {
"cause_compensate"
}
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
if let EventType::MatchSelected(m_event) = &event.etype {
let compensated_event =
Event::caused_by(event.source_id, EventType::CauseCompensatedMatch(CauseCompensatedMatchEvent { m: m_event.chosen.clone() }));
if let Some(trigger) = &m_event.chosen.trigger {
dispatch(compensated_event);
// Before the event, place a trigger compensation
return Event::caused_by(event.source_id, EventType::TriggerCompensation(TriggerCompensationEvent {
trigger: trigger.clone(),
left_separator: m_event.chosen.left_separator.clone(),
}));
} else {
return compensated_event;
}
}
event
}
}
// TODO: test

View File

@ -0,0 +1,185 @@
/*
* This file is part of espanso.
*
* Copyright id: (), label: () id: (), label: () id: (), label: ()(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::cell::RefCell;
use super::super::Middleware;
use crate::event::{
ui::{MenuItem, ShowContextMenuEvent, SimpleMenuItem},
Event, EventType, ExitMode,
};
const CONTEXT_ITEM_EXIT: u32 = 0;
const CONTEXT_ITEM_RELOAD: u32 = 1;
const CONTEXT_ITEM_ENABLE: u32 = 2;
const CONTEXT_ITEM_DISABLE: u32 = 3;
const CONTEXT_ITEM_SECURE_INPUT_EXPLAIN: u32 = 4;
const CONTEXT_ITEM_SECURE_INPUT_TRIGGER_WORKAROUND: u32 = 5;
const CONTEXT_ITEM_OPEN_SEARCH: u32 = 6;
pub struct ContextMenuMiddleware {
is_enabled: RefCell<bool>,
is_secure_input_enabled: RefCell<bool>,
}
impl ContextMenuMiddleware {
pub fn new() -> Self {
Self {
is_enabled: RefCell::new(true),
is_secure_input_enabled: RefCell::new(false),
}
}
}
#[allow(clippy::needless_return)]
impl Middleware for ContextMenuMiddleware {
fn name(&self) -> &'static str {
"context_menu"
}
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
let mut is_enabled = self.is_enabled.borrow_mut();
let mut is_secure_input_enabled = self.is_secure_input_enabled.borrow_mut();
match &event.etype {
EventType::TrayIconClicked => {
// TODO: fetch top matches for the active config to be added
let mut items = vec![
MenuItem::Simple(if *is_enabled {
SimpleMenuItem {
id: CONTEXT_ITEM_DISABLE,
label: "Disable".to_string(),
}
} else {
SimpleMenuItem {
id: CONTEXT_ITEM_ENABLE,
label: "Enable".to_string(),
}
}),
MenuItem::Simple(SimpleMenuItem {
id: CONTEXT_ITEM_OPEN_SEARCH,
label: "Open search bar".to_string(),
}),
MenuItem::Separator,
MenuItem::Simple(SimpleMenuItem {
id: CONTEXT_ITEM_RELOAD,
label: "Reload config".to_string(),
}),
MenuItem::Separator,
MenuItem::Simple(SimpleMenuItem {
id: CONTEXT_ITEM_EXIT,
label: "Exit espanso".to_string(),
}),
];
if *is_secure_input_enabled {
items.insert(
0,
MenuItem::Simple(SimpleMenuItem {
id: CONTEXT_ITEM_SECURE_INPUT_EXPLAIN,
label: "Why is espanso not working?".to_string(),
}),
);
items.insert(
1,
MenuItem::Simple(SimpleMenuItem {
id: CONTEXT_ITEM_SECURE_INPUT_TRIGGER_WORKAROUND,
label: "Launch SecureInput auto-fix".to_string(),
}),
);
items.insert(2, MenuItem::Separator);
}
// TODO: my idea is to use a set of reserved u32 ids for built-in
// actions such as Exit, Open Editor etc
// then we need some u32 for the matches, so we need to create
// a mapping structure match_id <-> context-menu-id
return Event::caused_by(
event.source_id,
EventType::ShowContextMenu(ShowContextMenuEvent {
// TODO: add actual entries
items,
}),
);
}
EventType::ContextMenuClicked(context_click_event) => {
match context_click_event.context_item_id {
CONTEXT_ITEM_EXIT => Event::caused_by(
event.source_id,
EventType::ExitRequested(ExitMode::ExitAllProcesses),
),
CONTEXT_ITEM_RELOAD => Event::caused_by(
event.source_id,
EventType::ExitRequested(ExitMode::RestartWorker),
),
CONTEXT_ITEM_ENABLE => {
dispatch(Event::caused_by(event.source_id, EventType::EnableRequest));
Event::caused_by(event.source_id, EventType::NOOP)
}
CONTEXT_ITEM_DISABLE => {
dispatch(Event::caused_by(event.source_id, EventType::DisableRequest));
Event::caused_by(event.source_id, EventType::NOOP)
}
CONTEXT_ITEM_SECURE_INPUT_EXPLAIN => {
dispatch(Event::caused_by(
event.source_id,
EventType::DisplaySecureInputTroubleshoot,
));
Event::caused_by(event.source_id, EventType::NOOP)
}
CONTEXT_ITEM_SECURE_INPUT_TRIGGER_WORKAROUND => {
dispatch(Event::caused_by(
event.source_id,
EventType::LaunchSecureInputAutoFix,
));
Event::caused_by(event.source_id, EventType::NOOP)
}
CONTEXT_ITEM_OPEN_SEARCH => {
dispatch(Event::caused_by(event.source_id, EventType::ShowSearchBar));
Event::caused_by(event.source_id, EventType::NOOP)
}
_ => {
// TODO: handle dynamic items
todo!()
}
}
}
EventType::Disabled => {
*is_enabled = false;
event
}
EventType::Enabled => {
*is_enabled = true;
event
}
EventType::SecureInputEnabled(_) => {
*is_secure_input_enabled = true;
event
}
EventType::SecureInputDisabled => {
*is_secure_input_enabled = false;
event
}
_ => event,
}
}
}
// TODO: test

View File

@ -0,0 +1,82 @@
/*
* 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 super::super::Middleware;
use crate::event::{
effect::CursorHintCompensationEvent, internal::RenderedEvent, Event, EventType,
};
pub struct CursorHintMiddleware {}
impl CursorHintMiddleware {
pub fn new() -> Self {
Self {}
}
}
impl Middleware for CursorHintMiddleware {
fn name(&self) -> &'static str {
"cursor_hint"
}
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
if let EventType::Rendered(m_event) = event.etype {
let (body, cursor_hint_back_count) = process_cursor_hint(m_event.body);
if let Some(cursor_hint_back_count) = cursor_hint_back_count {
dispatch(Event::caused_by(
event.source_id,
EventType::CursorHintCompensation(CursorHintCompensationEvent {
cursor_hint_back_count,
}),
));
}
// Alter the rendered event to remove the cursor hint from the body
return Event::caused_by(
event.source_id,
EventType::Rendered(RenderedEvent { body, ..m_event }),
);
}
event
}
}
// TODO: test
fn process_cursor_hint(body: String) -> (String, Option<usize>) {
if let Some(index) = body.find("$|$") {
// Convert the byte index to a char index
let char_str = &body[0..index];
let char_index = char_str.chars().count();
let total_size = body.chars().count();
// Remove the $|$ placeholder
let body = body.replace("$|$", "");
// Calculate the amount of rewind moves needed (LEFT ARROW).
// Subtract also 3, equal to the number of chars of the placeholder "$|$"
let moves = total_size - char_index - 3;
(body, Some(moves))
} else {
(body, None)
}
}
// TODO: test

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/>.
*/
use std::time::{Duration, Instant};
use log::{trace, warn};
use super::super::Middleware;
use crate::event::{Event, EventType};
/// Maximum time to wait for modifiers being released before
/// giving up.
const MODIFIER_DELAY_TIMEOUT: Duration = Duration::from_secs(3);
pub trait ModifierStatusProvider {
fn is_any_conflicting_modifier_pressed(&self) -> bool;
}
/// This middleware is used to delay the injection of text until
/// all modifiers have been released. This is needed as otherwise,
/// injections might misbehave as pressed modifiers might alter
/// the keys being injected.
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.etype) {
let start = Instant::now();
while self.provider.is_any_conflicting_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;
}
// TODO: here we might show a popup window to tell the users to release those keys
trace!("delaying injection event as some modifiers are pressed");
std::thread::sleep(Duration::from_millis(100));
}
}
event
}
}
fn is_injection_event(event_type: &EventType) -> bool {
matches!(
event_type,
EventType::TriggerCompensation(_)
| EventType::CursorHintCompensation(_)
| EventType::KeySequenceInject(_)
| EventType::TextInject(_)
)
}
// TODO: test

View File

@ -0,0 +1,138 @@
/*
* 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::{
cell::RefCell,
time::{Duration, Instant},
};
use log::info;
use super::super::Middleware;
use crate::event::{
input::{Key, KeyboardEvent, Status, Variant},
Event, EventType,
};
pub struct DisableOptions {
pub toggle_key: Option<Key>,
pub toggle_key_variant: Option<Variant>,
pub toggle_key_maximum_window: Duration,
// TODO: toggle shortcut?
}
pub struct DisableMiddleware {
enabled: RefCell<bool>,
last_toggle_press: RefCell<Option<Instant>>,
options: DisableOptions,
}
impl DisableMiddleware {
pub fn new(options: DisableOptions) -> Self {
Self {
enabled: RefCell::new(true),
last_toggle_press: RefCell::new(None),
options,
}
}
}
impl Middleware for DisableMiddleware {
fn name(&self) -> &'static str {
"disable"
}
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
let mut has_status_changed = false;
let mut enabled = self.enabled.borrow_mut();
match &event.etype {
EventType::Keyboard(m_event) => {
if is_toggle_key(m_event, &self.options) {
let mut last_toggle_press = self.last_toggle_press.borrow_mut();
if let Some(previous_press) = *last_toggle_press {
if previous_press.elapsed() < self.options.toggle_key_maximum_window {
*enabled = !*enabled;
*last_toggle_press = None;
has_status_changed = true;
} else {
*last_toggle_press = Some(Instant::now());
}
} else {
*last_toggle_press = Some(Instant::now());
}
}
}
EventType::EnableRequest => {
*enabled = true;
has_status_changed = true;
}
EventType::DisableRequest => {
*enabled = false;
has_status_changed = true;
}
_ => {}
}
if has_status_changed {
info!("toggled enabled state, is_enabled = {}", *enabled);
dispatch(Event::caused_by(
event.source_id,
if *enabled {
EventType::Enabled
} else {
EventType::Disabled
},
))
}
// Block keyboard events when disabled
if let EventType::Keyboard(_) = &event.etype {
if !*enabled {
return Event::caused_by(event.source_id, EventType::NOOP);
}
}
// TODO: also ignore hotkey and mouse events
event
}
}
fn is_toggle_key(event: &KeyboardEvent, options: &DisableOptions) -> bool {
if event.status != Status::Released {
return false;
}
if options
.toggle_key
.as_ref()
.map(|key| key == &event.key)
.unwrap_or(false)
{
if let (Some(variant), Some(e_variant)) = (&options.toggle_key_variant, &event.variant) {
variant == e_variant
} else {
true
}
} else {
false
}
}
// TODO: test

View File

@ -0,0 +1,51 @@
/*
* 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::debug;
use super::super::Middleware;
use crate::event::{Event, EventType};
pub struct ExitMiddleware {}
impl ExitMiddleware {
pub fn new() -> Self {
Self {}
}
}
impl Middleware for ExitMiddleware {
fn name(&self) -> &'static str {
"exit"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
if let EventType::ExitRequested(mode) = &event.etype {
debug!(
"received ExitRequested event with mode: {:?}, dispatching exit",
mode
);
return Event::caused_by(event.source_id, EventType::Exit(mode.clone()));
}
event
}
}
// TODO: test

View File

@ -0,0 +1,56 @@
/*
* This file is part of espanso id: (), trigger: (), trigger: (), left_separator: (), right_separator: (), args: () left_separator: (), right_separator: (), args: () id: (), trigger: (), left_separator: (), right_separator: (), args: ().
*
* 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 super::super::Middleware;
use crate::event::{
internal::{DetectedMatch, MatchesDetectedEvent},
Event, EventType,
};
pub struct HotKeyMiddleware {}
impl HotKeyMiddleware {
pub fn new() -> Self {
Self {}
}
}
impl Middleware for HotKeyMiddleware {
fn name(&self) -> &'static str {
"hotkey"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
if let EventType::HotKey(m_event) = &event.etype {
return Event::caused_by(
event.source_id,
EventType::MatchesDetected(MatchesDetectedEvent {
matches: vec![DetectedMatch {
id: m_event.hotkey_id,
..Default::default()
}],
}),
);
}
event
}
}
// TODO: test

View File

@ -0,0 +1,81 @@
/*
* 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::cell::RefCell;
use super::super::Middleware;
use crate::event::{
ui::{IconStatus, IconStatusChangeEvent},
Event, EventType,
};
pub struct IconStatusMiddleware {
enabled: RefCell<bool>,
secure_input_enabled: RefCell<bool>,
}
impl IconStatusMiddleware {
pub fn new() -> Self {
Self {
enabled: RefCell::new(true),
secure_input_enabled: RefCell::new(false),
}
}
}
impl Middleware for IconStatusMiddleware {
fn name(&self) -> &'static str {
"icon_status"
}
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
let mut enabled = self.enabled.borrow_mut();
let mut secure_input_enabled = self.secure_input_enabled.borrow_mut();
let mut did_update = true;
match &event.etype {
EventType::Enabled => *enabled = true,
EventType::Disabled => *enabled = false,
EventType::SecureInputEnabled(_) => *secure_input_enabled = true,
EventType::SecureInputDisabled => *secure_input_enabled = false,
_ => did_update = false,
}
if did_update {
let status = if *enabled {
if *secure_input_enabled {
IconStatus::SecureInputDisabled
} else {
IconStatus::Enabled
}
} else {
IconStatus::Disabled
};
dispatch(Event::caused_by(
event.source_id,
EventType::IconStatusChange(IconStatusChangeEvent { status }),
));
}
event
}
}
// TODO: test

View File

@ -0,0 +1,81 @@
/*
* 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::path::Path;
use log::error;
use super::super::Middleware;
use crate::event::{internal::ImageResolvedEvent, Event, EventType};
pub trait PathProvider {
fn get_config_path(&self) -> &Path;
}
pub struct ImageResolverMiddleware<'a> {
provider: &'a dyn PathProvider,
}
impl<'a> ImageResolverMiddleware<'a> {
pub fn new(provider: &'a dyn PathProvider) -> Self {
Self { provider }
}
}
impl<'a> Middleware for ImageResolverMiddleware<'a> {
fn name(&self) -> &'static str {
"image_resolve"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
if let EventType::ImageRequested(m_event) = &event.etype {
// On Windows, we have to replace the forward / with the backslash \ in the path
let path = if cfg!(target_os = "windows") {
m_event.image_path.replace("/", "\\")
} else {
m_event.image_path.to_owned()
};
let path = if path.contains("$CONFIG") {
let config_path = match self.provider.get_config_path().canonicalize() {
Ok(path) => path,
Err(err) => {
error!(
"unable to canonicalize the config path into the image resolver: {}",
err
);
self.provider.get_config_path().to_owned()
}
};
path.replace("$CONFIG", &config_path.to_string_lossy())
} else {
path
};
return Event::caused_by(
event.source_id,
EventType::ImageResolved(ImageResolvedEvent { image_path: path }),
);
}
event
}
}
// TODO: test

View File

@ -0,0 +1,63 @@
/*
* 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 super::super::Middleware;
use crate::event::{effect::HtmlInjectRequest, Event, EventType};
// Convert markdown injection requests to HTML on the fly
pub struct MarkdownMiddleware {}
impl MarkdownMiddleware {
pub fn new() -> Self {
Self {}
}
}
impl Middleware for MarkdownMiddleware {
fn name(&self) -> &'static str {
"markdown"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
if let EventType::MarkdownInject(m_event) = &event.etype {
// Render the markdown into HTML
let html = markdown::to_html(&m_event.markdown);
let mut html = html.trim();
// Remove the surrounding paragraph
if html.starts_with("<p>") {
html = html.trim_start_matches("<p>");
}
if html.ends_with("</p>") {
html = html.trim_end_matches("</p>");
}
return Event::caused_by(
event.source_id,
EventType::HtmlInject(HtmlInjectRequest {
html: html.to_owned(),
}),
);
}
event
}
}
// TODO: test

View File

@ -0,0 +1,102 @@
/*
* 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::{debug, error};
use super::super::Middleware;
use crate::event::{internal::MatchSelectedEvent, Event, EventType};
pub trait MatchFilter {
fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32>;
}
pub trait MatchSelector {
fn select(&self, matches_ids: &[i32]) -> Option<i32>;
}
pub struct MatchSelectMiddleware<'a> {
match_filter: &'a dyn MatchFilter,
match_selector: &'a dyn MatchSelector,
}
impl<'a> MatchSelectMiddleware<'a> {
pub fn new(match_filter: &'a dyn MatchFilter, match_selector: &'a dyn MatchSelector) -> Self {
Self {
match_filter,
match_selector,
}
}
}
impl<'a> Middleware for MatchSelectMiddleware<'a> {
fn name(&self) -> &'static str {
"match_select"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
if let EventType::MatchesDetected(m_event) = event.etype {
let matches_ids: Vec<i32> = m_event.matches.iter().map(|m| m.id).collect();
// Find the matches that are actually valid in the current context
let valid_ids = self.match_filter.filter_active(&matches_ids);
return match valid_ids.len() {
0 => Event::caused_by(event.source_id, EventType::NOOP), // No valid matches, consume the event
1 => {
// Only one match, no need to show a selection dialog
let m = m_event
.matches
.into_iter()
.find(|m| m.id == *valid_ids.first().unwrap());
if let Some(m) = m {
Event::caused_by(
event.source_id,
EventType::MatchSelected(MatchSelectedEvent { chosen: m }),
)
} else {
error!("MatchSelectMiddleware could not find the correspondent match");
Event::caused_by(event.source_id, EventType::NOOP)
}
}
_ => {
// Multiple matches, we need to ask the user which one to use
if let Some(selected_id) = self.match_selector.select(&valid_ids) {
let m = m_event.matches.into_iter().find(|m| m.id == selected_id);
if let Some(m) = m {
Event::caused_by(
event.source_id,
EventType::MatchSelected(MatchSelectedEvent { chosen: m }),
)
} else {
error!("MatchSelectMiddleware could not find the correspondent match");
Event::caused_by(event.source_id, EventType::NOOP)
}
} else {
debug!("MatchSelectMiddleware did not receive any match selection");
Event::caused_by(event.source_id, EventType::NOOP)
}
}
};
}
event
}
}
// TODO: test

View File

@ -0,0 +1,207 @@
/*
* 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 struct MatcherMiddleware<'a, State> {
matchers: &'a [&'a dyn Matcher<'a, State>],
matcher_states: RefCell<VecDeque<Vec<State>>>,
max_history_size: usize,
}
impl<'a, State> MatcherMiddleware<'a, State> {
pub fn new(
matchers: &'a [&'a dyn Matcher<'a, State>],
options_provider: &'a dyn MatcherMiddlewareConfigProvider,
) -> Self {
let max_history_size = options_provider.max_history_size();
Self {
matchers,
matcher_states: RefCell::new(VecDeque::new()),
max_history_size,
}
}
}
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;
}
}
// 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(),
}),
);
}
}
}
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 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,
}
}
// TODO: test

View File

@ -0,0 +1,36 @@
/*
* 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/>.
*/
pub mod action;
pub mod cause;
pub mod context_menu;
pub mod cursor_hint;
pub mod delay_modifiers;
pub mod disable;
pub mod exit;
pub mod hotkey;
pub mod icon_status;
pub mod image_resolve;
pub mod markdown;
pub mod match_select;
pub mod matcher;
pub mod multiplex;
pub mod past_discard;
pub mod render;
pub mod search;

View File

@ -0,0 +1,59 @@
/*
* 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::error;
use super::super::Middleware;
use crate::event::{internal::DetectedMatch, Event, EventType};
pub trait Multiplexer {
fn convert(&self, m: DetectedMatch) -> Option<EventType>;
}
pub struct MultiplexMiddleware<'a> {
multiplexer: &'a dyn Multiplexer,
}
impl<'a> MultiplexMiddleware<'a> {
pub fn new(multiplexer: &'a dyn Multiplexer) -> Self {
Self { multiplexer }
}
}
impl<'a> Middleware for MultiplexMiddleware<'a> {
fn name(&self) -> &'static str {
"multiplex"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
if let EventType::CauseCompensatedMatch(m_event) = event.etype {
return match self.multiplexer.convert(m_event.m) {
Some(new_event) => Event::caused_by(event.source_id, new_event),
None => {
error!("match multiplexing failed");
Event::caused_by(event.source_id, EventType::NOOP)
}
};
}
event
}
}
// TODO: test

View File

@ -0,0 +1,69 @@
/*
* 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::cell::RefCell;
use log::trace;
use super::super::Middleware;
use crate::event::{Event, EventType, SourceId};
/// This middleware discards all events that have a source_id smaller than its
/// configured threshold. This useful to discard past events that might have
/// been stuck in the event queue for too long.
pub struct PastEventsDiscardMiddleware {
source_id_threshold: RefCell<SourceId>,
}
impl PastEventsDiscardMiddleware {
pub fn new() -> Self {
Self {
source_id_threshold: RefCell::new(0),
}
}
}
impl Middleware for PastEventsDiscardMiddleware {
fn name(&self) -> &'static str {
"past_discard"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
let mut source_id_threshold = self.source_id_threshold.borrow_mut();
// Filter out previous events
if event.source_id < *source_id_threshold {
trace!("discarding previous event: {:?}", event);
return Event::caused_by(event.source_id, EventType::NOOP);
}
// Update the minimum threshold
if let EventType::DiscardPrevious(m_event) = &event.etype {
trace!(
"updating minimum source id threshold for events to: {}",
m_event.minimum_source_id
);
*source_id_threshold = m_event.minimum_source_id;
}
event
}
}
// TODO: test

View File

@ -0,0 +1,104 @@
/*
* 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::collections::HashMap;
use log::error;
use super::super::Middleware;
use crate::event::{internal::RenderedEvent, Event, EventType};
use anyhow::Result;
use thiserror::Error;
pub trait Renderer<'a> {
fn render(
&'a self,
match_id: i32,
trigger: Option<&str>,
trigger_args: HashMap<String, String>,
) -> Result<String>;
}
#[derive(Error, Debug)]
pub enum RendererError {
#[error("rendering error")]
RenderingError(#[from] anyhow::Error),
#[error("match not found")]
NotFound,
#[error("aborted")]
Aborted,
}
pub struct RenderMiddleware<'a> {
renderer: &'a dyn Renderer<'a>,
}
impl<'a> RenderMiddleware<'a> {
pub fn new(renderer: &'a dyn Renderer<'a>) -> Self {
Self { renderer }
}
}
impl<'a> Middleware for RenderMiddleware<'a> {
fn name(&self) -> &'static str {
"render"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
if let EventType::RenderingRequested(m_event) = event.etype {
match self.renderer.render(
m_event.match_id,
m_event.trigger.as_deref(),
m_event.trigger_args,
) {
Ok(body) => {
let body = if let Some(right_separator) = m_event.right_separator {
format!("{}{}", body, right_separator)
} else {
body
};
return Event::caused_by(
event.source_id,
EventType::Rendered(RenderedEvent {
match_id: m_event.match_id,
body,
format: m_event.format,
}),
);
}
Err(err) => match err.downcast_ref::<RendererError>() {
Some(RendererError::Aborted) => {
return Event::caused_by(event.source_id, EventType::NOOP)
}
_ => {
error!("error during rendering: {}", err);
return Event::caused_by(event.source_id, EventType::ProcessingError("An error has occurred during rendering, please examine the logs or contact support.".to_string()));
}
},
}
}
event
}
}
// TODO: test

View File

@ -0,0 +1,75 @@
/*
* 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::collections::HashMap;
use super::super::Middleware;
use crate::event::{
internal::{DetectedMatch, MatchesDetectedEvent},
Event, EventType,
};
pub trait MatchProvider {
fn get_all_matches_ids(&self) -> Vec<i32>;
}
pub struct SearchMiddleware<'a> {
match_provider: &'a dyn MatchProvider,
}
impl<'a> SearchMiddleware<'a> {
pub fn new(match_provider: &'a dyn MatchProvider) -> Self {
Self { match_provider }
}
}
impl<'a> Middleware for SearchMiddleware<'a> {
fn name(&self) -> &'static str {
"search"
}
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
if let EventType::ShowSearchBar = event.etype {
let detected_matches = Event::caused_by(
event.source_id,
EventType::MatchesDetected(MatchesDetectedEvent {
matches: self
.match_provider
.get_all_matches_ids()
.into_iter()
.map(|id| DetectedMatch {
id,
trigger: None,
left_separator: None,
right_separator: None,
args: HashMap::new(),
})
.collect(),
}),
);
dispatch(detected_matches);
return Event::caused_by(event.source_id, EventType::NOOP);
}
event
}
}
// TODO: test

View File

@ -0,0 +1,77 @@
/*
* 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 super::Event;
mod default;
mod middleware;
pub trait Middleware {
fn name(&self) -> &'static str;
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event;
}
pub trait Processor {
fn process(&mut self, event: Event) -> Vec<Event>;
}
// Dependency inversion entities
pub use middleware::action::{EventSequenceProvider, MatchInfoProvider};
pub use middleware::delay_modifiers::ModifierStatusProvider;
pub use middleware::disable::DisableOptions;
pub use middleware::image_resolve::PathProvider;
pub use middleware::match_select::{MatchFilter, MatchSelector};
pub use middleware::matcher::{
MatchResult, Matcher, MatcherEvent, MatcherMiddlewareConfigProvider,
};
pub use middleware::multiplex::Multiplexer;
pub use middleware::render::{Renderer, RendererError};
pub use middleware::search::MatchProvider;
#[allow(clippy::too_many_arguments)]
pub fn default<'a, MatcherState>(
matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
match_filter: &'a dyn MatchFilter,
match_selector: &'a dyn MatchSelector,
multiplexer: &'a dyn Multiplexer,
renderer: &'a dyn Renderer<'a>,
match_info_provider: &'a dyn MatchInfoProvider,
modifier_status_provider: &'a dyn ModifierStatusProvider,
event_sequence_provider: &'a dyn EventSequenceProvider,
path_provider: &'a dyn PathProvider,
disable_options: DisableOptions,
matcher_options_provider: &'a dyn MatcherMiddlewareConfigProvider,
match_provider: &'a dyn MatchProvider,
) -> impl Processor + 'a {
default::DefaultProcessor::new(
matchers,
match_filter,
match_selector,
multiplexer,
renderer,
match_info_provider,
modifier_status_provider,
event_sequence_provider,
path_provider,
disable_options,
matcher_options_provider,
match_provider,
)
}