Move match rendering to standalone component. Add support for nested triggers ( Fix #110 )
This commit is contained in:
parent
6b6cac059e
commit
6eec895b21
|
@ -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 {
|
||||
|
|
|
@ -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
155
src/render/default.rs
Normal 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
34
src/render/mod.rs
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user