feat(core): introduce basic injection backend switch logic

This commit is contained in:
Federico Terzi 2021-04-17 13:27:08 +02:00
parent e532a377b1
commit 3a51efda2c
14 changed files with 296 additions and 75 deletions

View File

@ -22,7 +22,7 @@ use std::{
collections::{HashMap, HashSet},
};
use crate::engine::process::MatchFilter;
use crate::engine::{dispatch::ModeProvider, process::MatchFilter};
use espanso_config::{
config::{AppProperties, Config, ConfigStore},
matches::store::{MatchSet, MatchStore},
@ -89,12 +89,24 @@ impl<'a> MatchFilter for ConfigManager<'a> {
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()
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()
}
}
impl<'a> ModeProvider for ConfigManager<'a> {
fn active_mode(&self) -> crate::engine::dispatch::Mode {
// TODO: implement the actual active mode detection starting from the active config
crate::engine::dispatch::Mode::Auto {
clipboard_threshold: 100
}
}
}

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 espanso_inject::{Injector, keys::Key};
use espanso_clipboard::Clipboard;
use crate::engine::{dispatch::TextInjector};
pub struct ClipboardInjectorAdapter<'a> {
injector: &'a dyn Injector,
clipboard: &'a dyn Clipboard,
}
impl <'a> ClipboardInjectorAdapter<'a> {
pub fn new(injector: &'a dyn Injector, clipboard: &'a dyn Clipboard) -> Self {
Self {
injector,
clipboard,
}
}
}
impl <'a> TextInjector for ClipboardInjectorAdapter<'a> {
fn name(&self) -> &'static str {
"clipboard"
}
fn inject_text(&self, text: &str) -> anyhow::Result<()> {
// TODO: handle clipboard restoration
self.clipboard.set_text(text)?;
// TODO: handle delay duration
std::thread::sleep(std::time::Duration::from_millis(100));
// TODO: handle options
self.injector.send_key_combination(&[Key::Control, Key::V], Default::default())?;
Ok(())
}
}

View File

@ -21,11 +21,11 @@ use espanso_inject::Injector;
use crate::engine::dispatch::TextInjector;
pub struct TextInjectorAdapter<'a> {
pub struct EventInjectorAdapter<'a> {
injector: &'a dyn Injector,
}
impl <'a> TextInjectorAdapter<'a> {
impl <'a> EventInjectorAdapter<'a> {
pub fn new(injector: &'a dyn Injector) -> Self {
Self {
injector
@ -33,7 +33,11 @@ impl <'a> TextInjectorAdapter<'a> {
}
}
impl <'a> TextInjector for TextInjectorAdapter<'a> {
impl <'a> TextInjector for EventInjectorAdapter<'a> {
fn name(&self) -> &'static str {
"event"
}
fn inject_text(&self, text: &str) -> anyhow::Result<()> {
// TODO: handle injection options
self.injector.send_string(text, Default::default())

View File

@ -17,5 +17,6 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
pub mod text_injector;
pub mod clipboard_injector;
pub mod event_injector;
pub mod key_injector;

View File

@ -19,10 +19,9 @@
use std::{collections::HashMap, iter::FromIterator};
use espanso_config::{
config::ConfigStore,
matches::{store::MatchStore, Match},
};
use espanso_config::{config::ConfigStore, matches::{Match, MatchEffect, store::MatchStore}};
use crate::engine::process::MatchInfoProvider;
use super::{multiplex::MatchProvider, render::MatchIterator};
@ -56,3 +55,14 @@ impl<'a> MatchIterator<'a> for MatchCache<'a> {
self.cache.iter().map(|(_, m)| *m).collect()
}
}
impl<'a> MatchInfoProvider for MatchCache<'a> {
fn get_force_mode(&self, match_id: i32) -> Option<crate::engine::event::text::TextInjectMode> {
let m = self.cache.get(&match_id)?;
if let MatchEffect::Text(text_effect) = &m.effect {
// TODO: read match effect and convert it to the actual injection mode
}
None
}
}

View File

@ -17,15 +17,15 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use engine::ui::selector::MatchSelectorAdapter;
use funnel::Source;
use process::Matcher;
use engine::ui::selector::MatchSelectorAdapter;
use crate::engine::{Engine, funnel, process, dispatch};
use super::{CliModule, CliModuleArgs};
use crate::engine::{dispatch, funnel, process, Engine};
mod engine;
mod config;
mod engine;
pub fn new() -> CliModule {
#[allow(clippy::needless_update)]
@ -40,35 +40,62 @@ pub fn new() -> CliModule {
}
fn worker_main(args: CliModuleArgs) {
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 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 = engine::matcher::convert::MatchConverter::new(&*config_store, &*match_store);
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 =
engine::matcher::convert::MatchConverter::new(&*config_store, &*match_store);
let match_cache = engine::match_cache::MatchCache::load(&*config_store, &*match_store);
let detect_source = engine::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 funnel = funnel::default(&sources);
let matcher = engine::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<engine::matcher::MatcherState>> = vec![&matcher];
let selector = MatchSelectorAdapter::new();
let multiplexer = engine::multiplex::MultiplexAdapter::new(&match_cache);
// TODO: add extensions
let renderer = espanso_render::create(Vec::new());
let renderer_adapter = engine::render::RendererAdapter::new(&match_cache, &config_manager, &renderer);
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 mut processor = process::default(
&matchers,
&config_manager,
&selector,
&multiplexer,
&renderer_adapter,
&match_cache,
);
let injector = espanso_inject::get_injector(Default::default()).expect("failed to initialize injector module"); // TODO: handle the options
let injector =
espanso_inject::get_injector(Default::default()).expect("failed to initialize injector module"); // TODO: handle the options
let clipboard = espanso_clipboard::get_clipboard(Default::default())
.expect("failed to initialize clipboard module"); // TODO: handle options
let text_injector = engine::executor::text_injector::TextInjectorAdapter::new(&*injector);
let event_injector = engine::executor::event_injector::EventInjectorAdapter::new(&*injector);
let clipboard_injector =
engine::executor::clipboard_injector::ClipboardInjectorAdapter::new(&*injector, &*clipboard);
let key_injector = engine::executor::key_injector::KeyInjectorAdapter::new(&*injector);
let dispatcher = dispatch::default(&text_injector, &key_injector);
let dispatcher = dispatch::default(
&event_injector,
&clipboard_injector,
&config_manager,
&key_injector,
);
let mut engine = Engine::new(&funnel, &mut processor, &dispatcher);
engine.run();
}
}

View File

@ -17,29 +17,40 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use super::{Dispatcher, Executor, KeyInjector, TextInjector};
use super::Event;
use super::{ModeProvider, Dispatcher, Executor, KeyInjector, TextInjector};
pub struct DefaultDispatcher<'a> {
executors: Vec<Box<dyn Executor + 'a>>,
}
impl <'a> DefaultDispatcher<'a> {
pub fn new(text_injector: &'a dyn TextInjector, key_injector: &'a dyn KeyInjector) -> Self {
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,
) -> Self {
Self {
executors: vec![
Box::new(super::executor::text_inject::TextInjectExecutor::new(text_injector)),
Box::new(super::executor::key_inject::KeyInjectExecutor::new(key_injector)),
]
Box::new(super::executor::text_inject::TextInjectExecutor::new(
event_injector,
clipboard_injector,
mode_provider,
)),
Box::new(super::executor::key_inject::KeyInjectExecutor::new(
key_injector,
)),
],
}
}
}
impl <'a> Dispatcher for DefaultDispatcher<'a> {
impl<'a> Dispatcher for DefaultDispatcher<'a> {
fn dispatch(&self, event: Event) {
for executor in self.executors.iter() {
if executor.execute(&event) {
break
break;
}
}
}

View File

@ -17,31 +17,92 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use super::super::{Event, Executor, TextInjector};
use anyhow::Result;
use super::super::{Event, Executor};
use crate::engine::event::text::TextInjectMode;
use log::error;
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> {
injector: &'a dyn TextInjector,
event_injector: &'a dyn TextInjector,
clipboard_injector: &'a dyn TextInjector,
mode_provider: &'a dyn ModeProvider,
}
impl<'a> TextInjectExecutor<'a> {
pub fn new(injector: &'a dyn TextInjector) -> Self {
Self { injector }
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 Event::TextInject(inject_event) = event {
if let Some(TextInjectMode::Keys) = inject_event.force_mode {
if let Err(error) = self.injector.inject_text(&inject_event.text) {
error!("text injector reported an error: {:?}", error);
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
}
return true;
} 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

@ -18,10 +18,11 @@
*/
use anyhow::Result;
use super::{Event, event::keyboard::Key};
mod executor;
use super::{event::keyboard::Key, Event};
mod default;
mod executor;
pub trait Executor {
fn execute(&self, event: &Event) -> bool;
@ -31,17 +32,19 @@ pub trait Dispatcher {
fn dispatch(&self, event: Event);
}
pub trait TextInjector {
fn inject_text(&self, text: &str) -> Result<()>;
}
// Re-export dependency injection entities
pub use executor::text_inject::{ModeProvider, Mode, TextInjector};
// TODO: move into module
pub trait KeyInjector {
fn inject_sequence(&self, keys: &[Key]) -> Result<()>;
}
pub fn default<'a>(text_injector: &'a dyn TextInjector, key_injector: &'a dyn KeyInjector) -> impl Dispatcher + 'a {
default::DefaultDispatcher::new(
text_injector,
key_injector,
)
}
pub fn default<'a>(
event_injector: &'a dyn TextInjector,
clipboard_injector: &'a dyn TextInjector,
mode_provider: &'a dyn ModeProvider,
key_injector: &'a dyn KeyInjector,
) -> impl Dispatcher + 'a {
default::DefaultDispatcher::new(event_injector, clipboard_injector, mode_provider, key_injector)
}

View File

@ -28,6 +28,8 @@ pub struct RenderingRequestedEvent {
#[derive(Debug, Clone, PartialEq)]
pub struct RenderedEvent {
pub match_id: i32,
pub trigger: String,
pub body: String,

View File

@ -19,13 +19,10 @@
use log::trace;
use super::{
middleware::{
use super::{Event, MatchFilter, MatchInfoProvider, MatchSelector, Matcher, Middleware, Multiplexer, Processor, Renderer, middleware::{
match_select::MatchSelectMiddleware, matcher::MatchMiddleware, multiplex::MultiplexMiddleware,
render::RenderMiddleware, action::ActionMiddleware,
},
Event, MatchFilter, MatchSelector, Matcher, Middleware, Multiplexer, Processor, Renderer,
};
}};
use std::collections::VecDeque;
pub struct DefaultProcessor<'a> {
@ -40,6 +37,7 @@ impl<'a> DefaultProcessor<'a> {
match_selector: &'a dyn MatchSelector,
multiplexer: &'a dyn Multiplexer,
renderer: &'a dyn Renderer<'a>,
match_info_provider: &'a dyn MatchInfoProvider,
) -> DefaultProcessor<'a> {
Self {
event_queue: VecDeque::new(),
@ -48,7 +46,7 @@ impl<'a> DefaultProcessor<'a> {
Box::new(MatchSelectMiddleware::new(match_filter, match_selector)),
Box::new(MultiplexMiddleware::new(multiplexer)),
Box::new(RenderMiddleware::new(renderer)),
Box::new(ActionMiddleware::new()),
Box::new(ActionMiddleware::new(match_info_provider)),
],
}
}

View File

@ -18,39 +18,57 @@
*/
use super::super::Middleware;
use crate::engine::{event::{Event, keyboard::{Key, KeySequenceInjectRequest}, text::{TextInjectMode, TextInjectRequest}}, process::{MatchFilter, MatchSelector, Multiplexer}};
use crate::engine::{
dispatch::Mode,
event::{
keyboard::{Key, KeySequenceInjectRequest},
text::{TextInjectMode, TextInjectRequest},
Event,
},
};
pub struct ActionMiddleware {
pub trait MatchInfoProvider {
fn get_force_mode(&self, match_id: i32) -> Option<TextInjectMode>;
}
impl ActionMiddleware {
pub fn new() -> Self {
Self {}
pub struct ActionMiddleware<'a> {
match_info_provider: &'a dyn MatchInfoProvider,
}
impl<'a> ActionMiddleware<'a> {
pub fn new(match_info_provider: &'a dyn MatchInfoProvider) -> Self {
Self {
match_info_provider,
}
}
}
impl Middleware for ActionMiddleware {
impl<'a> Middleware for ActionMiddleware<'a> {
fn name(&self) -> &'static str {
"action"
}
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
if let Event::Rendered(m_event) = &event {
dispatch(Event::TextInject(TextInjectRequest {
text: m_event.body.clone(),
force_mode: Some(TextInjectMode::Keys), // TODO: determine this one dynamically
force_mode: self.match_info_provider.get_force_mode(m_event.match_id),
}));
if let Some(cursor_hint_back_count) = m_event.cursor_hint_back_count {
dispatch(Event::KeySequenceInject(KeySequenceInjectRequest {
keys: (0..cursor_hint_back_count).map(|_| Key::ArrowLeft).collect(),
keys: (0..cursor_hint_back_count)
.map(|_| Key::ArrowLeft)
.collect(),
}))
}
// This is executed before the dispatched event
return Event::KeySequenceInject(KeySequenceInjectRequest {
keys: (0..m_event.trigger.chars().count()).map(|_| Key::Backspace).collect()
})
keys: (0..m_event.trigger.chars().count())
.map(|_| Key::Backspace)
.collect(),
});
}
// TODO: handle images

View File

@ -50,6 +50,7 @@ impl<'a> Middleware for RenderMiddleware<'a> {
let (body, cursor_hint_back_count) = process_cursor_hint(body);
return Event::Rendered(RenderedEvent {
match_id: m_event.match_id,
trigger: m_event.trigger,
body,
cursor_hint_back_count,

View File

@ -36,6 +36,8 @@ pub trait Processor {
// Dependency inversion entities
// TODO: move these traits inside the various modules and then re-export it
pub trait Matcher<'a, State> {
fn process(
&'a self,
@ -66,7 +68,12 @@ pub trait MatchSelector {
}
pub trait Multiplexer {
fn convert(&self, match_id: i32, trigger: String, trigger_args: HashMap<String, String>) -> Option<Event>;
fn convert(
&self,
match_id: i32,
trigger: String,
trigger_args: HashMap<String, String>,
) -> Option<Event>;
}
pub trait Renderer<'a> {
@ -85,12 +92,22 @@ pub enum RendererError {
Aborted,
}
pub use middleware::action::MatchInfoProvider;
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,
) -> impl Processor + 'a {
default::DefaultProcessor::new(matchers, match_filter, match_selector, multiplexer, renderer)
default::DefaultProcessor::new(
matchers,
match_filter,
match_selector,
multiplexer,
renderer,
match_info_provider,
)
}