feat(core): implement image matches

This commit is contained in:
Federico Terzi 2021-05-01 19:41:22 +02:00
parent 7a8e39fdad
commit ddd62b225f
16 changed files with 277 additions and 30 deletions

View File

@ -17,10 +17,12 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::path::PathBuf;
use espanso_inject::{Injector, keys::Key}; use espanso_inject::{Injector, keys::Key};
use espanso_clipboard::Clipboard; use espanso_clipboard::Clipboard;
use crate::engine::{dispatch::TextInjector, dispatch::HtmlInjector}; use crate::engine::{dispatch::HtmlInjector, dispatch::{ImageInjector, TextInjector}};
pub struct ClipboardInjectorAdapter<'a> { pub struct ClipboardInjectorAdapter<'a> {
injector: &'a dyn Injector, injector: &'a dyn Injector,
@ -71,3 +73,19 @@ impl <'a> HtmlInjector for ClipboardInjectorAdapter<'a> {
Ok(()) Ok(())
} }
} }
impl <'a> ImageInjector for ClipboardInjectorAdapter<'a> {
fn inject_image(&self, image_path: &str) -> anyhow::Result<()> {
let path = PathBuf::from(image_path);
if !path.is_file() {
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "image can't be found in the given path").into());
}
// TODO: handle clipboard restoration
self.clipboard.set_image(&path)?;
self.send_paste_combination()?;
Ok(())
}
}

View File

@ -22,12 +22,15 @@ use espanso_config::{config::ConfigStore, matches::store::MatchStore};
use espanso_path::Paths; use espanso_path::Paths;
use ui::selector::MatchSelectorAdapter; use ui::selector::MatchSelectorAdapter;
use crate::cli::worker::engine::path::PathProviderAdapter;
use super::ui::icon::IconPaths; use super::ui::icon::IconPaths;
pub mod executor; pub mod executor;
pub mod match_cache; pub mod match_cache;
pub mod matcher; pub mod matcher;
pub mod multiplex; pub mod multiplex;
pub mod path;
pub mod render; pub mod render;
pub mod source; pub mod source;
pub mod ui; pub mod ui;
@ -99,6 +102,7 @@ pub fn initialize_and_spawn(
]); ]);
let renderer_adapter = let renderer_adapter =
super::engine::render::RendererAdapter::new(&match_cache, &config_manager, &renderer); super::engine::render::RendererAdapter::new(&match_cache, &config_manager, &renderer);
let path_provider = PathProviderAdapter::new(&paths);
let mut processor = crate::engine::process::default( let mut processor = crate::engine::process::default(
&matchers, &matchers,
@ -109,6 +113,7 @@ pub fn initialize_and_spawn(
&match_cache, &match_cache,
&modifier_state_store, &modifier_state_store,
&sequencer, &sequencer,
&path_provider,
); );
let event_injector = let event_injector =
@ -125,6 +130,7 @@ pub fn initialize_and_spawn(
&config_manager, &config_manager,
&key_injector, &key_injector,
&clipboard_injector, &clipboard_injector,
&clipboard_injector,
); );
let mut engine = crate::engine::Engine::new(&funnel, &mut processor, &dispatcher); let mut engine = crate::engine::Engine::new(&funnel, &mut processor, &dispatcher);

View File

@ -19,14 +19,7 @@
use espanso_config::matches::{Match, MatchEffect}; use espanso_config::matches::{Match, MatchEffect};
use crate::engine::{ use crate::engine::{event::{EventType, internal::DetectedMatch, internal::{ImageRequestedEvent, RenderingRequestedEvent, TextFormat}}, process::Multiplexer};
event::{
internal::DetectedMatch,
internal::{RenderingRequestedEvent, TextFormat},
EventType,
},
process::Multiplexer,
};
pub trait MatchProvider<'a> { pub trait MatchProvider<'a> {
fn get(&self, match_id: i32) -> Option<&'a Match>; fn get(&self, match_id: i32) -> Option<&'a Match>;
@ -55,7 +48,10 @@ impl<'a> Multiplexer for MultiplexAdapter<'a> {
trigger_args: detected_match.args, trigger_args: detected_match.args,
format: convert_format(&effect.format), format: convert_format(&effect.format),
})), })),
// TODO: think about image MatchEffect::Image(effect) => Some(EventType::ImageRequested(ImageRequestedEvent {
match_id: detected_match.id,
image_path: effect.path.clone(),
})),
MatchEffect::None => None, MatchEffect::None => None,
} }
} }

View File

@ -0,0 +1,40 @@
/*
* 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_path::Paths;
use crate::engine::process::PathProvider;
pub struct PathProviderAdapter<'a> {
paths: &'a Paths,
}
impl <'a> PathProviderAdapter<'a> {
pub fn new(paths: &'a Paths) -> Self {
Self {
paths,
}
}
}
impl <'a> PathProvider for PathProviderAdapter<'a> {
fn get_config_path(&self) -> &std::path::Path {
&self.paths.config
}
}

View File

@ -17,7 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use super::Event; use super::{Event, ImageInjector};
use super::{ModeProvider, Dispatcher, Executor, KeyInjector, TextInjector, HtmlInjector}; use super::{ModeProvider, Dispatcher, Executor, KeyInjector, TextInjector, HtmlInjector};
pub struct DefaultDispatcher<'a> { pub struct DefaultDispatcher<'a> {
@ -31,6 +31,7 @@ impl<'a> DefaultDispatcher<'a> {
mode_provider: &'a dyn ModeProvider, mode_provider: &'a dyn ModeProvider,
key_injector: &'a dyn KeyInjector, key_injector: &'a dyn KeyInjector,
html_injector: &'a dyn HtmlInjector, html_injector: &'a dyn HtmlInjector,
image_injector: &'a dyn ImageInjector,
) -> Self { ) -> Self {
Self { Self {
executors: vec![ executors: vec![
@ -44,6 +45,9 @@ impl<'a> DefaultDispatcher<'a> {
)), )),
Box::new(super::executor::html_inject::HtmlInjectExecutor::new( Box::new(super::executor::html_inject::HtmlInjectExecutor::new(
html_injector, html_injector,
)),
Box::new(super::executor::image_inject::ImageInjectExecutor::new(
image_injector,
)) ))
], ],
} }

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 super::super::{Event, Executor};
use crate::engine::event::EventType;
use anyhow::Result;
use log::error;
pub trait ImageInjector {
fn inject_image(&self, path: &str) -> Result<()>;
}
pub struct ImageInjectExecutor<'a> {
injector: &'a dyn ImageInjector,
}
impl<'a> ImageInjectExecutor<'a> {
pub fn new(injector: &'a dyn ImageInjector) -> Self {
Self { injector }
}
}
impl<'a> Executor for ImageInjectExecutor<'a> {
fn execute(&self, event: &Event) -> bool {
if let EventType::ImageInject(inject_event) = &event.etype {
if let Err(error) = self
.injector
.inject_image(&inject_event.image_path)
{
error!("image injector reported an error: {:?}", error);
}
return true;
}
false
}
}
// TODO: test

View File

@ -19,4 +19,5 @@
pub mod text_inject; pub mod text_inject;
pub mod key_inject; pub mod key_inject;
pub mod html_inject; pub mod html_inject;
pub mod image_inject;

View File

@ -35,6 +35,7 @@ pub trait Dispatcher {
// Re-export dependency injection entities // Re-export dependency injection entities
pub use executor::html_inject::HtmlInjector; pub use executor::html_inject::HtmlInjector;
pub use executor::text_inject::{Mode, ModeProvider, TextInjector}; pub use executor::text_inject::{Mode, ModeProvider, TextInjector};
pub use executor::image_inject::{ImageInjector};
// TODO: move into module // TODO: move into module
pub trait KeyInjector { pub trait KeyInjector {
@ -47,6 +48,7 @@ pub fn default<'a>(
mode_provider: &'a dyn ModeProvider, mode_provider: &'a dyn ModeProvider,
key_injector: &'a dyn KeyInjector, key_injector: &'a dyn KeyInjector,
html_injector: &'a dyn HtmlInjector, html_injector: &'a dyn HtmlInjector,
image_injector: &'a dyn ImageInjector,
) -> impl Dispatcher + 'a { ) -> impl Dispatcher + 'a {
default::DefaultDispatcher::new( default::DefaultDispatcher::new(
event_injector, event_injector,
@ -54,5 +56,6 @@ pub fn default<'a>(
mode_provider, mode_provider,
key_injector, key_injector,
html_injector, html_injector,
image_injector,
) )
} }

View File

@ -55,4 +55,9 @@ pub enum TextInjectMode {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct KeySequenceInjectRequest { pub struct KeySequenceInjectRequest {
pub keys: Vec<Key>, pub keys: Vec<Key>,
}
#[derive(Debug, Clone)]
pub struct ImageInjectRequest {
pub image_path: String,
} }

View File

@ -60,6 +60,17 @@ pub enum TextFormat {
Html, Html,
} }
#[derive(Debug, Clone, PartialEq)]
pub struct ImageRequestedEvent {
pub match_id: i32,
pub image_path: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImageResolvedEvent {
pub image_path: String,
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct RenderedEvent { pub struct RenderedEvent {
pub match_id: i32, pub match_id: i32,

View File

@ -59,7 +59,9 @@ pub enum EventType {
CauseCompensatedMatch(internal::CauseCompensatedMatchEvent), CauseCompensatedMatch(internal::CauseCompensatedMatchEvent),
RenderingRequested(internal::RenderingRequestedEvent), RenderingRequested(internal::RenderingRequestedEvent),
ImageRequested(internal::ImageRequestedEvent),
Rendered(internal::RenderedEvent), Rendered(internal::RenderedEvent),
ImageResolved(internal::ImageResolvedEvent),
MatchInjected, MatchInjected,
DiscardPrevious(internal::DiscardPreviousEvent), DiscardPrevious(internal::DiscardPreviousEvent),
@ -71,4 +73,5 @@ pub enum EventType {
TextInject(effect::TextInjectRequest), TextInject(effect::TextInjectRequest),
MarkdownInject(effect::MarkdownInjectRequest), MarkdownInject(effect::MarkdownInjectRequest),
HtmlInject(effect::HtmlInjectRequest), HtmlInject(effect::HtmlInjectRequest),
ImageInject(effect::ImageInjectRequest),
} }

View File

@ -19,13 +19,13 @@
use log::trace; use log::trace;
use super::{MatchFilter, MatchInfoProvider, MatchSelector, Matcher, Middleware, Multiplexer, Processor, Renderer, middleware::{ use super::{MatchFilter, MatchInfoProvider, MatchSelector, Matcher, Middleware, Multiplexer, PathProvider, Processor, Renderer, middleware::{
match_select::MatchSelectMiddleware, matcher::MatcherMiddleware, multiplex::MultiplexMiddleware, match_select::MatchSelectMiddleware, matcher::MatcherMiddleware, multiplex::MultiplexMiddleware,
render::RenderMiddleware, action::{ActionMiddleware, EventSequenceProvider}, cursor_hint::CursorHintMiddleware, cause::CauseCompensateMiddleware, render::RenderMiddleware, action::{ActionMiddleware, EventSequenceProvider}, cursor_hint::CursorHintMiddleware, cause::CauseCompensateMiddleware,
delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider}, markdown::MarkdownMiddleware, delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider}, markdown::MarkdownMiddleware,
past_discard::PastEventsDiscardMiddleware, past_discard::PastEventsDiscardMiddleware,
}}; }};
use crate::engine::event::{Event, EventType}; use crate::engine::{event::{Event, EventType}, process::middleware::image_resolve::ImageResolverMiddleware};
use std::collections::VecDeque; use std::collections::VecDeque;
pub struct DefaultProcessor<'a> { pub struct DefaultProcessor<'a> {
@ -43,6 +43,7 @@ impl<'a> DefaultProcessor<'a> {
match_info_provider: &'a dyn MatchInfoProvider, match_info_provider: &'a dyn MatchInfoProvider,
modifier_status_provider: &'a dyn ModifierStatusProvider, modifier_status_provider: &'a dyn ModifierStatusProvider,
event_sequence_provider: &'a dyn EventSequenceProvider, event_sequence_provider: &'a dyn EventSequenceProvider,
path_provider: &'a dyn PathProvider,
) -> DefaultProcessor<'a> { ) -> DefaultProcessor<'a> {
Self { Self {
event_queue: VecDeque::new(), event_queue: VecDeque::new(),
@ -53,6 +54,7 @@ impl<'a> DefaultProcessor<'a> {
Box::new(CauseCompensateMiddleware::new()), Box::new(CauseCompensateMiddleware::new()),
Box::new(MultiplexMiddleware::new(multiplexer)), Box::new(MultiplexMiddleware::new(multiplexer)),
Box::new(RenderMiddleware::new(renderer)), Box::new(RenderMiddleware::new(renderer)),
Box::new(ImageResolverMiddleware::new(path_provider)),
Box::new(CursorHintMiddleware::new()), Box::new(CursorHintMiddleware::new()),
Box::new(ActionMiddleware::new(match_info_provider, event_sequence_provider)), Box::new(ActionMiddleware::new(match_info_provider, event_sequence_provider)),
Box::new(MarkdownMiddleware::new()), Box::new(MarkdownMiddleware::new()),

View File

@ -18,7 +18,15 @@
*/ */
use super::super::Middleware; use super::super::Middleware;
use crate::engine::event::{Event, EventType, effect::{HtmlInjectRequest, KeySequenceInjectRequest, MarkdownInjectRequest, TextInjectMode, TextInjectRequest}, input::Key, internal::{DiscardPreviousEvent, TextFormat}}; use crate::engine::event::{
effect::{
HtmlInjectRequest, ImageInjectRequest, KeySequenceInjectRequest, MarkdownInjectRequest,
TextInjectMode, TextInjectRequest,
},
input::Key,
internal::{DiscardPreviousEvent, TextFormat},
Event, EventType,
};
pub trait MatchInfoProvider { pub trait MatchInfoProvider {
fn get_force_mode(&self, match_id: i32) -> Option<TextInjectMode>; fn get_force_mode(&self, match_id: i32) -> Option<TextInjectMode>;
@ -52,7 +60,7 @@ impl<'a> Middleware for ActionMiddleware<'a> {
fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event { fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event {
match &event.etype { match &event.etype {
EventType::Rendered(m_event) => { EventType::Rendered(_) | EventType::ImageResolved(_) => {
dispatch(Event::caused_by(event.source_id, EventType::MatchInjected)); dispatch(Event::caused_by(event.source_id, EventType::MatchInjected));
dispatch(Event::caused_by( dispatch(Event::caused_by(
event.source_id, event.source_id,
@ -61,21 +69,30 @@ impl<'a> Middleware for ActionMiddleware<'a> {
}), }),
)); ));
Event::caused_by( match &event.etype {
event.source_id, EventType::Rendered(m_event) => Event::caused_by(
match m_event.format { event.source_id,
TextFormat::Plain => EventType::TextInject(TextInjectRequest { match m_event.format {
text: m_event.body.clone(), TextFormat::Plain => EventType::TextInject(TextInjectRequest {
force_mode: self.match_info_provider.get_force_mode(m_event.match_id), text: m_event.body.clone(),
force_mode: self.match_info_provider.get_force_mode(m_event.match_id),
}),
TextFormat::Html => EventType::HtmlInject(HtmlInjectRequest {
html: m_event.body.clone(),
}),
TextFormat::Markdown => EventType::MarkdownInject(MarkdownInjectRequest {
markdown: m_event.body.clone(),
}),
},
),
EventType::ImageResolved(m_event) => Event::caused_by(
event.source_id,
EventType::ImageInject(ImageInjectRequest {
image_path: m_event.image_path.clone(),
}), }),
TextFormat::Html => EventType::HtmlInject(HtmlInjectRequest { ),
html: m_event.body.clone(), _ => unreachable!()
}), }
TextFormat::Markdown => EventType::MarkdownInject(MarkdownInjectRequest {
markdown: m_event.body.clone(),
}),
},
)
} }
EventType::CursorHintCompensation(m_event) => { EventType::CursorHintCompensation(m_event) => {
dispatch(Event::caused_by( dispatch(Event::caused_by(

View File

@ -0,0 +1,81 @@
/*
* 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::{
path::Path,
};
use log::{error};
use super::super::Middleware;
use crate::engine::event::{Event, EventType, internal::ImageResolvedEvent};
pub trait PathProvider {
fn get_config_path(&self) -> &Path;
}
pub struct ImageResolverMiddleware<'a> {
provider: &'a dyn PathProvider,
}
impl<'a> ImageResolverMiddleware<'a> {
pub fn new(provider: &'a dyn PathProvider) -> Self {
Self { provider }
}
}
impl<'a> Middleware for ImageResolverMiddleware<'a> {
fn name(&self) -> &'static str {
"image_resolve"
}
fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event {
if let EventType::ImageRequested(m_event) = &event.etype {
// On Windows, we have to replace the forward / with the backslash \ in the path
let path = if cfg!(target_os = "windows") {
m_event.image_path.replace("/", "\\")
} else {
m_event.image_path.to_owned()
};
let path = if path.contains("$CONFIG") {
let config_path = match self.provider.get_config_path().canonicalize() {
Ok(path) => path,
Err(err) => {
error!("unable to canonicalize the config path into the image resolver: {}", err);
self.provider.get_config_path().to_owned()
},
};
path.replace("$CONFIG", &config_path.to_string_lossy())
} else {
path
};
return Event::caused_by(event.source_id, EventType::ImageResolved(ImageResolvedEvent {
image_path: path,
}))
}
event
}
}
// TODO: test

View File

@ -21,6 +21,7 @@ pub mod action;
pub mod cause; pub mod cause;
pub mod cursor_hint; pub mod cursor_hint;
pub mod delay_modifiers; pub mod delay_modifiers;
pub mod image_resolve;
pub mod match_select; pub mod match_select;
pub mod matcher; pub mod matcher;
pub mod markdown; pub mod markdown;

View File

@ -94,6 +94,7 @@ pub enum RendererError {
pub use middleware::action::{MatchInfoProvider, EventSequenceProvider}; pub use middleware::action::{MatchInfoProvider, EventSequenceProvider};
pub use middleware::delay_modifiers::ModifierStatusProvider; pub use middleware::delay_modifiers::ModifierStatusProvider;
pub use middleware::image_resolve::PathProvider;
pub fn default<'a, MatcherState>( pub fn default<'a, MatcherState>(
matchers: &'a [&'a dyn Matcher<'a, MatcherState>], matchers: &'a [&'a dyn Matcher<'a, MatcherState>],
@ -104,6 +105,7 @@ pub fn default<'a, MatcherState>(
match_info_provider: &'a dyn MatchInfoProvider, match_info_provider: &'a dyn MatchInfoProvider,
modifier_status_provider: &'a dyn ModifierStatusProvider, modifier_status_provider: &'a dyn ModifierStatusProvider,
event_sequence_provider: &'a dyn EventSequenceProvider, event_sequence_provider: &'a dyn EventSequenceProvider,
path_provider: &'a dyn PathProvider,
) -> impl Processor + 'a { ) -> impl Processor + 'a {
default::DefaultProcessor::new( default::DefaultProcessor::new(
matchers, matchers,
@ -114,5 +116,6 @@ pub fn default<'a, MatcherState>(
match_info_provider, match_info_provider,
modifier_status_provider, modifier_status_provider,
event_sequence_provider, event_sequence_provider,
path_provider,
) )
} }