From 48d05a3f3292d2d68778e51229820fb06e8c26ac Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 31 Jul 2021 21:19:50 +0200 Subject: [PATCH] feat(core): first draft of builtin matches --- espanso/src/cli/worker/builtin/debug.rs | 38 ++++++++ espanso/src/cli/worker/builtin/mod.rs | 57 ++++++++++++ espanso/src/cli/worker/config.rs | 18 +++- espanso/src/cli/worker/context/default.rs | 43 +++++++++ espanso/src/cli/worker/context/mod.rs | 32 +++++++ espanso/src/cli/worker/engine/mod.rs | 18 ++-- .../engine/process/middleware/match_select.rs | 16 ++-- .../process/middleware/matcher/convert.rs | 27 +++++- .../engine/process/middleware/multiplex.rs | 63 ++++++++----- espanso/src/cli/worker/match_cache.rs | 93 ++++++++++++++++--- espanso/src/cli/worker/mod.rs | 2 + 11 files changed, 348 insertions(+), 59 deletions(-) create mode 100644 espanso/src/cli/worker/builtin/debug.rs create mode 100644 espanso/src/cli/worker/builtin/mod.rs create mode 100644 espanso/src/cli/worker/context/default.rs create mode 100644 espanso/src/cli/worker/context/mod.rs diff --git a/espanso/src/cli/worker/builtin/debug.rs b/espanso/src/cli/worker/builtin/debug.rs new file mode 100644 index 0000000..eecf128 --- /dev/null +++ b/espanso/src/cli/worker/builtin/debug.rs @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +use crate::{cli::worker::builtin::generate_next_builtin_id, engine::event::{EventType, effect::TextInjectRequest}}; + +use super::BuiltInMatch; + +pub fn create_match_show_active_config_info() -> BuiltInMatch { + BuiltInMatch { + id: generate_next_builtin_id(), + label: "Display active config information", + triggers: vec!["#acfg#".to_string()], + action: |context| { + println!("active config: {:?}", context.get_active_config().label()); + + EventType::TextInject(TextInjectRequest { + text: "test".to_string(), + force_mode: None, + }) + }, + } +} \ No newline at end of file diff --git a/espanso/src/cli/worker/builtin/mod.rs b/espanso/src/cli/worker/builtin/mod.rs new file mode 100644 index 0000000..05c7c5c --- /dev/null +++ b/espanso/src/cli/worker/builtin/mod.rs @@ -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 . + */ + +use std::cell::Cell; + +use crate::engine::event::EventType; + +use super::context::Context; + +mod debug; + +const MIN_BUILTIN_MATCH_ID: i32 = 1_000_000_000; + +pub struct BuiltInMatch { + pub id: i32, + pub label: &'static str, + pub triggers: Vec, + pub action: fn(context: &dyn Context) -> EventType, +} + +pub fn get_builtin_matches() -> Vec { + vec![ + debug::create_match_show_active_config_info(), + ] +} + +pub fn is_builtin_match(id: i32) -> bool { + id >= MIN_BUILTIN_MATCH_ID +} + +thread_local! { + static CURRENT_BUILTIN_MATCH_ID: Cell = Cell::new(MIN_BUILTIN_MATCH_ID); +} + +fn generate_next_builtin_id() -> i32 { + CURRENT_BUILTIN_MATCH_ID.with(|value| { + let current = value.get(); + value.set(current + 1); + current + }) +} diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs index d9625c2..e9ea90d 100644 --- a/espanso/src/cli/worker/config.rs +++ b/espanso/src/cli/worker/config.rs @@ -26,6 +26,8 @@ use espanso_config::{ use espanso_info::{AppInfo, AppInfoProvider}; use std::iter::FromIterator; +use super::builtin::is_builtin_match; + pub struct ConfigManager<'a> { config_store: &'a dyn ConfigStore, match_store: &'a dyn MatchStore, @@ -57,7 +59,7 @@ impl<'a> ConfigManager<'a> { (config.clone(), self.match_store.query(&match_paths)) } - pub fn default(&self) -> Arc{ + pub fn default(&self) -> Arc { self.config_store.default() } } @@ -76,12 +78,22 @@ impl<'a> crate::engine::process::MatchFilter for ConfigManager<'a> { let ids_set: HashSet = HashSet::from_iter(matches_ids.iter().copied()); let (_, match_set) = self.active_context(); - match_set + let active_user_defined_matches: Vec = match_set .matches .iter() .filter(|m| ids_set.contains(&m.id)) .map(|m| m.id) - .collect() + .collect(); + + let builtin_matches: Vec = matches_ids + .into_iter() + .filter(|id| is_builtin_match(**id)) + .map(|id| *id) + .collect(); + + let mut output = active_user_defined_matches; + output.extend(builtin_matches); + output } } diff --git a/espanso/src/cli/worker/context/default.rs b/espanso/src/cli/worker/context/default.rs new file mode 100644 index 0000000..7e09be9 --- /dev/null +++ b/espanso/src/cli/worker/context/default.rs @@ -0,0 +1,43 @@ +/* + * 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 . + */ + +use super::*; +use crate::cli::worker::config::ConfigManager; + +pub struct DefaultContext<'a> { + config_manager: &'a ConfigManager<'a>, +} + +impl<'a> DefaultContext<'a> { + pub fn new(config_manager: &'a ConfigManager<'a>) -> Self { + Self { config_manager } + } +} + +impl<'a> Context for DefaultContext<'a> {} + +impl<'a> ConfigContext for DefaultContext<'a> { + fn get_default_config(&self) -> Arc { + self.config_manager.default() + } + + fn get_active_config(&self) -> Arc { + self.config_manager.active() + } +} diff --git a/espanso/src/cli/worker/context/mod.rs b/espanso/src/cli/worker/context/mod.rs new file mode 100644 index 0000000..41eefde --- /dev/null +++ b/espanso/src/cli/worker/context/mod.rs @@ -0,0 +1,32 @@ +/* + * 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 . + */ + +use std::sync::Arc; + +use espanso_config::config::Config; + +mod default; +pub use default::DefaultContext; + +pub trait Context: ConfigContext {} + +pub trait ConfigContext { + fn get_default_config(&self) -> Arc; + fn get_active_config(&self) -> Arc; +} \ No newline at end of file diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index c9a4305..f4c9a4a 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -28,8 +28,7 @@ use espanso_path::Paths; use espanso_ui::{event::UIEvent, UIRemote}; use log::{debug, error, info, warn}; -use crate::{cli::worker::{ - engine::{ +use crate::{cli::worker::{context::Context, engine::{ dispatch::executor::{ clipboard_injector::ClipboardInjectorAdapter, context_menu::ContextMenuHandlerAdapter, event_injector::EventInjectorAdapter, icon::IconHandlerAdapter, @@ -49,9 +48,7 @@ use crate::{cli::worker::{ RendererAdapter, }, }, - }, - match_cache::MatchCache, - }, engine::event::ExitMode, preferences::Preferences}; + }, match_cache::{CombinedMatchCache, MatchCache}}, engine::event::ExitMode, preferences::Preferences}; use super::secure_input::SecureInputEvent; @@ -82,13 +79,18 @@ pub fn initialize_and_spawn( espanso_info::get_provider().expect("unable to initialize app info provider"); let config_manager = super::config::ConfigManager::new(&*config_store, &*match_store, &*app_info_provider); - let match_converter = MatchConverter::new(&*config_store, &*match_store); let match_cache = MatchCache::load(&*config_store, &*match_store); let modulo_manager = crate::gui::modulo::manager::ModuloManager::new(); let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager); let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager); + let context: Box = Box::new(super::context::DefaultContext::new(&config_manager)); + let builtin_matches = super::builtin::get_builtin_matches(); + let combined_match_cache = CombinedMatchCache::load(&match_cache, &builtin_matches); + + let match_converter = MatchConverter::new(&*config_store, &*match_store, &builtin_matches); + let has_granted_capabilities = grant_linux_capabilities(use_evdev_backend); // TODO: pass all the options @@ -129,8 +131,8 @@ pub fn initialize_and_spawn( super::engine::process::middleware::matcher::MatcherState, >, > = vec![&rolling_matcher, ®ex_matcher]; - let selector = MatchSelectorAdapter::new(&modulo_search_ui, &match_cache); - let multiplexer = MultiplexAdapter::new(&match_cache); + let selector = MatchSelectorAdapter::new(&modulo_search_ui, &combined_match_cache); + let multiplexer = MultiplexAdapter::new(&combined_match_cache, &*context); let injector = espanso_inject::get_injector(InjectorCreationOptions { use_evdev: use_evdev_backend, diff --git a/espanso/src/cli/worker/engine/process/middleware/match_select.rs b/espanso/src/cli/worker/engine/process/middleware/match_select.rs index d39af6c..fd65be5 100644 --- a/espanso/src/cli/worker/engine/process/middleware/match_select.rs +++ b/espanso/src/cli/worker/engine/process/middleware/match_select.rs @@ -17,7 +17,6 @@ * along with espanso. If not, see . */ -use espanso_config::matches::{Match}; use log::error; use crate::{ @@ -28,7 +27,13 @@ use crate::{ const MAX_LABEL_LEN: usize = 100; pub trait MatchProvider<'a> { - fn get_matches(&self, ids: &[i32]) -> Vec<&'a Match>; + fn get_matches(&self, ids: &[i32]) -> Vec>; +} + +pub struct MatchSummary<'a> { + pub id: i32, + pub label: &'a str, + pub tag: Option<&'a str>, } pub struct MatchSelectorAdapter<'a> { @@ -49,15 +54,14 @@ impl<'a> MatchSelector for MatchSelectorAdapter<'a> { fn select(&self, matches_ids: &[i32]) -> Option { let matches = self.match_provider.get_matches(&matches_ids); let search_items: Vec = matches - .iter() + .into_iter() .map(|m| { - let label = m.description(); - let clipped_label = &label[..std::cmp::min(label.len(), MAX_LABEL_LEN)]; + let clipped_label = &m.label[..std::cmp::min(m.label.len(), MAX_LABEL_LEN)]; SearchItem { id: m.id.to_string(), label: clipped_label.to_string(), - tag: m.cause_description().map(String::from), + tag: m.tag.map(String::from), } }) .collect(); diff --git a/espanso/src/cli/worker/engine/process/middleware/matcher/convert.rs b/espanso/src/cli/worker/engine/process/middleware/matcher/convert.rs index 61039b2..24bad2b 100644 --- a/espanso/src/cli/worker/engine/process/middleware/matcher/convert.rs +++ b/espanso/src/cli/worker/engine/process/middleware/matcher/convert.rs @@ -30,16 +30,24 @@ use espanso_match::{ }; use std::iter::FromIterator; +use crate::cli::worker::builtin::BuiltInMatch; + pub struct MatchConverter<'a> { config_store: &'a dyn ConfigStore, match_store: &'a dyn MatchStore, + builtin_matches: &'a [BuiltInMatch], } impl<'a> MatchConverter<'a> { - pub fn new(config_store: &'a dyn ConfigStore, match_store: &'a dyn MatchStore) -> Self { + pub fn new( + config_store: &'a dyn ConfigStore, + match_store: &'a dyn MatchStore, + builtin_matches: &'a [BuiltInMatch], + ) -> Self { Self { config_store, match_store, + builtin_matches, } } @@ -48,6 +56,7 @@ impl<'a> MatchConverter<'a> { let match_set = self.global_match_set(); let mut matches = Vec::new(); + // First convert configuration (user-defined) matches for m in match_set.matches { if let MatchCause::Trigger(cause) = &m.cause { for trigger in cause.triggers.iter() { @@ -64,6 +73,17 @@ impl<'a> MatchConverter<'a> { } } + // Then convert built-in ones + for m in self.builtin_matches { + for trigger in m.triggers.iter() { + matches.push(RollingMatch::from_string( + m.id, + &trigger, + &StringMatchOptions::default() + )) + } + } + matches } @@ -74,10 +94,7 @@ impl<'a> MatchConverter<'a> { for m in match_set.matches { if let MatchCause::Regex(cause) = &m.cause { - matches.push(RegexMatch::new( - m.id, - &cause.regex, - )) + matches.push(RegexMatch::new(m.id, &cause.regex)) } } diff --git a/espanso/src/cli/worker/engine/process/middleware/multiplex.rs b/espanso/src/cli/worker/engine/process/middleware/multiplex.rs index b0db6e2..2c73a8d 100644 --- a/espanso/src/cli/worker/engine/process/middleware/multiplex.rs +++ b/espanso/src/cli/worker/engine/process/middleware/multiplex.rs @@ -19,40 +19,59 @@ use espanso_config::matches::{Match, MatchEffect}; -use crate::engine::{event::{EventType, internal::DetectedMatch, internal::{ImageRequestedEvent, RenderingRequestedEvent, TextFormat}}, process::Multiplexer}; +use crate::{ + cli::worker::{builtin::BuiltInMatch, context::Context}, + engine::{ + event::{ + internal::DetectedMatch, + internal::{ImageRequestedEvent, RenderingRequestedEvent, TextFormat}, + EventType, + }, + process::Multiplexer, + }, +}; pub trait MatchProvider<'a> { - fn get(&self, match_id: i32) -> Option<&'a Match>; + fn get(&self, match_id: i32) -> Option>; +} + +pub enum MatchResult<'a> { + User(&'a Match), + Builtin(&'a BuiltInMatch), } pub struct MultiplexAdapter<'a> { provider: &'a dyn MatchProvider<'a>, + context: &'a dyn Context, } impl<'a> MultiplexAdapter<'a> { - pub fn new(provider: &'a dyn MatchProvider<'a>) -> Self { - Self { provider } + pub fn new(provider: &'a dyn MatchProvider<'a>, context: &'a dyn Context) -> Self { + Self { provider, context } } } impl<'a> Multiplexer for MultiplexAdapter<'a> { fn convert(&self, detected_match: DetectedMatch) -> Option { - let m = self.provider.get(detected_match.id)?; - - match &m.effect { - MatchEffect::Text(effect) => Some(EventType::RenderingRequested(RenderingRequestedEvent { - match_id: detected_match.id, - trigger: detected_match.trigger, - left_separator: detected_match.left_separator, - right_separator: detected_match.right_separator, - trigger_args: detected_match.args, - format: convert_format(&effect.format), - })), - MatchEffect::Image(effect) => Some(EventType::ImageRequested(ImageRequestedEvent { - match_id: detected_match.id, - image_path: effect.path.clone(), - })), - MatchEffect::None => None, + match self.provider.get(detected_match.id)? { + MatchResult::User(m) => match &m.effect { + MatchEffect::Text(effect) => Some(EventType::RenderingRequested(RenderingRequestedEvent { + match_id: detected_match.id, + trigger: detected_match.trigger, + left_separator: detected_match.left_separator, + right_separator: detected_match.right_separator, + trigger_args: detected_match.args, + format: convert_format(&effect.format), + })), + MatchEffect::Image(effect) => Some(EventType::ImageRequested(ImageRequestedEvent { + match_id: detected_match.id, + image_path: effect.path.clone(), + })), + MatchEffect::None => None, + }, + MatchResult::Builtin(m) => { + Some((m.action)(self.context)) + } } } } @@ -60,7 +79,7 @@ impl<'a> Multiplexer for MultiplexAdapter<'a> { fn convert_format(format: &espanso_config::matches::TextFormat) -> TextFormat { match format { espanso_config::matches::TextFormat::Plain => TextFormat::Plain, - espanso_config::matches::TextFormat::Markdown => TextFormat::Markdown, - espanso_config::matches::TextFormat::Html => TextFormat::Html, + espanso_config::matches::TextFormat::Markdown => TextFormat::Markdown, + espanso_config::matches::TextFormat::Html => TextFormat::Html, } } diff --git a/espanso/src/cli/worker/match_cache.rs b/espanso/src/cli/worker/match_cache.rs index 58d681d..4ce7d85 100644 --- a/espanso/src/cli/worker/match_cache.rs +++ b/espanso/src/cli/worker/match_cache.rs @@ -24,6 +24,8 @@ use espanso_config::{ matches::{store::MatchStore, Match, MatchEffect}, }; +use super::{builtin::BuiltInMatch, engine::process::middleware::match_select::MatchSummary}; + pub struct MatchCache<'a> { cache: HashMap, } @@ -43,12 +45,6 @@ impl<'a> MatchCache<'a> { } } -impl<'a> super::engine::process::middleware::multiplex::MatchProvider<'a> for MatchCache<'a> { - fn get(&self, match_id: i32) -> Option<&'a Match> { - self.cache.get(&match_id).map(|m| *m) - } -} - impl<'a> super::engine::process::middleware::render::MatchProvider<'a> for MatchCache<'a> { fn matches(&self) -> Vec<&'a Match> { self.cache.iter().map(|(_, m)| *m).collect() @@ -59,15 +55,6 @@ impl<'a> super::engine::process::middleware::render::MatchProvider<'a> for Match } } -impl<'a> super::engine::process::middleware::match_select::MatchProvider<'a> for MatchCache<'a> { - fn get_matches(&self, ids: &[i32]) -> Vec<&'a Match> { - ids - .iter() - .flat_map(|id| self.cache.get(&id).map(|m| *m)) - .collect() - } -} - impl<'a> crate::engine::process::MatchInfoProvider for MatchCache<'a> { fn get_force_mode(&self, match_id: i32) -> Option { let m = self.cache.get(&match_id)?; @@ -87,3 +74,79 @@ impl<'a> crate::engine::process::MatchInfoProvider for MatchCache<'a> { None } } + +pub struct CombinedMatchCache<'a> { + user_match_cache: &'a MatchCache<'a>, + builtin_match_cache: HashMap, +} + +pub enum MatchVariant<'a> { + User(&'a Match), + Builtin(&'a BuiltInMatch), +} + +impl<'a> CombinedMatchCache<'a> { + pub fn load(user_match_cache: &'a MatchCache<'a>, builtin_matches: &'a [BuiltInMatch]) -> Self { + let mut builtin_match_cache = HashMap::new(); + + for m in builtin_matches { + builtin_match_cache.insert(m.id, m); + } + + Self { + builtin_match_cache, + user_match_cache, + } + } + + pub fn get(&self, match_id: i32) -> Option> { + if let Some(user_match) = self.user_match_cache.cache.get(&match_id).map(|m| *m) { + return Some(MatchVariant::User(user_match)); + } + + if let Some(builtin_match) = self.builtin_match_cache.get(&match_id).map(|m| *m) { + return Some(MatchVariant::Builtin(builtin_match)); + } + + None + } +} + +impl<'a> super::engine::process::middleware::match_select::MatchProvider<'a> + for CombinedMatchCache<'a> +{ + fn get_matches(&self, ids: &[i32]) -> Vec> { + ids + .iter() + .filter_map(|id| self.get(*id)) + .map(|m| match m { + MatchVariant::User(m) => MatchSummary { + id: m.id, + label: m.description(), + tag: m.cause_description(), + }, + MatchVariant::Builtin(m) => MatchSummary { + id: m.id, + label: m.label, + tag: m.triggers.first().map(String::as_ref), + }, + }) + .collect() + } +} + +impl<'a> super::engine::process::middleware::multiplex::MatchProvider<'a> + for CombinedMatchCache<'a> +{ + fn get( + &self, + match_id: i32, + ) -> Option> { + Some(match self.get(match_id)? { + MatchVariant::User(m) => super::engine::process::middleware::multiplex::MatchResult::User(m), + MatchVariant::Builtin(m) => { + super::engine::process::middleware::multiplex::MatchResult::Builtin(m) + } + }) + } +} diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs index ce7f6aa..efee5d9 100644 --- a/espanso/src/cli/worker/mod.rs +++ b/espanso/src/cli/worker/mod.rs @@ -33,7 +33,9 @@ use self::ui::util::convert_icon_paths_to_tray_vec; use super::{CliModule, CliModuleArgs}; +mod builtin; mod config; +mod context; mod daemon_monitor; mod engine; mod ipc;