feat(core): progress on the core pipeline

This commit is contained in:
Federico Terzi 2021-04-10 12:05:32 +02:00
parent 14fdfe4149
commit 4af4a434a3
28 changed files with 634 additions and 44 deletions

12
Cargo.lock generated
View File

@ -296,6 +296,7 @@ dependencies = [
"espanso-inject", "espanso-inject",
"espanso-match", "espanso-match",
"espanso-path", "espanso-path",
"espanso-render",
"espanso-ui", "espanso-ui",
"lazy_static", "lazy_static",
"log", "log",
@ -324,9 +325,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce", "dunce",
"enum-as-inner",
"glob", "glob",
"lazy_static", "lazy_static",
"log", "log",
"ordered-float",
"regex", "regex",
"serde", "serde",
"serde_yaml", "serde_yaml",
@ -666,6 +669,15 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "ordered-float"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "766f840da25490628d8e63e529cd21c014f6600c6b8517add12a6fa6167a6218"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.19" version = "0.3.19"

View File

@ -21,6 +21,7 @@ espanso-config = { path = "../espanso-config" }
espanso-match = { path = "../espanso-match" } espanso-match = { path = "../espanso-match" }
espanso-clipboard = { path = "../espanso-clipboard" } espanso-clipboard = { path = "../espanso-clipboard" }
espanso-info = { path = "../espanso-info" } espanso-info = { path = "../espanso-info" }
espanso-render = { path = "../espanso-render" }
espanso-path = { path = "../espanso-path" } espanso-path = { path = "../espanso-path" }
maplit = "1.0.2" maplit = "1.0.2"
simplelog = "0.9.0" simplelog = "0.9.0"

View File

@ -17,7 +17,10 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::collections::HashSet; use std::{
cell::RefCell,
collections::{HashMap, HashSet},
};
use crate::engine::process::MatchFilter; use crate::engine::process::MatchFilter;
use espanso_config::{ use espanso_config::{
@ -27,6 +30,8 @@ use espanso_config::{
use espanso_info::{AppInfo, AppInfoProvider}; use espanso_info::{AppInfo, AppInfoProvider};
use std::iter::FromIterator; use std::iter::FromIterator;
use super::engine::render::ConfigProvider;
pub struct ConfigManager<'a> { pub struct ConfigManager<'a> {
config_store: &'a dyn ConfigStore, config_store: &'a dyn ConfigStore,
match_store: &'a dyn MatchStore, match_store: &'a dyn MatchStore,
@ -46,29 +51,16 @@ impl<'a> ConfigManager<'a> {
} }
} }
fn active(&self) -> &'a dyn Config { pub fn active(&self) -> &'a dyn Config {
let current_app = self.app_info_provider.get_info(); let current_app = self.app_info_provider.get_info();
let info = to_app_properties(&current_app); let info = to_app_properties(&current_app);
self.config_store.active(&info) self.config_store.active(&info)
} }
fn active_match_set(&self) -> MatchSet { pub fn active_context(&self) -> (&'a dyn Config, MatchSet) {
let match_paths = self.active().match_paths(); let config = self.active();
self.match_store.query(&match_paths) let match_paths = config.match_paths();
} (config, self.match_store.query(&match_paths))
}
impl<'a> MatchFilter for ConfigManager<'a> {
fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32> {
let ids_set: HashSet<i32> = HashSet::from_iter(matches_ids.iter().copied());
let match_set = self.active_match_set();
match_set
.matches
.iter()
.filter(|m| ids_set.contains(&m.id))
.map(|m| m.id)
.collect()
} }
} }
@ -80,3 +72,29 @@ fn to_app_properties(info: &AppInfo) -> AppProperties {
exec: info.exec.as_deref(), exec: info.exec.as_deref(),
} }
} }
impl<'a> MatchFilter for ConfigManager<'a> {
fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32> {
let ids_set: HashSet<i32> = HashSet::from_iter(matches_ids.iter().copied());
let (_, match_set) = self.active_context();
match_set
.matches
.iter()
.filter(|m| ids_set.contains(&m.id))
.map(|m| m.id)
.collect()
}
}
impl<'a> ConfigProvider<'a> for ConfigManager<'a> {
fn configs(&self) -> Vec<(&'a dyn Config, MatchSet)> {
self.config_store.configs().into_iter().map(|config| {
(config, self.match_store.query(config.match_paths()))
}).collect()
}
fn active(&self) -> (&'a dyn Config, MatchSet) {
self.active_context()
}
}

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 std::{collections::HashMap, iter::FromIterator};
use espanso_config::{
config::ConfigStore,
matches::{store::MatchStore, Match},
};
use super::{multiplex::MatchProvider, render::MatchIterator};
pub struct MatchCache<'a> {
cache: HashMap<i32, &'a Match>,
}
impl<'a> MatchCache<'a> {
pub fn load(config_store: &'a dyn ConfigStore, match_store: &'a dyn MatchStore) -> Self {
let mut cache = HashMap::new();
let paths = config_store.get_all_match_paths();
let global_set = match_store.query(&Vec::from_iter(paths.into_iter()));
for m in global_set.matches {
cache.insert(m.id, m);
}
Self { cache }
}
}
impl<'a> MatchProvider<'a> for MatchCache<'a> {
fn get(&self, match_id: i32) -> Option<&'a Match> {
self.cache.get(&match_id).map(|m| *m)
}
}
impl<'a> MatchIterator<'a> for MatchCache<'a> {
fn matches(&self) -> Vec<&'a Match> {
self.cache.iter().map(|(_, m)| *m).collect()
}
}

View File

@ -81,7 +81,7 @@ impl From<espanso_match::MatchResult<i32>> for MatchResult {
Self { Self {
id: result.id, id: result.id,
trigger: result.trigger, trigger: result.trigger,
vars: result.vars, args: result.vars,
} }
} }
} }

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 ui;
pub mod source;
pub mod render;
pub mod matcher;
pub mod executor;
pub mod multiplex;
pub mod match_cache;

View File

@ -0,0 +1,57 @@
/*
* 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 espanso_config::matches::{Match, MatchEffect};
use crate::engine::{
event::{render::RenderingRequestedEvent, Event},
process::Multiplexer,
};
pub trait MatchProvider<'a> {
fn get(&self, match_id: i32) -> Option<&'a Match>;
}
pub struct MultiplexAdapter<'a> {
provider: &'a dyn MatchProvider<'a>,
}
impl<'a> MultiplexAdapter<'a> {
pub fn new(provider: &'a dyn MatchProvider<'a>) -> Self {
Self { provider }
}
}
impl<'a> Multiplexer for MultiplexAdapter<'a> {
fn convert(&self, match_id: i32, trigger: String, trigger_args: HashMap<String, String>) -> Option<Event> {
let m = self.provider.get(match_id)?;
match &m.effect {
MatchEffect::Text(_) => Some(Event::RenderingRequested(RenderingRequestedEvent {
match_id,
trigger,
trigger_args,
})),
// TODO: think about rich text and image
MatchEffect::None => None,
}
}
}

View File

@ -0,0 +1,228 @@
/*
* 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, collections::HashMap, convert::TryInto};
use espanso_config::{
config::Config,
matches::{store::MatchSet, Match, MatchCause, MatchEffect},
};
use espanso_render::{CasingStyle, Context, RenderOptions, Template, Value, Variable};
use crate::{cli::worker::config::ConfigManager, engine::process::{Renderer, RendererError}};
pub trait MatchIterator<'a> {
fn matches(&self) -> Vec<&'a Match>;
}
pub trait ConfigProvider<'a> {
fn configs(&self) -> Vec<(&'a dyn Config, MatchSet)>;
fn active(&self) -> (&'a dyn Config, MatchSet);
}
pub struct RendererAdapter<'a> {
renderer: &'a dyn espanso_render::Renderer,
config_provider: &'a dyn ConfigProvider<'a>,
template_map: HashMap<i32, Option<Template>>,
global_vars_map: HashMap<i32, Variable>,
context_cache: RefCell<HashMap<i32, Context<'a>>>,
}
impl<'a> RendererAdapter<'a> {
pub fn new(
match_iterator: &'a dyn MatchIterator,
config_provider: &'a dyn ConfigProvider<'a>,
renderer: &'a dyn espanso_render::Renderer,
) -> Self {
let template_map = generate_template_map(match_iterator);
let global_vars_map = generate_global_vars_map(config_provider);
Self {
renderer,
config_provider,
template_map,
global_vars_map,
context_cache: RefCell::new(HashMap::new()),
}
}
}
// TODO: test
fn generate_template_map(match_iterator: &dyn MatchIterator) -> HashMap<i32, Option<Template>> {
let mut template_map = HashMap::new();
for m in match_iterator.matches() {
let entry = convert_to_template(m);
template_map.insert(m.id, entry);
}
template_map
}
// TODO: test
fn generate_global_vars_map(config_provider: &dyn ConfigProvider) -> HashMap<i32, Variable> {
let mut global_vars_map = HashMap::new();
for (_, match_set) in config_provider.configs() {
for var in match_set.global_vars.iter() {
if !global_vars_map.contains_key(&var.id) {
global_vars_map.insert(var.id, convert_var((*var).clone()));
}
}
}
global_vars_map
}
// TODO: test
fn generate_context<'a>(
match_set: &MatchSet,
template_map: &'a HashMap<i32, Option<Template>>,
global_vars_map: &'a HashMap<i32, Variable>,
) -> Context<'a> {
let mut templates = Vec::new();
let mut global_vars = Vec::new();
for m in match_set.matches.iter() {
if let Some(Some(template)) = template_map.get(&m.id) {
templates.push(template);
}
}
for var in match_set.global_vars.iter() {
if let Some(var) = global_vars_map.get(&var.id) {
global_vars.push(var);
}
}
Context {
templates,
global_vars
}
}
// TODO: move conversion methods to new file?
fn convert_to_template(m: &Match) -> Option<Template> {
if let MatchEffect::Text(text_effect) = &m.effect {
let ids = if let MatchCause::Trigger(cause) = &m.cause {
cause.triggers.clone()
} else {
Vec::new()
};
Some(Template {
ids,
body: text_effect.replace.clone(),
vars: convert_vars(text_effect.vars.clone()),
})
} else {
None
}
}
fn convert_vars(vars: Vec<espanso_config::matches::Variable>) -> Vec<espanso_render::Variable> {
vars.into_iter().map(convert_var).collect()
}
fn convert_var(var: espanso_config::matches::Variable) -> espanso_render::Variable {
Variable {
name: var.name,
var_type: var.var_type,
params: convert_params(var.params),
}
}
fn convert_params(params: espanso_config::matches::Params) -> espanso_render::Params {
let mut new_params = espanso_render::Params::new();
for (key, value) in params {
new_params.insert(key, convert_value(value));
}
new_params
}
fn convert_value(value: espanso_config::matches::Value) -> espanso_render::Value {
match value {
espanso_config::matches::Value::Null => espanso_render::Value::Null,
espanso_config::matches::Value::Bool(v) => espanso_render::Value::Bool(v),
espanso_config::matches::Value::Number(n) => match n {
espanso_config::matches::Number::Integer(i) => {
espanso_render::Value::Number(espanso_render::Number::Integer(i))
}
espanso_config::matches::Number::Float(f) => {
espanso_render::Value::Number(espanso_render::Number::Float(f.into_inner()))
}
},
espanso_config::matches::Value::String(s) => espanso_render::Value::String(s),
espanso_config::matches::Value::Array(v) => {
espanso_render::Value::Array(v.into_iter().map(convert_value).collect())
}
espanso_config::matches::Value::Object(params) => {
espanso_render::Value::Object(convert_params(params))
}
}
}
impl<'a> Renderer<'a> for RendererAdapter<'a> {
fn render(&'a self, match_id: i32, trigger_vars: HashMap<String, String>) -> anyhow::Result<String> {
if let Some(Some(template)) = self.template_map.get(&match_id) {
let (config, match_set) = self.config_provider.active();
let mut context_cache = self.context_cache.borrow_mut();
let context = context_cache.entry(config.id()).or_insert(generate_context(&match_set, &self.template_map, &self.global_vars_map));
// TODO: calculate the casing style instead of hardcoding it
let options = RenderOptions {
casing_style: CasingStyle::None,
};
// If some trigger vars are specified, augment the template with them
let augmented_template = if !trigger_vars.is_empty() {
let mut augmented = template.clone();
for (name, value) in trigger_vars {
let mut params = espanso_render::Params::new();
params.insert("echo".to_string(), Value::String(value));
augmented.vars.push(Variable {
name,
var_type: "echo".to_string(),
params,
})
}
Some(augmented)
} else {
None
};
let template = if let Some(augmented) = augmented_template.as_ref() {
augmented
} else {
template
};
match self.renderer.render(template, context, &options) {
espanso_render::RenderResult::Success(body) => Ok(body),
espanso_render::RenderResult::Aborted => Err(RendererError::Aborted.into()),
espanso_render::RenderResult::Error(err) => Err(RendererError::RenderingError(err).into()),
}
} else {
Err(RendererError::NotFound.into())
}
}
}

View File

@ -19,16 +19,13 @@
use funnel::Source; use funnel::Source;
use process::Matcher; use process::Matcher;
use ui::selector::MatchSelectorAdapter; use engine::ui::selector::MatchSelectorAdapter;
use crate::engine::{Engine, funnel, process, dispatch}; use crate::engine::{Engine, funnel, process, dispatch};
use super::{CliModule, CliModuleArgs}; use super::{CliModule, CliModuleArgs};
mod ui; mod engine;
mod config; mod config;
mod source;
mod matcher;
mod executor;
pub fn new() -> CliModule { pub fn new() -> CliModule {
#[allow(clippy::needless_update)] #[allow(clippy::needless_update)]
@ -48,21 +45,27 @@ fn worker_main(args: CliModuleArgs) {
let app_info_provider = espanso_info::get_provider().expect("unable to initialize app info provider"); let app_info_provider = espanso_info::get_provider().expect("unable to initialize app info provider");
let config_manager = config::ConfigManager::new(&*config_store, &*match_store, &*app_info_provider); let config_manager = config::ConfigManager::new(&*config_store, &*match_store, &*app_info_provider);
let match_converter = matcher::convert::MatchConverter::new(&*config_store, &*match_store); let match_converter = engine::matcher::convert::MatchConverter::new(&*config_store, &*match_store);
let match_cache = engine::match_cache::MatchCache::load(&*config_store, &*match_store);
let detect_source = source::detect::init_and_spawn().expect("failed to initialize detector module"); let detect_source = engine::source::detect::init_and_spawn().expect("failed to initialize detector module");
let sources: Vec<&dyn Source> = vec![&detect_source]; let sources: Vec<&dyn Source> = vec![&detect_source];
let funnel = funnel::default(&sources); let funnel = funnel::default(&sources);
let matcher = matcher::rolling::RollingMatcherAdapter::new(&match_converter.get_rolling_matches()); let matcher = engine::matcher::rolling::RollingMatcherAdapter::new(&match_converter.get_rolling_matches());
let matchers: Vec<&dyn Matcher<matcher::MatcherState>> = vec![&matcher]; let matchers: Vec<&dyn Matcher<engine::matcher::MatcherState>> = vec![&matcher];
let selector = MatchSelectorAdapter::new(); let selector = MatchSelectorAdapter::new();
let mut processor = process::default(&matchers, &config_manager, &selector); let multiplexer = engine::multiplex::MultiplexAdapter::new(&match_cache);
let text_injector = executor::text_injector::TextInjectorAdapter::new(); // TODO: add extensions
let renderer = espanso_render::create(Vec::new());
let renderer_adapter = engine::render::RendererAdapter::new(&match_cache, &config_manager, &renderer);
let mut processor = process::default(&matchers, &config_manager, &selector, &multiplexer, &renderer_adapter);
let text_injector = engine::executor::text_injector::TextInjectorAdapter::new();
let dispatcher = dispatch::default(&text_injector); let dispatcher = dispatch::default(&text_injector);
let mut engine = Engine::new(&funnel, &mut processor, &dispatcher); let mut engine = Engine::new(&funnel, &mut processor, &dispatcher);
engine.run(); engine.run();
} }

View File

@ -34,7 +34,7 @@ impl<'a> TextInjectExecutor<'a> {
impl<'a> Executor for TextInjectExecutor<'a> { impl<'a> Executor for TextInjectExecutor<'a> {
fn execute(&self, event: &Event) -> bool { fn execute(&self, event: &Event) -> bool {
if let Event::TextInject(inject_event) = event { if let Event::TextInject(inject_event) = event {
if inject_event.mode == TextInjectMode::Keys { if let Some(TextInjectMode::Keys) = inject_event.force_mode {
if let Err(error) = self.injector.inject(&inject_event.text) { if let Err(error) = self.injector.inject(&inject_event.text) {
error!("text injector reported an error: {:?}", error); error!("text injector reported an error: {:?}", error);
} }

View File

@ -18,10 +18,10 @@
*/ */
#[derive(Debug)] #[derive(Debug)]
pub struct TextInjectEvent { pub struct TextInjectRequest {
pub delete_count: i32, pub delete_count: i32,
pub text: String, pub text: String,
pub mode: TextInjectMode, pub force_mode: Option<TextInjectMode>,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]

View File

@ -28,7 +28,7 @@ pub struct MatchesDetectedEvent {
pub struct DetectedMatch { pub struct DetectedMatch {
pub id: i32, pub id: i32,
pub trigger: String, pub trigger: String,
pub vars: HashMap<String, String>, pub args: HashMap<String, String>,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]

View File

@ -20,10 +20,12 @@
pub mod keyboard; pub mod keyboard;
pub mod inject; pub mod inject;
pub mod matches; pub mod matches;
pub mod render;
#[derive(Debug)] #[derive(Debug)]
pub enum Event { pub enum Event {
NOOP, NOOP,
ProcessingError(String),
// Inputs // Inputs
Keyboard(keyboard::KeyboardEvent), Keyboard(keyboard::KeyboardEvent),
@ -32,6 +34,9 @@ pub enum Event {
MatchesDetected(matches::MatchesDetectedEvent), MatchesDetected(matches::MatchesDetectedEvent),
MatchSelected(matches::MatchSelectedEvent), MatchSelected(matches::MatchSelectedEvent),
RenderingRequested(render::RenderingRequestedEvent),
Rendered(render::RenderedEvent),
// Effects // Effects
TextInject(inject::TextInjectEvent), TextInject(inject::TextInjectRequest),
} }

View File

@ -0,0 +1,33 @@
/*
* 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 RenderingRequestedEvent {
pub match_id: i32,
pub trigger: String,
pub trigger_args: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RenderedEvent {
pub trigger: String,
pub body: String,
}

View File

@ -19,7 +19,13 @@
use log::trace; use log::trace;
use super::{Event, MatchFilter, MatchSelector, Matcher, Middleware, Processor, middleware::match_select::MatchSelectMiddleware, middleware::matcher::MatchMiddleware}; use super::{
middleware::{
match_select::MatchSelectMiddleware, matcher::MatchMiddleware, multiplex::MultiplexMiddleware,
render::RenderMiddleware,
},
Event, MatchFilter, MatchSelector, Matcher, Middleware, Multiplexer, Processor, Renderer,
};
use std::collections::VecDeque; use std::collections::VecDeque;
pub struct DefaultProcessor<'a> { pub struct DefaultProcessor<'a> {
@ -32,12 +38,16 @@ impl<'a> DefaultProcessor<'a> {
matchers: &'a [&'a dyn Matcher<'a, MatcherState>], matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
match_filter: &'a dyn MatchFilter, match_filter: &'a dyn MatchFilter,
match_selector: &'a dyn MatchSelector, match_selector: &'a dyn MatchSelector,
multiplexer: &'a dyn Multiplexer,
renderer: &'a dyn Renderer<'a>,
) -> DefaultProcessor<'a> { ) -> DefaultProcessor<'a> {
Self { Self {
event_queue: VecDeque::new(), event_queue: VecDeque::new(),
middleware: vec![ middleware: vec![
Box::new(MatchMiddleware::new(matchers)), Box::new(MatchMiddleware::new(matchers)),
Box::new(MatchSelectMiddleware::new(match_filter, match_selector)), Box::new(MatchSelectMiddleware::new(match_filter, match_selector)),
Box::new(MultiplexMiddleware::new(multiplexer)),
Box::new(RenderMiddleware::new(renderer)),
], ],
} }
} }

View File

@ -91,7 +91,7 @@ impl<'a, State> Middleware for MatchMiddleware<'a, State> {
DetectedMatch { DetectedMatch {
id: result.id, id: result.id,
trigger: result.trigger, trigger: result.trigger,
vars: result.vars, args: result.args,
} }
}).collect() }).collect()
}) })

View File

@ -17,5 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
pub mod render;
pub mod matcher; pub mod matcher;
pub mod multiplex;
pub mod match_select; pub mod match_select;

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, error};
use super::super::Middleware;
use crate::engine::{event::{Event, inject::{TextInjectRequest, TextInjectMode}, matches::MatchSelectedEvent}, process::{MatchFilter, MatchSelector, Multiplexer}};
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 next(&self, event: Event, _: &dyn FnMut(Event)) -> Event {
if let Event::MatchSelected(m_event) = event {
return match self.multiplexer.convert(m_event.chosen.id, m_event.chosen.trigger, m_event.chosen.args) {
Some(event) => event,
None => {
error!("match multiplexing failed");
Event::NOOP
},
}
}
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 log::{debug, error};
use super::super::Middleware;
use crate::engine::{event::{Event, inject::{TextInjectRequest, TextInjectMode}, matches::MatchSelectedEvent, render::RenderedEvent}, process::{MatchFilter, MatchSelector, Renderer, RendererError}};
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 next(&self, event: Event, _: &dyn FnMut(Event)) -> Event {
if let Event::RenderingRequested(m_event) = event {
match self.renderer.render(m_event.match_id, m_event.trigger_args) {
Ok(body) => {
return Event::Rendered(RenderedEvent {
trigger: m_event.trigger,
body,
})
}
Err(err) => {
match err.downcast_ref::<RendererError>() {
Some(RendererError::Aborted) => {
return Event::NOOP
}
_ => {
error!("error during rendering: {}", err);
return Event::ProcessingError("An error has occurred during rendering, please examine the logs or contact support.".to_string())
}
}
}
}
}
event
}
}
// TODO: test

View File

@ -17,9 +17,10 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::collections::HashMap;
use super::{event::keyboard::Key, Event}; use super::{event::keyboard::Key, Event};
use anyhow::Result;
use std::collections::HashMap;
use thiserror::Error;
mod default; mod default;
mod middleware; mod middleware;
@ -52,7 +53,7 @@ pub enum MatcherEvent {
pub struct MatchResult { pub struct MatchResult {
pub id: i32, pub id: i32,
pub trigger: String, pub trigger: String,
pub vars: HashMap<String, String>, pub args: HashMap<String, String>,
} }
pub trait MatchFilter { pub trait MatchFilter {
@ -63,10 +64,32 @@ pub trait MatchSelector {
fn select(&self, matches_ids: &[i32]) -> Option<i32>; fn select(&self, matches_ids: &[i32]) -> Option<i32>;
} }
pub trait Multiplexer {
fn convert(&self, match_id: i32, trigger: String, trigger_args: HashMap<String, String>) -> Option<Event>;
}
pub trait Renderer<'a> {
fn render(&'a self, match_id: i32, 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 fn default<'a, MatcherState>( pub fn default<'a, MatcherState>(
matchers: &'a [&'a dyn Matcher<'a, MatcherState>], matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
match_filter: &'a dyn MatchFilter, match_filter: &'a dyn MatchFilter,
match_selector: &'a dyn MatchSelector, match_selector: &'a dyn MatchSelector,
multiplexer: &'a dyn Multiplexer,
renderer: &'a dyn Renderer<'a>,
) -> impl Processor + 'a { ) -> impl Processor + 'a {
default::DefaultProcessor::new(matchers, match_filter, match_selector) default::DefaultProcessor::new(matchers, match_filter, match_selector, multiplexer, renderer)
} }