diff --git a/espanso/src/cli/worker/engine/executor/clipboard_injector.rs b/espanso/src/cli/worker/engine/executor/clipboard_injector.rs index 848ef0b..b549afc 100644 --- a/espanso/src/cli/worker/engine/executor/clipboard_injector.rs +++ b/espanso/src/cli/worker/engine/executor/clipboard_injector.rs @@ -17,10 +17,12 @@ * along with espanso. If not, see . */ +use std::path::PathBuf; + use espanso_inject::{Injector, keys::Key}; use espanso_clipboard::Clipboard; -use crate::engine::{dispatch::TextInjector, dispatch::HtmlInjector}; +use crate::engine::{dispatch::HtmlInjector, dispatch::{ImageInjector, TextInjector}}; pub struct ClipboardInjectorAdapter<'a> { injector: &'a dyn Injector, @@ -71,3 +73,19 @@ impl <'a> HtmlInjector for ClipboardInjectorAdapter<'a> { 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(()) + } +} diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index 04a31a1..2c59bd7 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -22,12 +22,15 @@ use espanso_config::{config::ConfigStore, matches::store::MatchStore}; use espanso_path::Paths; use ui::selector::MatchSelectorAdapter; +use crate::cli::worker::engine::path::PathProviderAdapter; + use super::ui::icon::IconPaths; pub mod executor; pub mod match_cache; pub mod matcher; pub mod multiplex; +pub mod path; pub mod render; pub mod source; pub mod ui; @@ -99,6 +102,7 @@ pub fn initialize_and_spawn( ]); let renderer_adapter = super::engine::render::RendererAdapter::new(&match_cache, &config_manager, &renderer); + let path_provider = PathProviderAdapter::new(&paths); let mut processor = crate::engine::process::default( &matchers, @@ -109,6 +113,7 @@ pub fn initialize_and_spawn( &match_cache, &modifier_state_store, &sequencer, + &path_provider, ); let event_injector = @@ -125,6 +130,7 @@ pub fn initialize_and_spawn( &config_manager, &key_injector, &clipboard_injector, + &clipboard_injector, ); let mut engine = crate::engine::Engine::new(&funnel, &mut processor, &dispatcher); diff --git a/espanso/src/cli/worker/engine/multiplex.rs b/espanso/src/cli/worker/engine/multiplex.rs index bbcfb1d..b0db6e2 100644 --- a/espanso/src/cli/worker/engine/multiplex.rs +++ b/espanso/src/cli/worker/engine/multiplex.rs @@ -19,14 +19,7 @@ use espanso_config::matches::{Match, MatchEffect}; -use crate::engine::{ - event::{ - internal::DetectedMatch, - internal::{RenderingRequestedEvent, TextFormat}, - EventType, - }, - process::Multiplexer, -}; +use crate::engine::{event::{EventType, internal::DetectedMatch, internal::{ImageRequestedEvent, RenderingRequestedEvent, TextFormat}}, process::Multiplexer}; pub trait MatchProvider<'a> { fn get(&self, match_id: i32) -> Option<&'a Match>; @@ -55,7 +48,10 @@ impl<'a> Multiplexer for MultiplexAdapter<'a> { trigger_args: detected_match.args, 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, } } diff --git a/espanso/src/cli/worker/engine/path.rs b/espanso/src/cli/worker/engine/path.rs new file mode 100644 index 0000000..1737393 --- /dev/null +++ b/espanso/src/cli/worker/engine/path.rs @@ -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 . + */ + +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 + } +} diff --git a/espanso/src/engine/dispatch/default.rs b/espanso/src/engine/dispatch/default.rs index 55d28a3..53051e7 100644 --- a/espanso/src/engine/dispatch/default.rs +++ b/espanso/src/engine/dispatch/default.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use super::Event; +use super::{Event, ImageInjector}; use super::{ModeProvider, Dispatcher, Executor, KeyInjector, TextInjector, HtmlInjector}; pub struct DefaultDispatcher<'a> { @@ -31,6 +31,7 @@ impl<'a> DefaultDispatcher<'a> { mode_provider: &'a dyn ModeProvider, key_injector: &'a dyn KeyInjector, html_injector: &'a dyn HtmlInjector, + image_injector: &'a dyn ImageInjector, ) -> Self { Self { executors: vec![ @@ -44,6 +45,9 @@ impl<'a> DefaultDispatcher<'a> { )), Box::new(super::executor::html_inject::HtmlInjectExecutor::new( html_injector, + )), + Box::new(super::executor::image_inject::ImageInjectExecutor::new( + image_injector, )) ], } diff --git a/espanso/src/engine/dispatch/executor/image_inject.rs b/espanso/src/engine/dispatch/executor/image_inject.rs new file mode 100644 index 0000000..1fdcfd2 --- /dev/null +++ b/espanso/src/engine/dispatch/executor/image_inject.rs @@ -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 . + */ + +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 diff --git a/espanso/src/engine/dispatch/executor/mod.rs b/espanso/src/engine/dispatch/executor/mod.rs index 09c73d0..b6a0d63 100644 --- a/espanso/src/engine/dispatch/executor/mod.rs +++ b/espanso/src/engine/dispatch/executor/mod.rs @@ -19,4 +19,5 @@ pub mod text_inject; pub mod key_inject; -pub mod html_inject; \ No newline at end of file +pub mod html_inject; +pub mod image_inject; \ No newline at end of file diff --git a/espanso/src/engine/dispatch/mod.rs b/espanso/src/engine/dispatch/mod.rs index ca80f8c..46ad9b8 100644 --- a/espanso/src/engine/dispatch/mod.rs +++ b/espanso/src/engine/dispatch/mod.rs @@ -35,6 +35,7 @@ pub trait Dispatcher { // Re-export dependency injection entities pub use executor::html_inject::HtmlInjector; pub use executor::text_inject::{Mode, ModeProvider, TextInjector}; +pub use executor::image_inject::{ImageInjector}; // TODO: move into module pub trait KeyInjector { @@ -47,6 +48,7 @@ pub fn default<'a>( mode_provider: &'a dyn ModeProvider, key_injector: &'a dyn KeyInjector, html_injector: &'a dyn HtmlInjector, + image_injector: &'a dyn ImageInjector, ) -> impl Dispatcher + 'a { default::DefaultDispatcher::new( event_injector, @@ -54,5 +56,6 @@ pub fn default<'a>( mode_provider, key_injector, html_injector, + image_injector, ) } diff --git a/espanso/src/engine/event/effect.rs b/espanso/src/engine/event/effect.rs index d554b1e..e8937b0 100644 --- a/espanso/src/engine/event/effect.rs +++ b/espanso/src/engine/event/effect.rs @@ -55,4 +55,9 @@ pub enum TextInjectMode { #[derive(Debug, Clone)] pub struct KeySequenceInjectRequest { pub keys: Vec, +} + +#[derive(Debug, Clone)] +pub struct ImageInjectRequest { + pub image_path: String, } \ No newline at end of file diff --git a/espanso/src/engine/event/internal.rs b/espanso/src/engine/event/internal.rs index 6d9a510..1afed3c 100644 --- a/espanso/src/engine/event/internal.rs +++ b/espanso/src/engine/event/internal.rs @@ -60,6 +60,17 @@ pub enum TextFormat { 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)] pub struct RenderedEvent { pub match_id: i32, diff --git a/espanso/src/engine/event/mod.rs b/espanso/src/engine/event/mod.rs index 324fead..e199867 100644 --- a/espanso/src/engine/event/mod.rs +++ b/espanso/src/engine/event/mod.rs @@ -59,7 +59,9 @@ pub enum EventType { CauseCompensatedMatch(internal::CauseCompensatedMatchEvent), RenderingRequested(internal::RenderingRequestedEvent), + ImageRequested(internal::ImageRequestedEvent), Rendered(internal::RenderedEvent), + ImageResolved(internal::ImageResolvedEvent), MatchInjected, DiscardPrevious(internal::DiscardPreviousEvent), @@ -71,4 +73,5 @@ pub enum EventType { TextInject(effect::TextInjectRequest), MarkdownInject(effect::MarkdownInjectRequest), HtmlInject(effect::HtmlInjectRequest), + ImageInject(effect::ImageInjectRequest), } \ No newline at end of file diff --git a/espanso/src/engine/process/default.rs b/espanso/src/engine/process/default.rs index 783e8eb..f8432eb 100644 --- a/espanso/src/engine/process/default.rs +++ b/espanso/src/engine/process/default.rs @@ -19,13 +19,13 @@ 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, render::RenderMiddleware, action::{ActionMiddleware, EventSequenceProvider}, cursor_hint::CursorHintMiddleware, cause::CauseCompensateMiddleware, delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider}, markdown::MarkdownMiddleware, past_discard::PastEventsDiscardMiddleware, }}; -use crate::engine::event::{Event, EventType}; +use crate::engine::{event::{Event, EventType}, process::middleware::image_resolve::ImageResolverMiddleware}; use std::collections::VecDeque; pub struct DefaultProcessor<'a> { @@ -43,6 +43,7 @@ impl<'a> DefaultProcessor<'a> { match_info_provider: &'a dyn MatchInfoProvider, modifier_status_provider: &'a dyn ModifierStatusProvider, event_sequence_provider: &'a dyn EventSequenceProvider, + path_provider: &'a dyn PathProvider, ) -> DefaultProcessor<'a> { Self { event_queue: VecDeque::new(), @@ -53,6 +54,7 @@ impl<'a> DefaultProcessor<'a> { Box::new(CauseCompensateMiddleware::new()), Box::new(MultiplexMiddleware::new(multiplexer)), Box::new(RenderMiddleware::new(renderer)), + Box::new(ImageResolverMiddleware::new(path_provider)), Box::new(CursorHintMiddleware::new()), Box::new(ActionMiddleware::new(match_info_provider, event_sequence_provider)), Box::new(MarkdownMiddleware::new()), diff --git a/espanso/src/engine/process/middleware/action.rs b/espanso/src/engine/process/middleware/action.rs index a581ac6..d1c96d8 100644 --- a/espanso/src/engine/process/middleware/action.rs +++ b/espanso/src/engine/process/middleware/action.rs @@ -18,7 +18,15 @@ */ 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 { fn get_force_mode(&self, match_id: i32) -> Option; @@ -52,7 +60,7 @@ impl<'a> Middleware for ActionMiddleware<'a> { fn next(&self, event: Event, dispatch: &mut dyn FnMut(Event)) -> Event { 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, @@ -61,21 +69,30 @@ impl<'a> Middleware for ActionMiddleware<'a> { }), )); - Event::caused_by( - event.source_id, - match m_event.format { - TextFormat::Plain => EventType::TextInject(TextInjectRequest { - text: m_event.body.clone(), - force_mode: self.match_info_provider.get_force_mode(m_event.match_id), + match &event.etype { + EventType::Rendered(m_event) => Event::caused_by( + event.source_id, + match m_event.format { + TextFormat::Plain => EventType::TextInject(TextInjectRequest { + 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(), - }), - TextFormat::Markdown => EventType::MarkdownInject(MarkdownInjectRequest { - markdown: m_event.body.clone(), - }), - }, - ) + ), + _ => unreachable!() + } } EventType::CursorHintCompensation(m_event) => { dispatch(Event::caused_by( diff --git a/espanso/src/engine/process/middleware/image_resolve.rs b/espanso/src/engine/process/middleware/image_resolve.rs new file mode 100644 index 0000000..7710611 --- /dev/null +++ b/espanso/src/engine/process/middleware/image_resolve.rs @@ -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 . + */ + +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 diff --git a/espanso/src/engine/process/middleware/mod.rs b/espanso/src/engine/process/middleware/mod.rs index bb85599..cd930b2 100644 --- a/espanso/src/engine/process/middleware/mod.rs +++ b/espanso/src/engine/process/middleware/mod.rs @@ -21,6 +21,7 @@ pub mod action; pub mod cause; pub mod cursor_hint; pub mod delay_modifiers; +pub mod image_resolve; pub mod match_select; pub mod matcher; pub mod markdown; diff --git a/espanso/src/engine/process/mod.rs b/espanso/src/engine/process/mod.rs index 57c772d..45f7060 100644 --- a/espanso/src/engine/process/mod.rs +++ b/espanso/src/engine/process/mod.rs @@ -94,6 +94,7 @@ pub enum RendererError { pub use middleware::action::{MatchInfoProvider, EventSequenceProvider}; pub use middleware::delay_modifiers::ModifierStatusProvider; +pub use middleware::image_resolve::PathProvider; pub fn default<'a, MatcherState>( matchers: &'a [&'a dyn Matcher<'a, MatcherState>], @@ -104,6 +105,7 @@ pub fn default<'a, MatcherState>( match_info_provider: &'a dyn MatchInfoProvider, modifier_status_provider: &'a dyn ModifierStatusProvider, event_sequence_provider: &'a dyn EventSequenceProvider, + path_provider: &'a dyn PathProvider, ) -> impl Processor + 'a { default::DefaultProcessor::new( matchers, @@ -114,5 +116,6 @@ pub fn default<'a, MatcherState>( match_info_provider, modifier_status_provider, event_sequence_provider, + path_provider, ) }