feat(core): progress in the core implementation

This commit is contained in:
Federico Terzi 2021-04-04 22:05:03 +02:00
parent 459e414a09
commit e643609d57
16 changed files with 380 additions and 38 deletions

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 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(&current_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<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()
}
}
// 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(),
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<RollingMatch<i32>> {
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()))
}
}

View File

@ -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> {

View File

@ -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<i32>]) -> Self {
let matcher = RollingMatcher::new(matches, Default::default());
Self { matcher }
}

View File

@ -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<matcher::MatcherState>> = 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);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
pub mod selector;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<i32> {
// TODO: replace with actual selection
Some(*matches_ids.first().unwrap())
}
}

View File

@ -19,6 +19,7 @@
#[derive(Debug)]
pub struct TextInjectEvent {
pub delete_count: i32,
pub text: String,
pub mode: TextInjectMode,
}

View File

@ -21,12 +21,17 @@ use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct MatchesDetectedEvent {
pub results: Vec<MatchResult>,
pub matches: Vec<DetectedMatch>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatchResult {
pub struct DetectedMatch {
pub id: i32,
pub trigger: String,
pub vars: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatchSelectedEvent {
pub chosen: DetectedMatch,
}

View File

@ -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),

View File

@ -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,13 +27,18 @@ pub struct DefaultProcessor<'a> {
middleware: Vec<Box<dyn Middleware + 'a>>,
}
impl <'a> DefaultProcessor<'a> {
pub fn new<MatcherState>(matchers: &'a [&'a dyn Matcher<'a, MatcherState>]) -> DefaultProcessor<'a> {
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,
) -> DefaultProcessor<'a> {
Self {
event_queue: VecDeque::new(),
middleware: vec![
Box::new(MatchMiddleware::new(matchers)),
]
Box::new(MatchSelectMiddleware::new(match_filter, match_selector)),
],
}
}
@ -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<Event> {
self.event_queue.push_front(event);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<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::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

View File

@ -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

View File

@ -18,3 +18,4 @@
*/
pub mod matcher;
pub mod match_select;

View File

@ -17,13 +17,12 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
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<MatchResult>);
fn process(
&'a self,
prev_state: Option<&State>,
event: &MatcherEvent,
) -> (State, Vec<MatchResult>);
}
#[derive(Debug)]
@ -52,6 +55,18 @@ pub struct MatchResult {
pub vars: HashMap<String, String>,
}
pub fn default<'a, MatcherState>(matchers: &'a [&'a dyn Matcher<'a, MatcherState>]) -> impl Processor + 'a {
default::DefaultProcessor::new(matchers)
pub trait MatchFilter {
fn filter_active(&self, matches_ids: &[i32]) -> Vec<i32>;
}
pub trait MatchSelector {
fn select(&self, matches_ids: &[i32]) -> Option<i32>;
}
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)
}

View File

@ -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