From 6eec895b2145b3069f1788defd9f984be6617d11 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 22 Dec 2019 00:06:55 +0100 Subject: [PATCH] Move match rendering to standalone component. Add support for nested triggers ( Fix #110 ) --- src/engine.rs | 84 ++++++----------------- src/main.rs | 5 +- src/render/default.rs | 155 ++++++++++++++++++++++++++++++++++++++++++ src/render/mod.rs | 34 +++++++++ 4 files changed, 215 insertions(+), 63 deletions(-) create mode 100644 src/render/default.rs create mode 100644 src/render/mod.rs diff --git a/src/engine.rs b/src/engine.rs index 8f49a5c..7cfd507 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -26,6 +26,7 @@ use log::{info, warn, error}; use crate::ui::{UIManager, MenuItem, MenuItemType}; use crate::event::{ActionEventReceiver, ActionType}; use crate::extension::Extension; +use crate::render::{Renderer, RenderResult}; use std::cell::RefCell; use std::process::exit; use std::collections::HashMap; @@ -33,35 +34,28 @@ use std::path::PathBuf; use regex::{Regex, Captures}; pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, - U: UIManager> { + U: UIManager, R: Renderer> { keyboard_manager: &'a S, clipboard_manager: &'a C, config_manager: &'a M, ui_manager: &'a U, - - extension_map: HashMap>, + renderer: &'a R, enabled: RefCell, } -impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> - Engine<'a, S, C, M, U> { +impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer> + Engine<'a, S, C, M, U, R> { pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C, config_manager: &'a M, ui_manager: &'a U, - extensions: Vec>) -> Engine<'a, S, C, M, U> { - // Register all the extensions - let mut extension_map = HashMap::new(); - for extension in extensions.into_iter() { - extension_map.insert(extension.name(), extension); - } - + renderer: &'a R) -> Engine<'a, S, C, M, U, R> { let enabled = RefCell::new(true); Engine{keyboard_manager, clipboard_manager, config_manager, ui_manager, - extension_map, + renderer, enabled } } @@ -114,8 +108,8 @@ lazy_static! { static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P\\w+)\\s*\\}\\}").unwrap(); } -impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> - MatchReceiver for Engine<'a, S, C, M, U>{ +impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer> + MatchReceiver for Engine<'a, S, C, M, U, R>{ fn on_match(&self, m: &Match, trailing_separator: Option) { let config = self.config_manager.active_config(); @@ -134,40 +128,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa let mut previous_clipboard_content : Option = None; - // Manage the different types of matches - match &m.content { - // Text Match - MatchContentType::Text(content) => { - let mut target_string = if content._has_vars { - let mut output_map = HashMap::new(); - - for variable in content.vars.iter() { - let extension = self.extension_map.get(&variable.var_type); - if let Some(extension) = extension { - let ext_out = extension.calculate(&variable.params); - if let Some(output) = ext_out { - output_map.insert(variable.name.clone(), output); - }else{ - output_map.insert(variable.name.clone(), "".to_owned()); - warn!("Could not generate output for variable: {}", variable.name); - } - }else{ - error!("No extension found for variable type: {}", variable.var_type); - } - } - - // Replace the variables - let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| { - let var_name = caps.name("name").unwrap().as_str(); - let output = output_map.get(var_name); - output.unwrap() - }); - - result.to_string() - }else{ // No variables, simple text substitution - content.replace.clone() - }; + let rendered = self.renderer.render_match(m, config, vec![]); + match rendered { + RenderResult::Text(mut target_string) => { // If a trailing separator was counted in the match, add it back to the target string if let Some(trailing_separator) = trailing_separator { if trailing_separator == '\r' { // If the trailing separator is a carriage return, @@ -234,20 +198,16 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.keyboard_manager.move_cursor_left(moves); } }, + RenderResult::Image(image_path) => { + // If the preserve_clipboard option is enabled, save the current + // clipboard content to restore it later. + previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); - // Image Match - MatchContentType::Image(content) => { - // Make sure the image exist beforehand - if content.path.exists() { - // If the preserve_clipboard option is enabled, save the current - // clipboard content to restore it later. - previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); - - self.clipboard_manager.set_clipboard_image(&content.path); - self.keyboard_manager.trigger_paste(&config.paste_shortcut); - }else{ - error!("Image not found in path: {:?}", content.path); - } + self.clipboard_manager.set_clipboard_image(&image_path); + self.keyboard_manager.trigger_paste(&config.paste_shortcut); + }, + RenderResult::Error => { + error!("Could not render match: {}", m.trigger); }, } @@ -274,7 +234,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa } impl <'a, S: KeyboardManager, C: ClipboardManager, - M: ConfigManager<'a>, U: UIManager> ActionEventReceiver for Engine<'a, S, C, M, U>{ + M: ConfigManager<'a>, U: UIManager, R: Renderer> ActionEventReceiver for Engine<'a, S, C, M, U, R>{ fn on_action_event(&self, e: ActionType) { match e { diff --git a/src/main.rs b/src/main.rs index 5d59a2a..c0e8434 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,7 @@ mod utils; mod bridge; mod engine; mod config; +mod render; mod system; mod context; mod matcher; @@ -332,11 +333,13 @@ fn daemon_background(receive_channel: Receiver, config_set: ConfigSet) { let extensions = extension::get_extensions(); + let renderer = render::default::DefaultRenderer::new(extensions); + let engine = Engine::new(&keyboard_manager, &clipboard_manager, &config_manager, &ui_manager, - extensions, + &renderer, ); let matcher = ScrollingMatcher::new(&config_manager, &engine); diff --git a/src/render/default.rs b/src/render/default.rs new file mode 100644 index 0000000..d0d3f93 --- /dev/null +++ b/src/render/default.rs @@ -0,0 +1,155 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019 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 serde_yaml::{Mapping, Value}; +use std::path::PathBuf; +use std::collections::HashMap; +use regex::{Regex, Captures}; +use log::{warn, error}; +use super::*; +use crate::matcher::{Match, MatchContentType}; +use crate::config::Configs; +use crate::extension::Extension; + +lazy_static! { + static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P\\w+)\\s*\\}\\}").unwrap(); +} + +pub struct DefaultRenderer { + extension_map: HashMap>, +} + +impl DefaultRenderer { + pub fn new(extensions: Vec>) -> DefaultRenderer { + // Register all the extensions + let mut extension_map = HashMap::new(); + for extension in extensions.into_iter() { + extension_map.insert(extension.name(), extension); + } + + DefaultRenderer{ + extension_map + } + } + + fn find_match(config: &Configs, trigger: &str) -> Option { + let mut result = None; + + // TODO: if performances become a problem, implement a more efficient lookup + for m in config.matches.iter() { + if m.trigger == trigger { + result = Some(m.clone()); + break; + } + } + + result + } +} + +impl super::Renderer for DefaultRenderer { + fn render_match(&self, m: &Match, config: &Configs, args: Vec) -> RenderResult { + // Manage the different types of matches + match &m.content { + // Text Match + MatchContentType::Text(content) => { + let mut target_string = if content._has_vars { + let mut output_map = HashMap::new(); + + for variable in content.vars.iter() { + // In case of variables of type match, we need to recursively call + // the render function + if variable.var_type == "match" { + // Extract the match trigger from the variable params + let trigger = variable.params.get(&Value::from("trigger")); + if trigger.is_none() { + warn!("Missing param 'trigger' in match variable: {}", variable.name); + continue; + } + let trigger = trigger.unwrap(); + + // Find the given match from the active configs + let inner_match = DefaultRenderer::find_match(config, trigger.as_str().unwrap_or("")); + + if inner_match.is_none() { + warn!("Could not find inner match with trigger: '{}'", trigger.as_str().unwrap_or("undefined")); + continue + } + + let inner_match = inner_match.unwrap(); + + // Render the inner match + // TODO: inner arguments + let result = self.render_match(&inner_match, config, vec![]); + + // Inner matches are only supported for text-expansions, warn the user otherwise + match result { + RenderResult::Text(inner_content) => { + output_map.insert(variable.name.clone(), inner_content); + }, + _ => { + warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.") + }, + } + }else{ // Normal extension variables + let extension = self.extension_map.get(&variable.var_type); + if let Some(extension) = extension { + let ext_out = extension.calculate(&variable.params); + if let Some(output) = ext_out { + output_map.insert(variable.name.clone(), output); + }else{ + output_map.insert(variable.name.clone(), "".to_owned()); + warn!("Could not generate output for variable: {}", variable.name); + } + }else{ + error!("No extension found for variable type: {}", variable.var_type); + } + } + } + + // Replace the variables + let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| { + let var_name = caps.name("name").unwrap().as_str(); + let output = output_map.get(var_name); + output.unwrap() + }); + + result.to_string() + }else{ // No variables, simple text substitution + content.replace.clone() + }; + + RenderResult::Text(target_string) + }, + + // Image Match + MatchContentType::Image(content) => { + // Make sure the image exist beforehand + if content.path.exists() { + RenderResult::Image(content.path.clone()) + }else{ + error!("Image not found in path: {:?}", content.path); + RenderResult::Error + } + }, + } + } +} + +// TODO: tests \ No newline at end of file diff --git a/src/render/mod.rs b/src/render/mod.rs new file mode 100644 index 0000000..a676f7a --- /dev/null +++ b/src/render/mod.rs @@ -0,0 +1,34 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019 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::PathBuf; +use crate::matcher::{Match}; +use crate::config::Configs; + +pub(crate) mod default; + +pub trait Renderer { + fn render_match(&self, m: &Match, config: &Configs, args: Vec) -> RenderResult; +} + +pub enum RenderResult { + Text(String), + Image(PathBuf), + Error +} \ No newline at end of file