Move match rendering to standalone component. Add support for nested triggers ( Fix #110 )

This commit is contained in:
Federico Terzi 2019-12-22 00:06:55 +01:00
parent 6b6cac059e
commit 6eec895b21
4 changed files with 215 additions and 63 deletions

View File

@ -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<String, Box<dyn Extension>>,
renderer: &'a R,
enabled: RefCell<bool>,
}
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<Box<dyn Extension>>) -> 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<name>\\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<char>) {
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<String> = 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 {

View File

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

155
src/render/default.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<name>\\w+)\\s*\\}\\}").unwrap();
}
pub struct DefaultRenderer {
extension_map: HashMap<String, Box<dyn Extension>>,
}
impl DefaultRenderer {
pub fn new(extensions: Vec<Box<dyn Extension>>) -> 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<Match> {
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<String>) -> 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

34
src/render/mod.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>) -> RenderResult;
}
pub enum RenderResult {
Text(String),
Image(PathBuf),
Error
}