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::ui::{UIManager, MenuItem, MenuItemType};
|
||||||
use crate::event::{ActionEventReceiver, ActionType};
|
use crate::event::{ActionEventReceiver, ActionType};
|
||||||
use crate::extension::Extension;
|
use crate::extension::Extension;
|
||||||
|
use crate::render::{Renderer, RenderResult};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -33,35 +34,28 @@ use std::path::PathBuf;
|
||||||
use regex::{Regex, Captures};
|
use regex::{Regex, Captures};
|
||||||
|
|
||||||
pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>,
|
pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>,
|
||||||
U: UIManager> {
|
U: UIManager, R: Renderer> {
|
||||||
keyboard_manager: &'a S,
|
keyboard_manager: &'a S,
|
||||||
clipboard_manager: &'a C,
|
clipboard_manager: &'a C,
|
||||||
config_manager: &'a M,
|
config_manager: &'a M,
|
||||||
ui_manager: &'a U,
|
ui_manager: &'a U,
|
||||||
|
renderer: &'a R,
|
||||||
extension_map: HashMap<String, Box<dyn Extension>>,
|
|
||||||
|
|
||||||
enabled: RefCell<bool>,
|
enabled: RefCell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
|
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
||||||
Engine<'a, S, C, M, U> {
|
Engine<'a, S, C, M, U, R> {
|
||||||
pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C,
|
pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C,
|
||||||
config_manager: &'a M, ui_manager: &'a U,
|
config_manager: &'a M, ui_manager: &'a U,
|
||||||
extensions: Vec<Box<dyn Extension>>) -> Engine<'a, S, C, M, U> {
|
renderer: &'a R) -> Engine<'a, S, C, M, U, R> {
|
||||||
// Register all the extensions
|
|
||||||
let mut extension_map = HashMap::new();
|
|
||||||
for extension in extensions.into_iter() {
|
|
||||||
extension_map.insert(extension.name(), extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
let enabled = RefCell::new(true);
|
let enabled = RefCell::new(true);
|
||||||
|
|
||||||
Engine{keyboard_manager,
|
Engine{keyboard_manager,
|
||||||
clipboard_manager,
|
clipboard_manager,
|
||||||
config_manager,
|
config_manager,
|
||||||
ui_manager,
|
ui_manager,
|
||||||
extension_map,
|
renderer,
|
||||||
enabled
|
enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,8 +108,8 @@ lazy_static! {
|
||||||
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
|
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
|
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
||||||
MatchReceiver for Engine<'a, S, C, M, U>{
|
MatchReceiver for Engine<'a, S, C, M, U, R>{
|
||||||
|
|
||||||
fn on_match(&self, m: &Match, trailing_separator: Option<char>) {
|
fn on_match(&self, m: &Match, trailing_separator: Option<char>) {
|
||||||
let config = self.config_manager.active_config();
|
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;
|
let mut previous_clipboard_content : Option<String> = None;
|
||||||
|
|
||||||
// Manage the different types of matches
|
let rendered = self.renderer.render_match(m, config, vec![]);
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
|
match rendered {
|
||||||
|
RenderResult::Text(mut target_string) => {
|
||||||
// If a trailing separator was counted in the match, add it back to the 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 let Some(trailing_separator) = trailing_separator {
|
||||||
if trailing_separator == '\r' { // If the trailing separator is a carriage return,
|
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);
|
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
|
self.clipboard_manager.set_clipboard_image(&image_path);
|
||||||
MatchContentType::Image(content) => {
|
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
|
||||||
// Make sure the image exist beforehand
|
},
|
||||||
if content.path.exists() {
|
RenderResult::Error => {
|
||||||
// If the preserve_clipboard option is enabled, save the current
|
error!("Could not render match: {}", m.trigger);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,7 +234,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'a, S: KeyboardManager, C: ClipboardManager,
|
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) {
|
fn on_action_event(&self, e: ActionType) {
|
||||||
match e {
|
match e {
|
||||||
|
|
|
@ -52,6 +52,7 @@ mod utils;
|
||||||
mod bridge;
|
mod bridge;
|
||||||
mod engine;
|
mod engine;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod render;
|
||||||
mod system;
|
mod system;
|
||||||
mod context;
|
mod context;
|
||||||
mod matcher;
|
mod matcher;
|
||||||
|
@ -332,11 +333,13 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet) {
|
||||||
|
|
||||||
let extensions = extension::get_extensions();
|
let extensions = extension::get_extensions();
|
||||||
|
|
||||||
|
let renderer = render::default::DefaultRenderer::new(extensions);
|
||||||
|
|
||||||
let engine = Engine::new(&keyboard_manager,
|
let engine = Engine::new(&keyboard_manager,
|
||||||
&clipboard_manager,
|
&clipboard_manager,
|
||||||
&config_manager,
|
&config_manager,
|
||||||
&ui_manager,
|
&ui_manager,
|
||||||
extensions,
|
&renderer,
|
||||||
);
|
);
|
||||||
|
|
||||||
let matcher = ScrollingMatcher::new(&config_manager, &engine);
|
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