diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs new file mode 100644 index 0000000..84b85ca --- /dev/null +++ b/espanso/src/cli/worker/config.rs @@ -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 . + */ + +use std::collections::HashSet; + +use crate::engine::process::MatchFilter; +use espanso_config::{ + config::{AppProperties, Config, ConfigStore}, + matches::store::{MatchSet, MatchStore}, +}; +use espanso_info::{AppInfo, AppInfoProvider}; +use std::iter::FromIterator; + +pub struct ConfigManager<'a> { + config_store: &'a dyn ConfigStore, + match_store: &'a dyn MatchStore, + app_info_provider: &'a dyn AppInfoProvider, +} + +impl<'a> ConfigManager<'a> { + pub fn new( + config_store: &'a dyn ConfigStore, + match_store: &'a dyn MatchStore, + app_info_provider: &'a dyn AppInfoProvider, + ) -> Self { + Self { + config_store, + match_store, + app_info_provider, + } + } + + fn active(&self) -> &'a dyn Config { + let current_app = self.app_info_provider.get_info(); + let info = to_app_properties(¤t_app); + self.config_store.active(&info) + } + + fn active_match_set(&self) -> MatchSet { + let match_paths = self.active().match_paths(); + self.match_store.query(&match_paths) + } +} + +impl<'a> MatchFilter for ConfigManager<'a> { + fn filter_active(&self, matches_ids: &[i32]) -> Vec { + let ids_set: HashSet = 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() + } +} + +// TODO: test +fn to_app_properties(info: &AppInfo) -> AppProperties { + AppProperties { + title: info.title.as_deref(), + class: info.class.as_deref(), + exec: info.exec.as_deref(), + } +} diff --git a/espanso/src/cli/worker/matcher/convert.rs b/espanso/src/cli/worker/matcher/convert.rs new file mode 100644 index 0000000..ffe52bf --- /dev/null +++ b/espanso/src/cli/worker/matcher/convert.rs @@ -0,0 +1,72 @@ +/* + * 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 espanso_config::{ + config::ConfigStore, + matches::{ + store::{MatchSet, MatchStore}, + MatchCause, + }, +}; +use espanso_match::rolling::{RollingMatch, StringMatchOptions}; +use std::iter::FromIterator; + +pub struct MatchConverter<'a> { + config_store: &'a dyn ConfigStore, + match_store: &'a dyn MatchStore, +} + +impl<'a> MatchConverter<'a> { + pub fn new(config_store: &'a dyn ConfigStore, match_store: &'a dyn MatchStore) -> Self { + Self { + config_store, + match_store, + } + } + + // TODO: test (might need to move the conversion logic into a separate function) + pub fn get_rolling_matches(&self) -> Vec> { + let match_set = self.global_match_set(); + let mut matches = Vec::new(); + + for m in match_set.matches { + if let MatchCause::Trigger(cause) = &m.cause { + for trigger in cause.triggers.iter() { + matches.push(RollingMatch::from_string( + m.id, + &trigger, + &StringMatchOptions { + case_insensitive: cause.propagate_case, + preserve_case_markers: cause.propagate_case, + left_word: cause.left_word, + right_word: cause.right_word, + }, + )) + } + } + } + + matches + } + + fn global_match_set(&self) -> MatchSet { + let paths = self.config_store.get_all_match_paths(); + self.match_store.query(&Vec::from_iter(paths.into_iter())) + } +} diff --git a/espanso/src/cli/worker/matcher/mod.rs b/espanso/src/cli/worker/matcher/mod.rs index 3e30275..ac109b6 100644 --- a/espanso/src/cli/worker/matcher/mod.rs +++ b/espanso/src/cli/worker/matcher/mod.rs @@ -22,6 +22,7 @@ use espanso_match::rolling::matcher::RollingMatcherState; use enum_as_inner::EnumAsInner; pub mod rolling; +pub mod convert; #[derive(Clone, EnumAsInner)] pub enum MatcherState<'a> { diff --git a/espanso/src/cli/worker/matcher/rolling.rs b/espanso/src/cli/worker/matcher/rolling.rs index b11fe58..4719308 100644 --- a/espanso/src/cli/worker/matcher/rolling.rs +++ b/espanso/src/cli/worker/matcher/rolling.rs @@ -31,14 +31,8 @@ pub struct RollingMatcherAdapter { } impl RollingMatcherAdapter { - pub fn new() -> Self { - // TODO: pass actual matches - let matches = vec![ - RollingMatch::from_string(1, "esp", &Default::default()), - RollingMatch::from_string(2, "test", &Default::default()), - ]; - - let matcher = RollingMatcher::new(&matches, Default::default()); + pub fn new(matches: &[RollingMatch]) -> Self { + let matcher = RollingMatcher::new(matches, Default::default()); Self { matcher } } diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs index 037deb2..b740cc6 100644 --- a/espanso/src/cli/worker/mod.rs +++ b/espanso/src/cli/worker/mod.rs @@ -19,10 +19,13 @@ use funnel::Source; use process::Matcher; +use ui::selector::MatchSelectorAdapter; use crate::engine::{Engine, funnel, process, dispatch}; use super::{CliModule, CliModuleArgs}; +mod ui; +mod config; mod source; mod matcher; mod executor; @@ -40,13 +43,21 @@ pub fn new() -> CliModule { } fn worker_main(args: CliModuleArgs) { - let detect_source = source::detect::init_and_spawn().unwrap(); // TODO: handle error + let config_store = args.config_store.expect("missing config store in worker main"); + let match_store = args.match_store.expect("missing match store in worker main"); + + 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 match_converter = matcher::convert::MatchConverter::new(&*config_store, &*match_store); + + let detect_source = source::detect::init_and_spawn().expect("failed to initialize detector module"); let sources: Vec<&dyn Source> = vec![&detect_source]; let funnel = funnel::default(&sources); - let matcher = matcher::rolling::RollingMatcherAdapter::new(); + let matcher = matcher::rolling::RollingMatcherAdapter::new(&match_converter.get_rolling_matches()); let matchers: Vec<&dyn Matcher> = vec![&matcher]; - let mut processor = process::default(&matchers); + let selector = MatchSelectorAdapter::new(); + let mut processor = process::default(&matchers, &config_manager, &selector); let text_injector = executor::text_injector::TextInjectorAdapter::new(); let dispatcher = dispatch::default(&text_injector); diff --git a/espanso/src/cli/worker/ui/mod.rs b/espanso/src/cli/worker/ui/mod.rs new file mode 100644 index 0000000..441c687 --- /dev/null +++ b/espanso/src/cli/worker/ui/mod.rs @@ -0,0 +1,20 @@ +/* + * 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 . + */ + +pub mod selector; \ No newline at end of file diff --git a/espanso/src/cli/worker/ui/selector.rs b/espanso/src/cli/worker/ui/selector.rs new file mode 100644 index 0000000..d68995d --- /dev/null +++ b/espanso/src/cli/worker/ui/selector.rs @@ -0,0 +1,37 @@ +/* + * 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::engine::process::MatchSelector; + +pub struct MatchSelectorAdapter { + // TODO: pass Modulo search UI manager +} + +impl MatchSelectorAdapter { + pub fn new() -> Self { + Self {} + } +} + +impl MatchSelector for MatchSelectorAdapter { + fn select(&self, matches_ids: &[i32]) -> Option { + // TODO: replace with actual selection + Some(*matches_ids.first().unwrap()) + } +} diff --git a/espanso/src/engine/event/inject.rs b/espanso/src/engine/event/inject.rs index de2b254..683cf3f 100644 --- a/espanso/src/engine/event/inject.rs +++ b/espanso/src/engine/event/inject.rs @@ -19,6 +19,7 @@ #[derive(Debug)] pub struct TextInjectEvent { + pub delete_count: i32, pub text: String, pub mode: TextInjectMode, } diff --git a/espanso/src/engine/event/matches_detected.rs b/espanso/src/engine/event/matches.rs similarity index 85% rename from espanso/src/engine/event/matches_detected.rs rename to espanso/src/engine/event/matches.rs index cb9e331..74f8a95 100644 --- a/espanso/src/engine/event/matches_detected.rs +++ b/espanso/src/engine/event/matches.rs @@ -21,12 +21,17 @@ use std::collections::HashMap; #[derive(Debug, Clone, PartialEq)] pub struct MatchesDetectedEvent { - pub results: Vec, + pub matches: Vec, } #[derive(Debug, Clone, PartialEq)] -pub struct MatchResult { +pub struct DetectedMatch { pub id: i32, pub trigger: String, pub vars: HashMap, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MatchSelectedEvent { + pub chosen: DetectedMatch, } \ No newline at end of file diff --git a/espanso/src/engine/event/mod.rs b/espanso/src/engine/event/mod.rs index ecb1f86..c357e87 100644 --- a/espanso/src/engine/event/mod.rs +++ b/espanso/src/engine/event/mod.rs @@ -19,7 +19,7 @@ pub mod keyboard; pub mod inject; -pub mod matches_detected; +pub mod matches; #[derive(Debug)] pub enum Event { @@ -29,7 +29,8 @@ pub enum Event { Keyboard(keyboard::KeyboardEvent), // Internal - MatchesDetected(matches_detected::MatchesDetectedEvent), + MatchesDetected(matches::MatchesDetectedEvent), + MatchSelected(matches::MatchSelectedEvent), // Effects TextInject(inject::TextInjectEvent), diff --git a/espanso/src/engine/process/default.rs b/espanso/src/engine/process/default.rs index 2eea714..17ba379 100644 --- a/espanso/src/engine/process/default.rs +++ b/espanso/src/engine/process/default.rs @@ -19,7 +19,7 @@ use log::trace; -use super::{Event, Matcher, Middleware, Processor, middleware::matcher::MatchMiddleware}; +use super::{Event, MatchFilter, MatchSelector, Matcher, Middleware, Processor, middleware::match_select::MatchSelectMiddleware, middleware::matcher::MatchMiddleware}; use std::collections::VecDeque; pub struct DefaultProcessor<'a> { @@ -27,20 +27,25 @@ pub struct DefaultProcessor<'a> { middleware: Vec>, } -impl <'a> DefaultProcessor<'a> { - pub fn new(matchers: &'a [&'a dyn Matcher<'a, MatcherState>]) -> DefaultProcessor<'a> { +impl<'a> DefaultProcessor<'a> { + pub fn new( + matchers: &'a [&'a dyn Matcher<'a, MatcherState>], + match_filter: &'a dyn MatchFilter, + match_selector: &'a dyn MatchSelector, + ) -> DefaultProcessor<'a> { Self { event_queue: VecDeque::new(), middleware: vec![ Box::new(MatchMiddleware::new(matchers)), - ] + Box::new(MatchSelectMiddleware::new(match_filter, match_selector)), + ], } } fn process_one(&mut self) -> Option { if let Some(event) = self.event_queue.pop_back() { let mut current_event = event; - + let mut current_queue = VecDeque::new(); let dispatch = |event: Event| { trace!("dispatched event: {:?}", event); @@ -51,7 +56,7 @@ impl <'a> DefaultProcessor<'a> { trace!("middleware received event: {:?}", current_event); current_event = middleware.next(current_event, &dispatch); - + trace!("middleware produced event: {:?}", current_event); } @@ -66,7 +71,7 @@ impl <'a> DefaultProcessor<'a> { } } -impl <'a> Processor for DefaultProcessor<'a> { +impl<'a> Processor for DefaultProcessor<'a> { fn process(&mut self, event: Event) -> Vec { self.event_queue.push_front(event); @@ -80,4 +85,4 @@ impl <'a> Processor for DefaultProcessor<'a> { processed_events } -} \ No newline at end of file +} diff --git a/espanso/src/engine/process/middleware/match_select.rs b/espanso/src/engine/process/middleware/match_select.rs new file mode 100644 index 0000000..146ad0b --- /dev/null +++ b/espanso/src/engine/process/middleware/match_select.rs @@ -0,0 +1,93 @@ +/* + * 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 log::{debug, error}; + +use super::super::Middleware; +use crate::engine::{ + event::{ + matches::{MatchSelectedEvent}, + Event, + }, + process::{MatchFilter, MatchSelector}, +}; + +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 next(&self, event: Event, _: &dyn FnMut(Event)) -> Event { + if let Event::MatchesDetected(m_event) = event { + let matches_ids: Vec = 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::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::MatchSelected(MatchSelectedEvent { chosen: m }) + } else { + error!("MatchSelectMiddleware could not find the correspondent match"); + Event::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::MatchSelected(MatchSelectedEvent { chosen: m }) + } else { + error!("MatchSelectMiddleware could not find the correspondent match"); + Event::NOOP + } + } else { + debug!("MatchSelectMiddleware did not receive any match selection"); + Event::NOOP + } + } + }; + } + + event + } +} + +// TODO: test \ No newline at end of file diff --git a/espanso/src/engine/process/middleware/matcher.rs b/espanso/src/engine/process/middleware/matcher.rs index c208b97..ab1c6ac 100644 --- a/espanso/src/engine/process/middleware/matcher.rs +++ b/espanso/src/engine/process/middleware/matcher.rs @@ -21,7 +21,7 @@ use log::trace; use std::{cell::RefCell, collections::VecDeque}; use super::super::Middleware; -use crate::engine::{event::{Event, keyboard::{Key, Status}, matches_detected::{MatchResult, MatchesDetectedEvent}}, process::{Matcher, MatcherEvent}}; +use crate::engine::{event::{Event, keyboard::{Key, Status}, matches::{DetectedMatch, MatchesDetectedEvent}}, process::{Matcher, MatcherEvent}}; const MAX_HISTORY: usize = 3; // TODO: get as parameter @@ -85,12 +85,10 @@ impl<'a, State> Middleware for MatchMiddleware<'a, State> { matcher_states.pop_front(); } - println!("results: {:?}", all_results); - if !all_results.is_empty() { return Event::MatchesDetected(MatchesDetectedEvent { - results: all_results.into_iter().map(|result | { - MatchResult { + matches: all_results.into_iter().map(|result | { + DetectedMatch { id: result.id, trigger: result.trigger, vars: result.vars, @@ -133,3 +131,5 @@ fn is_invalidating_key(key: &Key) -> bool { _ => false, } } + +// TODO: test \ No newline at end of file diff --git a/espanso/src/engine/process/middleware/mod.rs b/espanso/src/engine/process/middleware/mod.rs index 4b34c33..c6e0fcb 100644 --- a/espanso/src/engine/process/middleware/mod.rs +++ b/espanso/src/engine/process/middleware/mod.rs @@ -17,4 +17,5 @@ * along with espanso. If not, see . */ -pub mod matcher; \ No newline at end of file +pub mod matcher; +pub mod match_select; \ No newline at end of file diff --git a/espanso/src/engine/process/mod.rs b/espanso/src/engine/process/mod.rs index 73cb897..e16a501 100644 --- a/espanso/src/engine/process/mod.rs +++ b/espanso/src/engine/process/mod.rs @@ -17,13 +17,12 @@ * along with espanso. If not, see . */ - use std::collections::HashMap; -use super::{Event, event::keyboard::Key}; +use super::{event::keyboard::Key, Event}; -mod middleware; mod default; +mod middleware; pub trait Middleware { fn next(&self, event: Event, dispatch: &dyn FnMut(Event)) -> Event; @@ -36,7 +35,11 @@ pub trait Processor { // Dependency inversion entities pub trait Matcher<'a, State> { - fn process(&'a self, prev_state: Option<&State>, event: &MatcherEvent) -> (State, Vec); + fn process( + &'a self, + prev_state: Option<&State>, + event: &MatcherEvent, + ) -> (State, Vec); } #[derive(Debug)] @@ -52,6 +55,18 @@ pub struct MatchResult { pub vars: HashMap, } -pub fn default<'a, MatcherState>(matchers: &'a [&'a dyn Matcher<'a, MatcherState>]) -> impl Processor + 'a { - default::DefaultProcessor::new(matchers) -} \ No newline at end of file +pub trait MatchFilter { + fn filter_active(&self, matches_ids: &[i32]) -> Vec; +} + +pub trait MatchSelector { + fn select(&self, matches_ids: &[i32]) -> Option; +} + +pub fn default<'a, MatcherState>( + matchers: &'a [&'a dyn Matcher<'a, MatcherState>], + match_filter: &'a dyn MatchFilter, + match_selector: &'a dyn MatchSelector, +) -> impl Processor + 'a { + default::DefaultProcessor::new(matchers, match_filter, match_selector) +} diff --git a/espanso/src/main.rs b/espanso/src/main.rs index ce68796..d347453 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -201,8 +201,12 @@ fn main() { let log_level = match matches.occurrences_of("v") { 0 => LevelFilter::Warn, 1 => LevelFilter::Info, - 2 => LevelFilter::Debug, - _ => LevelFilter::Trace, + + // Trace mode is only available in debug mode for security reasons + #[cfg(debug_assertions)] + 3 => LevelFilter::Trace, + + _ => LevelFilter::Debug, }; let handler = CLI_HANDLERS