From 6eec895b2145b3069f1788defd9f984be6617d11 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 22 Dec 2019 00:06:55 +0100
Subject: [PATCH 01/25] 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<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 {
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<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);
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 <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
\ 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 <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
+}
\ No newline at end of file

From 0eb58704a92740be31b2bf36a4c7792507b9d675 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Fri, 10 Jan 2020 23:29:21 +0100
Subject: [PATCH 02/25] First steps in passive match rendering

---
 src/config/mod.rs     |  10 +++
 src/main.rs           |   5 +-
 src/render/default.rs | 182 +++++++++++++++++++++++++++++++++++++++++-
 src/render/mod.rs     |   4 +
 4 files changed, 195 insertions(+), 6 deletions(-)

diff --git a/src/config/mod.rs b/src/config/mod.rs
index c849527..361751b 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -54,6 +54,8 @@ fn default_config_caching_interval() -> i32 { 800 }
 fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8 as char] }
 fn default_toggle_interval() -> u32 { 230 }
 fn default_preserve_clipboard() -> bool {false}
+fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned() }
+fn default_passive_arg_regex() -> String{ "((\\\\/)|[^/])+".to_owned() }
 fn default_backspace_limit() -> i32 { 3 }
 fn default_exclude_default_matches() -> bool {false}
 fn default_matches() -> Vec<Match> { Vec::new() }
@@ -102,6 +104,12 @@ pub struct Configs {
     #[serde(default = "default_preserve_clipboard")]
     pub preserve_clipboard: bool,
 
+    #[serde(default = "default_passive_match_regex")]
+    pub passive_match_regex: String,
+
+    #[serde(default = "default_passive_arg_regex")]
+    pub passive_arg_regex: String,
+
     #[serde(default)]
     pub paste_shortcut: PasteShortcut,
 
@@ -150,6 +158,8 @@ impl Configs {
         validate_field!(result, self.ipc_server_port, default_ipc_server_port());
         validate_field!(result, self.use_system_agent, default_use_system_agent());
         validate_field!(result, self.preserve_clipboard, default_preserve_clipboard());
+        validate_field!(result, self.passive_match_regex, default_passive_match_regex());
+        validate_field!(result, self.passive_arg_regex, default_passive_arg_regex());
 
         result
     }
diff --git a/src/main.rs b/src/main.rs
index c0e8434..12c7561 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -32,7 +32,7 @@ use fs2::FileExt;
 use log::{info, warn, LevelFilter};
 use simplelog::{CombinedLogger, SharedLogger, TerminalMode, TermLogger, WriteLogger};
 
-use crate::config::ConfigSet;
+use crate::config::{ConfigSet, ConfigManager};
 use crate::config::runtime::RuntimeConfigManager;
 use crate::engine::Engine;
 use crate::event::*;
@@ -333,7 +333,8 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet) {
 
     let extensions = extension::get_extensions();
 
-    let renderer = render::default::DefaultRenderer::new(extensions);
+    let renderer = render::default::DefaultRenderer::new(extensions,
+                                                          config_manager.default_config().clone());
 
     let engine = Engine::new(&keyboard_manager,
                              &clipboard_manager,
diff --git a/src/render/default.rs b/src/render/default.rs
index d0d3f93..f1ef5f2 100644
--- a/src/render/default.rs
+++ b/src/render/default.rs
@@ -33,18 +33,36 @@ lazy_static! {
 
 pub struct DefaultRenderer {
     extension_map: HashMap<String, Box<dyn Extension>>,
+
+    // Regex used to identify matches (and arguments) in passive expansions
+    passive_match_regex: Regex,
+
+    // Regex used to separate arguments in passive expansions
+    passive_arg_regex: Regex,
 }
 
 impl DefaultRenderer {
-    pub fn new(extensions: Vec<Box<dyn Extension>>) -> DefaultRenderer {
+    pub fn new(extensions: Vec<Box<dyn Extension>>, config: Configs) -> DefaultRenderer {
         // Register all the extensions
         let mut extension_map = HashMap::new();
         for extension in extensions.into_iter() {
             extension_map.insert(extension.name(), extension);
         }
 
+        // Compile the regexes
+        let passive_match_regex = Regex::new(&config.passive_match_regex)
+                                        .unwrap_or_else(|e| {
+                                            panic!("Invalid passive match regex");
+                                        });
+        let passive_arg_regex = Regex::new(&config.passive_arg_regex)
+                                        .unwrap_or_else(|e| {
+                                            panic!("Invalid passive arg regex");
+                                        });
+
         DefaultRenderer{
-            extension_map
+            extension_map,
+            passive_match_regex,
+            passive_arg_regex
         }
     }
 
@@ -69,7 +87,7 @@ impl super::Renderer for DefaultRenderer {
         match &m.content {
             // Text Match
             MatchContentType::Text(content) => {
-                let mut target_string = if content._has_vars {
+                let target_string = if content._has_vars {
                     let mut output_map = HashMap::new();
 
                     for variable in content.vars.iter() {
@@ -108,6 +126,7 @@ impl super::Renderer for DefaultRenderer {
                                 },
                             }
                         }else{  // Normal extension variables
+                            // TODO: pass the arguments to the extension
                             let extension = self.extension_map.get(&variable.var_type);
                             if let Some(extension) = extension {
                                 let ext_out = extension.calculate(&variable.params);
@@ -123,6 +142,11 @@ impl super::Renderer for DefaultRenderer {
                         }
                     }
 
+                    // TODO: replace the arguments
+                    // the idea is that every param placeholder, such as $1$, is replaced with
+                    // an ArgExtension when loading a match, which renders the argument as output
+                    // this is only used as syntactic sugar
+
                     // Replace the variables
                     let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
                         let var_name = caps.name("name").unwrap().as_str();
@@ -150,6 +174,156 @@ impl super::Renderer for DefaultRenderer {
             },
         }
     }
+
+    fn render_passive(&self, text: &str, config: &Configs) -> RenderResult {
+        // Render the matches
+        let result = self.passive_match_regex.replace_all(&text, |caps: &Captures| {
+            let match_name = if let Some(name) = caps.name("name") {
+                name.as_str()
+            }else{
+                ""
+            };
+
+
+            // Get the original matching string, useful to return the match untouched
+            let original_match = caps.get(0).unwrap().as_str();
+
+            // Find the corresponding match
+            let m = DefaultRenderer::find_match(config, match_name);
+
+            // If no match is found, leave the match without modifications
+            if m.is_none() {
+                return original_match.to_owned();
+            }
+
+            // Compute the args by separating them
+            let match_args = if let Some(args) = caps.name("args") {
+                args.as_str()
+            }else{
+                ""
+            };
+            let args : Vec<String> = self.passive_arg_regex.split(match_args).into_iter().map(
+                |arg| arg.to_owned()
+            ).collect();
+
+            let m = m.unwrap();
+            // Render the actual match
+            let result = self.render_match(&m, &config, args);
+
+            match result {
+                RenderResult::Text(out) => {
+                    out
+                },
+                _ => {
+                    original_match.to_owned()
+                }
+            }
+        });
+
+        RenderResult::Text(result.into_owned())
+    }
 }
 
-// TODO: tests
\ No newline at end of file
+// TESTS
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn get_renderer(config: Configs) -> DefaultRenderer {
+        DefaultRenderer::new(crate::extension::get_extensions(), config)
+    }
+
+    fn get_config_for(s: &str) -> Configs {
+        let config : Configs = serde_yaml::from_str(s).unwrap();
+        config
+    }
+
+    fn verify_render(rendered: RenderResult, target: &str) {
+        match rendered {
+            RenderResult::Text(rendered) => {
+                println!("{}", rendered);
+                assert_eq!(rendered, target);
+            },
+            _ => {
+                assert!(false)
+            }
+        }
+    }
+
+    #[test]
+    fn test_render_passive_no_matches() {
+        let text = r###"
+        this text contains no matches
+        "###;
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: test
+              replace: result
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, text);
+    }
+
+    #[test]
+    fn test_render_passive_simple_match_no_args() {
+        let text = "this is a :test";
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: ':test'
+              replace: result
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "this is a result");
+    }
+
+    #[test]
+    fn test_render_passive_multiple_match_no_args() {
+        let text = "this is a :test and then another :test";
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: ':test'
+              replace: result
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "this is a result and then another result");
+    }
+
+    #[test]
+    fn test_render_passive_simple_match_multiline_no_args() {
+        let text = r###"this is a
+        :test
+        "###;
+
+        let result= r###"this is a
+        result
+        "###;
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: ':test'
+              replace: result
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, result);
+    }
+}
\ No newline at end of file
diff --git a/src/render/mod.rs b/src/render/mod.rs
index a676f7a..d83e982 100644
--- a/src/render/mod.rs
+++ b/src/render/mod.rs
@@ -24,7 +24,11 @@ use crate::config::Configs;
 pub(crate) mod default;
 
 pub trait Renderer {
+    // Render a match output
     fn render_match(&self, m: &Match, config: &Configs, args: Vec<String>) -> RenderResult;
+
+    // Render a passive expansion text
+    fn render_passive(&self, text: &str, config: &Configs) -> RenderResult;
 }
 
 pub enum RenderResult {

From 6378aa3bccc77818bd0cec7107a2672bee652d83 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sat, 18 Jan 2020 22:55:50 +0100
Subject: [PATCH 03/25] Add argument rendering in passive matches

---
 src/config/mod.rs       |  13 ++--
 src/extension/date.rs   |   2 +-
 src/extension/mod.rs    |   2 +-
 src/extension/random.rs |   6 +-
 src/extension/script.rs |   2 +-
 src/extension/shell.rs  |  14 +++--
 src/render/default.rs   | 118 ++++++++++++++++++++++++++++++------
 src/render/mod.rs       |   1 +
 src/render/utils.rs     | 130 ++++++++++++++++++++++++++++++++++++++++
 9 files changed, 255 insertions(+), 33 deletions(-)
 create mode 100644 src/render/utils.rs

diff --git a/src/config/mod.rs b/src/config/mod.rs
index 361751b..32bfb93 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -55,7 +55,8 @@ fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8
 fn default_toggle_interval() -> u32 { 230 }
 fn default_preserve_clipboard() -> bool {false}
 fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned() }
-fn default_passive_arg_regex() -> String{ "((\\\\/)|[^/])+".to_owned() }
+fn default_passive_arg_delimiter() -> char { '/' }
+fn default_passive_arg_escape() -> char { '\\' }
 fn default_backspace_limit() -> i32 { 3 }
 fn default_exclude_default_matches() -> bool {false}
 fn default_matches() -> Vec<Match> { Vec::new() }
@@ -107,8 +108,11 @@ pub struct Configs {
     #[serde(default = "default_passive_match_regex")]
     pub passive_match_regex: String,
 
-    #[serde(default = "default_passive_arg_regex")]
-    pub passive_arg_regex: String,
+    #[serde(default = "default_passive_arg_delimiter")]
+    pub passive_arg_delimiter: char,
+
+    #[serde(default = "default_passive_arg_escape")]
+    pub passive_arg_escape: char,
 
     #[serde(default)]
     pub paste_shortcut: PasteShortcut,
@@ -159,7 +163,8 @@ impl Configs {
         validate_field!(result, self.use_system_agent, default_use_system_agent());
         validate_field!(result, self.preserve_clipboard, default_preserve_clipboard());
         validate_field!(result, self.passive_match_regex, default_passive_match_regex());
-        validate_field!(result, self.passive_arg_regex, default_passive_arg_regex());
+        validate_field!(result, self.passive_arg_delimiter, default_passive_arg_delimiter());
+        validate_field!(result, self.passive_arg_escape, default_passive_arg_escape());
 
         result
     }
diff --git a/src/extension/date.rs b/src/extension/date.rs
index 6c81924..431b4e5 100644
--- a/src/extension/date.rs
+++ b/src/extension/date.rs
@@ -33,7 +33,7 @@ impl super::Extension for DateExtension {
         String::from("date")
     }
 
-    fn calculate(&self, params: &Mapping) -> Option<String> {
+    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {
         let now: DateTime<Local> = Local::now();
 
         let format = params.get(&Value::from("format"));
diff --git a/src/extension/mod.rs b/src/extension/mod.rs
index 4c0f11e..ccb6934 100644
--- a/src/extension/mod.rs
+++ b/src/extension/mod.rs
@@ -26,7 +26,7 @@ mod random;
 
 pub trait Extension {
     fn name(&self) -> String;
-    fn calculate(&self, params: &Mapping) -> Option<String>;
+    fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String>;
 }
 
 pub fn get_extensions() -> Vec<Box<dyn Extension>> {
diff --git a/src/extension/random.rs b/src/extension/random.rs
index e44a2e6..909a696 100644
--- a/src/extension/random.rs
+++ b/src/extension/random.rs
@@ -34,7 +34,7 @@ impl super::Extension for RandomExtension {
         String::from("random")
     }
 
-    fn calculate(&self, params: &Mapping) -> Option<String> {
+    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {  // TODO: add argument handling
         let choices = params.get(&Value::from("choices"));
         if choices.is_none() {
             warn!("No 'choices' parameter specified for random variable");
@@ -82,7 +82,7 @@ mod tests {
         params.insert(Value::from("choices"), Value::from(choices.clone()));
 
         let extension = RandomExtension::new();
-        let output = extension.calculate(&params);
+        let output = extension.calculate(&params, &vec![]);
 
         assert!(output.is_some());
 
@@ -90,4 +90,6 @@ mod tests {
 
         assert!(choices.iter().any(|x| x == &output));
     }
+
+    // TODO: add test with arguments
 }
\ No newline at end of file
diff --git a/src/extension/script.rs b/src/extension/script.rs
index a5519aa..ecbc1bd 100644
--- a/src/extension/script.rs
+++ b/src/extension/script.rs
@@ -34,7 +34,7 @@ impl super::Extension for ScriptExtension {
         String::from("script")
     }
 
-    fn calculate(&self, params: &Mapping) -> Option<String> {
+    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {  // TODO: add argument handling
         let args = params.get(&Value::from("args"));
         if args.is_none() {
             warn!("No 'args' parameter specified for script variable");
diff --git a/src/extension/shell.rs b/src/extension/shell.rs
index aa1012a..9736628 100644
--- a/src/extension/shell.rs
+++ b/src/extension/shell.rs
@@ -34,7 +34,7 @@ impl super::Extension for ShellExtension {
         String::from("shell")
     }
 
-    fn calculate(&self, params: &Mapping) -> Option<String> {
+    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {  // TODO: add argument handling
         let cmd = params.get(&Value::from("cmd"));
         if cmd.is_none() {
             warn!("No 'cmd' parameter specified for shell variable");
@@ -90,7 +90,7 @@ mod tests {
         params.insert(Value::from("cmd"), Value::from("echo hello world"));
 
         let extension = ShellExtension::new();
-        let output = extension.calculate(&params);
+        let output = extension.calculate(&params, &vec![]);
 
         assert!(output.is_some());
 
@@ -108,7 +108,7 @@ mod tests {
         params.insert(Value::from("trim"), Value::from(true));
 
         let extension = ShellExtension::new();
-        let output = extension.calculate(&params);
+        let output = extension.calculate(&params, &vec![]);
 
         assert!(output.is_some());
         assert_eq!(output.unwrap(), "hello world");
@@ -126,7 +126,7 @@ mod tests {
         params.insert(Value::from("trim"), Value::from(true));
 
         let extension = ShellExtension::new();
-        let output = extension.calculate(&params);
+        let output = extension.calculate(&params, &vec![]);
 
         assert!(output.is_some());
         assert_eq!(output.unwrap(), "hello world");
@@ -139,7 +139,7 @@ mod tests {
         params.insert(Value::from("trim"), Value::from("error"));
 
         let extension = ShellExtension::new();
-        let output = extension.calculate(&params);
+        let output = extension.calculate(&params, &vec![]);
 
         assert!(output.is_some());
         if cfg!(target_os = "windows") {
@@ -157,9 +157,11 @@ mod tests {
         params.insert(Value::from("trim"), Value::from(true));
 
         let extension = ShellExtension::new();
-        let output = extension.calculate(&params);
+        let output = extension.calculate(&params, &vec![]);
 
         assert!(output.is_some());
         assert_eq!(output.unwrap(), "hello world");
     }
+
+    // TODO: add tests with arguments
 }
\ No newline at end of file
diff --git a/src/render/default.rs b/src/render/default.rs
index f1ef5f2..2663c0b 100644
--- a/src/render/default.rs
+++ b/src/render/default.rs
@@ -36,9 +36,6 @@ pub struct DefaultRenderer {
 
     // Regex used to identify matches (and arguments) in passive expansions
     passive_match_regex: Regex,
-
-    // Regex used to separate arguments in passive expansions
-    passive_arg_regex: Regex,
 }
 
 impl DefaultRenderer {
@@ -54,15 +51,10 @@ impl DefaultRenderer {
                                         .unwrap_or_else(|e| {
                                             panic!("Invalid passive match regex");
                                         });
-        let passive_arg_regex = Regex::new(&config.passive_arg_regex)
-                                        .unwrap_or_else(|e| {
-                                            panic!("Invalid passive arg regex");
-                                        });
 
         DefaultRenderer{
             extension_map,
             passive_match_regex,
-            passive_arg_regex
         }
     }
 
@@ -129,7 +121,7 @@ impl super::Renderer for DefaultRenderer {
                             // TODO: pass the arguments to the extension
                             let extension = self.extension_map.get(&variable.var_type);
                             if let Some(extension) = extension {
-                                let ext_out = extension.calculate(&variable.params);
+                                let ext_out = extension.calculate(&variable.params, &args);
                                 if let Some(output) = ext_out {
                                     output_map.insert(variable.name.clone(), output);
                                 }else{
@@ -142,11 +134,6 @@ impl super::Renderer for DefaultRenderer {
                         }
                     }
 
-                    // TODO: replace the arguments
-                    // the idea is that every param placeholder, such as $1$, is replaced with
-                    // an ArgExtension when loading a match, which renders the argument as output
-                    // this is only used as syntactic sugar
-
                     // Replace the variables
                     let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
                         let var_name = caps.name("name").unwrap().as_str();
@@ -159,6 +146,9 @@ impl super::Renderer for DefaultRenderer {
                     content.replace.clone()
                 };
 
+                // Render any argument that may be present
+                let target_string = utils::render_args(&target_string, &args);
+
                 RenderResult::Text(target_string)
             },
 
@@ -202,9 +192,9 @@ impl super::Renderer for DefaultRenderer {
             }else{
                 ""
             };
-            let args : Vec<String> = self.passive_arg_regex.split(match_args).into_iter().map(
-                |arg| arg.to_owned()
-            ).collect();
+            let args : Vec<String> = utils::split_args(match_args,
+                                                       config.passive_arg_delimiter,
+                                                       config.passive_arg_escape);
 
             let m = m.unwrap();
             // Render the actual match
@@ -242,7 +232,6 @@ mod tests {
     fn verify_render(rendered: RenderResult, target: &str) {
         match rendered {
             RenderResult::Text(rendered) => {
-                println!("{}", rendered);
                 assert_eq!(rendered, target);
             },
             _ => {
@@ -326,4 +315,97 @@ mod tests {
 
         verify_render(rendered, result);
     }
+
+    #[test]
+    fn test_render_passive_nested_matches_no_args() {
+        let text = ":greet";
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: ':greet'
+              replace: "hi {{name}}"
+              vars:
+                - name: name
+                  type: match
+                  params:
+                    trigger: ":name"
+
+            - trigger: ':name'
+              replace: john
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "hi john");
+    }
+
+    #[test]
+    fn test_render_passive_simple_match_with_args() {
+        let text = ":greet/Jon/";
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: ':greet'
+              replace: "Hi $0$"
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "Hi Jon");
+    }
+
+    #[test]
+    fn test_render_passive_simple_match_with_multiple_args() {
+        let text = ":greet/Jon/Snow/";
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: ':greet'
+              replace: "Hi $0$, there is $1$ outside"
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "Hi Jon, there is Snow outside");
+    }
+
+    #[test]
+    fn test_render_passive_simple_match_with_escaped_args() {
+        let text = ":greet/Jon/10\\/12/";
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: ':greet'
+              replace: "Hi $0$, today is $1$"
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "Hi Jon, today is 10/12");
+    }
+
+    #[test]
+    fn test_render_passive_simple_match_with_args_not_closed() {
+        let text = ":greet/Jon/Snow";
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: ':greet'
+              replace: "Hi $0$"
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "Hi JonSnow");
+    }
 }
\ No newline at end of file
diff --git a/src/render/mod.rs b/src/render/mod.rs
index d83e982..80bf645 100644
--- a/src/render/mod.rs
+++ b/src/render/mod.rs
@@ -22,6 +22,7 @@ use crate::matcher::{Match};
 use crate::config::Configs;
 
 pub(crate) mod default;
+pub(crate) mod utils;
 
 pub trait Renderer {
     // Render a match output
diff --git a/src/render/utils.rs b/src/render/utils.rs
new file mode 100644
index 0000000..fe486b6
--- /dev/null
+++ b/src/render/utils.rs
@@ -0,0 +1,130 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2020 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 regex::{Regex, Captures};
+
+lazy_static! {
+    static ref ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)\\$").unwrap();
+}
+
+pub fn render_args(text: &str, args: &Vec<String>) -> String {
+    let result = ARG_REGEX.replace_all(text, |caps: &Captures| {
+        let position_str  = caps.name("pos").unwrap().as_str();
+        let position = position_str.parse::<i32>().unwrap_or(-1);
+
+        if position >= 0 && position < args.len() as i32 {
+            args[position as usize].to_owned()
+        }else{
+            "".to_owned()
+        }
+    });
+
+    result.to_string()
+}
+
+pub fn split_args(text: &str, delimiter: char, escape: char) -> Vec<String> {
+    let mut output = vec![];
+
+    // Make sure the text is not empty
+    if text.is_empty() {
+        return output
+    }
+
+    let mut last = String::from("");
+    let mut previous : char = char::from(0);
+    text.chars().into_iter().for_each(|c| {
+        if c == delimiter {
+            if previous != escape {
+                output.push(last.clone());
+                last = String::from("");
+            }else{
+                last.push(c);
+            }
+        }else if c == escape {
+            if previous == escape {
+                last.push(c);
+            }
+        }else{
+            last.push(c);
+        }
+        previous = c;
+    });
+
+    // Add the last one
+    output.push(last);
+
+    output
+}
+
+// TESTS
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_render_args_no_args() {
+        let args = vec!("hello".to_owned());
+        assert_eq!(render_args("no args", &args), "no args")
+    }
+
+    #[test]
+    fn test_render_args_one_arg() {
+        let args = vec!("jon".to_owned());
+        assert_eq!(render_args("hello $0$", &args), "hello jon")
+    }
+
+    #[test]
+    fn test_render_args_one_multiple_args() {
+        let args = vec!("jon".to_owned(), "snow".to_owned());
+        assert_eq!(render_args("hello $0$, the $1$ is white", &args), "hello jon, the snow is white")
+    }
+
+    #[test]
+    fn test_render_args_out_of_range() {
+        let args = vec!("jon".to_owned());
+        assert_eq!(render_args("hello $10$", &args), "hello ")
+    }
+
+    #[test]
+    fn test_split_args_one_arg() {
+        assert_eq!(split_args("jon", '/', '\\'), vec!["jon"])
+    }
+
+    #[test]
+    fn test_split_args_two_args() {
+        assert_eq!(split_args("jon/snow", '/', '\\'), vec!["jon", "snow"])
+    }
+
+    #[test]
+    fn test_split_args_escaping() {
+        assert_eq!(split_args("jon\\/snow", '/', '\\'), vec!["jon/snow"])
+    }
+
+    #[test]
+    fn test_split_args_escaping_escape() {
+        assert_eq!(split_args("jon\\\\snow", '/', '\\'), vec!["jon\\snow"])
+    }
+
+    #[test]
+    fn test_split_args_empty() {
+        let empty_vec : Vec<String> = vec![];
+        assert_eq!(split_args("", '/', '\\'), empty_vec)
+    }
+}
\ No newline at end of file

From 9e5a2a7c95d3e5106d9a576a3136f8127a46625f Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sat, 18 Jan 2020 23:33:02 +0100
Subject: [PATCH 04/25] Add argument handling in extensions

---
 src/extension/random.rs | 33 ++++++++++++++++++--
 src/extension/script.rs | 69 +++++++++++++++++++++++++++++++++++++++--
 src/extension/shell.rs  | 54 +++++++++++++++++++++++++++++---
 3 files changed, 147 insertions(+), 9 deletions(-)

diff --git a/src/extension/random.rs b/src/extension/random.rs
index 909a696..6923a6e 100644
--- a/src/extension/random.rs
+++ b/src/extension/random.rs
@@ -34,7 +34,7 @@ impl super::Extension for RandomExtension {
         String::from("random")
     }
 
-    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {  // TODO: add argument handling
+    fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String> {
         let choices = params.get(&Value::from("choices"));
         if choices.is_none() {
             warn!("No 'choices' parameter specified for random variable");
@@ -51,7 +51,10 @@ impl super::Extension for RandomExtension {
 
             match choice {
                 Some(output) => {
-                    return Some(output.clone())
+                    // Render arguments
+                    let output = crate::render::utils::render_args(output, args);
+
+                    return Some(output)
                 },
                 None => {
                     error!("Could not select a random choice.");
@@ -91,5 +94,29 @@ mod tests {
         assert!(choices.iter().any(|x| x == &output));
     }
 
-    // TODO: add test with arguments
+    #[test]
+    fn test_random_with_args() {
+        let mut params = Mapping::new();
+        let choices = vec!(
+            "first $0$",
+            "second $0$",
+            "$0$ third",
+        );
+        params.insert(Value::from("choices"), Value::from(choices.clone()));
+
+        let extension = RandomExtension::new();
+        let output = extension.calculate(&params, &vec!["test".to_owned()]);
+
+        assert!(output.is_some());
+
+        let output = output.unwrap();
+
+        let rendered_choices = vec!(
+            "first test",
+            "second test",
+            "test third",
+        );
+
+        assert!(rendered_choices.iter().any(|x| x == &output));
+    }
 }
\ No newline at end of file
diff --git a/src/extension/script.rs b/src/extension/script.rs
index ecbc1bd..554fdf5 100644
--- a/src/extension/script.rs
+++ b/src/extension/script.rs
@@ -34,7 +34,7 @@ impl super::Extension for ScriptExtension {
         String::from("script")
     }
 
-    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {  // TODO: add argument handling
+    fn calculate(&self, params: &Mapping, user_args: &Vec<String>) -> Option<String> {
         let args = params.get(&Value::from("args"));
         if args.is_none() {
             warn!("No 'args' parameter specified for script variable");
@@ -42,10 +42,17 @@ impl super::Extension for ScriptExtension {
         }
         let args = args.unwrap().as_sequence();
         if let Some(args) = args {
-            let str_args = args.iter().map(|arg| {
+            let mut str_args = args.iter().map(|arg| {
                arg.as_str().unwrap_or_default().to_string()
             }).collect::<Vec<String>>();
 
+            // The user has to enable argument concatenation explicitly
+            let inject_args = params.get(&Value::from("inject_args"))
+                .unwrap_or(&Value::from(false)).as_bool().unwrap_or(false);
+            if inject_args {
+                str_args.extend(user_args.clone());
+            }
+
             let output = if str_args.len() > 1 {
                 Command::new(&str_args[0])
                     .args(&str_args[1..])
@@ -71,4 +78,62 @@ impl super::Extension for ScriptExtension {
         error!("Could not execute script with args '{:?}'", args);
         None
     }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::extension::Extension;
+
+    #[test]
+    fn test_script_basic() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"]));
+
+        let extension = ScriptExtension::new();
+        let output = extension.calculate(&params, &vec![]);
+
+        assert!(output.is_some());
+
+        if cfg!(target_os = "windows") {
+            assert_eq!(output.unwrap(), "hello world\r\n");
+        }else{
+            assert_eq!(output.unwrap(), "hello world\n");
+        }
+    }
+
+    #[test]
+    fn test_script_inject_args_off() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"]));
+
+        let extension = ScriptExtension::new();
+        let output = extension.calculate(&params, &vec!["jon".to_owned()]);
+
+        assert!(output.is_some());
+
+        if cfg!(target_os = "windows") {
+            assert_eq!(output.unwrap(), "hello world\r\n");
+        }else{
+            assert_eq!(output.unwrap(), "hello world\n");
+        }
+    }
+
+    #[test]
+    fn test_script_inject_args_on() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"]));
+        params.insert(Value::from("inject_args"), Value::from(true));
+
+        let extension = ScriptExtension::new();
+        let output = extension.calculate(&params, &vec!["jon".to_owned()]);
+
+        assert!(output.is_some());
+
+        if cfg!(target_os = "windows") {
+            assert_eq!(output.unwrap(), "hello world jon\r\n");
+        }else{
+            assert_eq!(output.unwrap(), "hello world jon\n");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/extension/shell.rs b/src/extension/shell.rs
index 9736628..f8e014a 100644
--- a/src/extension/shell.rs
+++ b/src/extension/shell.rs
@@ -20,6 +20,15 @@
 use serde_yaml::{Mapping, Value};
 use std::process::Command;
 use log::{warn, error};
+use regex::{Regex, Captures};
+
+lazy_static! {
+    static ref POS_ARG_REGEX: Regex = if cfg!(target_os = "windows") {
+        Regex::new("\\%(?P<pos>\\d+)").unwrap()
+    }else{
+        Regex::new("\\$(?P<pos>\\d+)").unwrap()
+    };
+}
 
 pub struct ShellExtension {}
 
@@ -34,7 +43,7 @@ impl super::Extension for ShellExtension {
         String::from("shell")
     }
 
-    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {  // TODO: add argument handling
+    fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String> {
         let cmd = params.get(&Value::from("cmd"));
         if cmd.is_none() {
             warn!("No 'cmd' parameter specified for shell variable");
@@ -42,14 +51,25 @@ impl super::Extension for ShellExtension {
         }
         let cmd = cmd.unwrap().as_str().unwrap();
 
+        // Render positional parameters in args
+        let cmd = POS_ARG_REGEX.replace_all(&cmd, |caps: &Captures| {
+            let position_str  = caps.name("pos").unwrap().as_str();
+            let position = position_str.parse::<i32>().unwrap_or(-1);
+            if position >= 0 && position < args.len() as i32 {
+                args[position as usize].to_owned()
+            }else{
+                "".to_owned()
+            }
+        }).to_string();
+
         let output = if cfg!(target_os = "windows") {
             Command::new("cmd")
-                .args(&["/C", cmd])
+                .args(&["/C", &cmd])
                 .output()
         } else {
             Command::new("sh")
                 .arg("-c")
-                .arg(cmd)
+                .arg(&cmd)
                 .output()
         };
 
@@ -163,5 +183,31 @@ mod tests {
         assert_eq!(output.unwrap(), "hello world");
     }
 
-    // TODO: add tests with arguments
+    #[test]
+    #[cfg(not(target_os = "windows"))]
+    fn test_shell_args_unix() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("cmd"), Value::from("echo $0"));
+
+        let extension = ShellExtension::new();
+        let output = extension.calculate(&params, &vec!["hello".to_owned()]);
+
+        assert!(output.is_some());
+
+        assert_eq!(output.unwrap(), "hello\n");
+    }
+
+    #[test]
+    #[cfg(target_os = "windows")]
+    fn test_shell_args_windows() {
+        let mut params = Mapping::new();
+        params.insert(Value::from("cmd"), Value::from("echo %0"));
+
+        let extension = ShellExtension::new();
+        let output = extension.calculate(&params, &vec!["hello".to_owned()]);
+
+        assert!(output.is_some());
+
+        assert_eq!(output.unwrap(), "hello\r\n");
+    }
 }
\ No newline at end of file

From 9332899969ce28d2649ff79b892c153a6dd7f30f Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 19 Jan 2020 00:30:30 +0100
Subject: [PATCH 05/25] First draft of working passive mode on linux

---
 native/liblinuxbridge/bridge.cpp |  8 ++++++
 native/liblinuxbridge/bridge.h   |  4 +++
 src/bridge/linux.rs              |  1 +
 src/config/mod.rs                | 10 ++++++--
 src/engine.rs                    | 33 ++++++++++++++++++++++++
 src/event/mod.rs                 |  6 -----
 src/keyboard/linux.rs            |  6 +++++
 src/keyboard/mod.rs              |  1 +
 src/matcher/mod.rs               |  1 +
 src/matcher/scrolling.rs         | 43 ++++++++++++++++++++++----------
 10 files changed, 92 insertions(+), 21 deletions(-)

diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp
index a34661a..beb9646 100644
--- a/native/liblinuxbridge/bridge.cpp
+++ b/native/liblinuxbridge/bridge.cpp
@@ -307,6 +307,14 @@ void trigger_alt_shift_ins_paste() {
     xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Alt+Insert", 8000);
 }
 
+void trigger_copy() {
+    // Release the other keys, for an explanation, read the 'trigger_paste' method
+
+    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Alt", 8000);
+    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift", 8000);
+    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+c", 8000);
+}
+
 // SYSTEM MODULE
 
 // Function taken from the wmlib tool source code
diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h
index 6e921bf..2389cf8 100644
--- a/native/liblinuxbridge/bridge.h
+++ b/native/liblinuxbridge/bridge.h
@@ -92,6 +92,10 @@ extern "C" void trigger_shift_ins_paste();
  */
 extern "C" void trigger_alt_shift_ins_paste();
 
+/*
+ * Trigger copy shortcut ( Pressing CTRL+C )
+ */
+extern "C" void trigger_copy();
 
 // SYSTEM MODULE
 
diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs
index 90edfc6..967aa9d 100644
--- a/src/bridge/linux.rs
+++ b/src/bridge/linux.rs
@@ -44,4 +44,5 @@ extern {
     pub fn trigger_terminal_paste();
     pub fn trigger_shift_ins_paste();
     pub fn trigger_alt_shift_ins_paste();
+    pub fn trigger_copy();
 }
\ No newline at end of file
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 32bfb93..166b6bc 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -53,10 +53,12 @@ fn default_use_system_agent() -> bool { true }
 fn default_config_caching_interval() -> i32 { 800 }
 fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8 as char] }
 fn default_toggle_interval() -> u32 { 230 }
+fn default_toggle_key() -> KeyModifier { KeyModifier::ALT }
 fn default_preserve_clipboard() -> bool {false}
 fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned() }
 fn default_passive_arg_delimiter() -> char { '/' }
 fn default_passive_arg_escape() -> char { '\\' }
+fn default_passive_key() -> KeyModifier { KeyModifier::OFF }
 fn default_backspace_limit() -> i32 { 3 }
 fn default_exclude_default_matches() -> bool {false}
 fn default_matches() -> Vec<Match> { Vec::new() }
@@ -96,7 +98,7 @@ pub struct Configs {
     #[serde(default = "default_word_separators")]
     pub word_separators: Vec<char>,  // TODO: add parsing test
 
-    #[serde(default)]
+    #[serde(default = "default_toggle_key")]
     pub toggle_key: KeyModifier,
 
     #[serde(default = "default_toggle_interval")]
@@ -114,6 +116,9 @@ pub struct Configs {
     #[serde(default = "default_passive_arg_escape")]
     pub passive_arg_escape: char,
 
+    #[serde(default = "default_passive_key")]
+    pub passive_key: KeyModifier,
+
     #[serde(default)]
     pub paste_shortcut: PasteShortcut,
 
@@ -156,7 +161,7 @@ impl Configs {
 
         validate_field!(result, self.config_caching_interval, default_config_caching_interval());
         validate_field!(result, self.log_level, default_log_level());
-        validate_field!(result, self.toggle_key, KeyModifier::default());
+        validate_field!(result, self.toggle_key, default_toggle_key());
         validate_field!(result, self.toggle_interval, default_toggle_interval());
         validate_field!(result, self.backspace_limit, default_backspace_limit());
         validate_field!(result, self.ipc_server_port, default_ipc_server_port());
@@ -165,6 +170,7 @@ impl Configs {
         validate_field!(result, self.passive_match_regex, default_passive_match_regex());
         validate_field!(result, self.passive_arg_delimiter, default_passive_arg_delimiter());
         validate_field!(result, self.passive_arg_escape, default_passive_arg_escape());
+        validate_field!(result, self.passive_key, default_passive_key());
 
         result
     }
diff --git a/src/engine.rs b/src/engine.rs
index 7cfd507..7ef9ee8 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -231,6 +231,39 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
 
         self.ui_manager.notify(message);
     }
+
+    fn on_passive(&self) {
+        info!("Passive mode activated");
+
+        // Trigger a copy shortcut to transfer the content of the selection to the clipboard
+        self.keyboard_manager.trigger_copy();
+
+        // Sleep for a while, giving time to effectively copy the text
+        std::thread::sleep(std::time::Duration::from_millis(100));  // TODO: avoid hardcoding
+
+        // Then get the text from the clipboard and render the match output
+        let clipboard = self.clipboard_manager.get_clipboard();
+
+        if let Some(clipboard) = clipboard {
+            let config = self.config_manager.active_config();
+
+            let rendered = self.renderer.render_passive(&clipboard,
+                                                        &config);
+
+            match rendered {
+                RenderResult::Text(payload) => {
+                    // Paste back the result in the field
+                    self.clipboard_manager.set_clipboard(&payload);
+
+                    std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
+                    self.keyboard_manager.trigger_paste(&config.paste_shortcut);
+                },
+                _ => {
+                    warn!("Cannot expand passive match")
+                },
+            }
+        }
+    }
 }
 
 impl <'a, S: KeyboardManager, C: ClipboardManager,
diff --git a/src/event/mod.rs b/src/event/mod.rs
index bc9828a..dc47691 100644
--- a/src/event/mod.rs
+++ b/src/event/mod.rs
@@ -66,12 +66,6 @@ pub enum KeyModifier {
     OFF,
 }
 
-impl Default for KeyModifier {
-    fn default() -> Self {
-        KeyModifier::ALT
-    }
-}
-
 // Receivers
 
 pub trait KeyEventReceiver {
diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs
index 538c375..78e0961 100644
--- a/src/keyboard/linux.rs
+++ b/src/keyboard/linux.rs
@@ -81,4 +81,10 @@ impl super::KeyboardManager for LinuxKeyboardManager {
             left_arrow(count);
         }
     }
+
+    fn trigger_copy(&self) {
+        unsafe {
+            trigger_copy();
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs
index d8f2611..66ebe77 100644
--- a/src/keyboard/mod.rs
+++ b/src/keyboard/mod.rs
@@ -34,6 +34,7 @@ pub trait KeyboardManager {
     fn trigger_paste(&self, shortcut: &PasteShortcut);
     fn delete_string(&self, count: i32);
     fn move_cursor_left(&self, count: i32);
+    fn trigger_copy(&self);
 }
 
 #[derive(Debug, Serialize, Deserialize, Clone)]
diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs
index c1ac263..a8f0ffa 100644
--- a/src/matcher/mod.rs
+++ b/src/matcher/mod.rs
@@ -181,6 +181,7 @@ pub enum TriggerEntry {
 pub trait MatchReceiver {
     fn on_match(&self, m: &Match, trailing_separator: Option<char>);
     fn on_enable_update(&self, status: bool);
+    fn on_passive(&self);
 }
 
 pub trait Matcher : KeyEventReceiver {
diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs
index 2d59b19..9c70fcd 100644
--- a/src/matcher/scrolling.rs
+++ b/src/matcher/scrolling.rs
@@ -18,7 +18,7 @@
  */
 
 use crate::matcher::{Match, MatchReceiver, TriggerEntry};
-use std::cell::RefCell;
+use std::cell::{RefCell, Ref};
 use crate::event::{KeyModifier, ActionEventReceiver, ActionType};
 use crate::config::ConfigManager;
 use crate::event::KeyModifier::BACKSPACE;
@@ -30,6 +30,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
     receiver: &'a R,
     current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>,
     toggle_press_time: RefCell<SystemTime>,
+    passive_press_time: RefCell<SystemTime>,
     is_enabled: RefCell<bool>,
     was_previous_char_word_separator: RefCell<bool>,
 }
@@ -45,12 +46,14 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
     pub fn new(config_manager: &'a M, receiver: &'a R) -> ScrollingMatcher<'a, R, M> {
         let current_set_queue = RefCell::new(VecDeque::new());
         let toggle_press_time = RefCell::new(SystemTime::now());
+        let passive_press_time = RefCell::new(SystemTime::now());
 
         ScrollingMatcher{
             config_manager,
             receiver,
             current_set_queue,
             toggle_press_time,
+            passive_press_time,
             is_enabled: RefCell::new(true),
             was_previous_char_word_separator: RefCell::new(true),
         }
@@ -193,22 +196,25 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
     fn handle_modifier(&self, m: KeyModifier) {
         let config = self.config_manager.default_config();
 
+        // TODO: at the moment, activating the passive key triggers the toggle key
+        // study a mechanism to avoid this problem
+
         if m == config.toggle_key {
-            if m == KeyModifier::OFF { return }
-            let mut toggle_press_time = self.toggle_press_time.borrow_mut();
-            if let Ok(elapsed) = toggle_press_time.elapsed() {
-                if elapsed.as_millis() < u128::from(config.toggle_interval) {
-                    self.toggle();
+            check_interval(&self.toggle_press_time,
+                           u128::from(config.toggle_interval), || {
+                self.toggle();
 
-                    let is_enabled = self.is_enabled.borrow();
+                let is_enabled = self.is_enabled.borrow();
 
-                    if !*is_enabled {
-                        self.current_set_queue.borrow_mut().clear();
-                    }
+                if !*is_enabled {
+                    self.current_set_queue.borrow_mut().clear();
                 }
-            }
-
-            (*toggle_press_time) = SystemTime::now();
+            });
+        }else if m == config.passive_key {
+            check_interval(&self.passive_press_time,
+                           u128::from(config.toggle_interval), || {
+                self.receiver.on_passive();
+            });
         }
 
         // Backspace handling, basically "rewinding history"
@@ -234,4 +240,15 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ActionEventReceiver for Scroll
             _ => {}
         }
     }
+}
+
+fn check_interval<F>(state_var: &RefCell<SystemTime>, interval: u128, elapsed_callback: F) where F:Fn() {
+    let mut press_time = state_var.borrow_mut();
+    if let Ok(elapsed) = press_time.elapsed() {
+        if elapsed.as_millis() < interval {
+            elapsed_callback();
+        }
+    }
+
+    (*press_time) = SystemTime::now();
 }
\ No newline at end of file

From e2c98284b6faf6d19c0d922ed68bdadc6cf5e468 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 19 Jan 2020 23:41:11 +0100
Subject: [PATCH 06/25] Add check to avoid espanso reinterpreting its own
 actions

---
 src/config/mod.rs |  5 +++++
 src/engine.rs     | 38 +++++++++++++++++++++++++++++++++++++-
 2 files changed, 42 insertions(+), 1 deletion(-)

diff --git a/src/config/mod.rs b/src/config/mod.rs
index 166b6bc..96e6a8f 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -59,6 +59,7 @@ fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?
 fn default_passive_arg_delimiter() -> char { '/' }
 fn default_passive_arg_escape() -> char { '\\' }
 fn default_passive_key() -> KeyModifier { KeyModifier::OFF }
+fn default_action_noop_interval() -> u128 { 500 }
 fn default_backspace_limit() -> i32 { 3 }
 fn default_exclude_default_matches() -> bool {false}
 fn default_matches() -> Vec<Match> { Vec::new() }
@@ -119,6 +120,9 @@ pub struct Configs {
     #[serde(default = "default_passive_key")]
     pub passive_key: KeyModifier,
 
+    #[serde(default = "default_action_noop_interval")]
+    pub action_noop_interval: u128,
+
     #[serde(default)]
     pub paste_shortcut: PasteShortcut,
 
@@ -171,6 +175,7 @@ impl Configs {
         validate_field!(result, self.passive_arg_delimiter, default_passive_arg_delimiter());
         validate_field!(result, self.passive_arg_escape, default_passive_arg_escape());
         validate_field!(result, self.passive_key, default_passive_key());
+        validate_field!(result, self.action_noop_interval, default_action_noop_interval());
 
         result
     }
diff --git a/src/engine.rs b/src/engine.rs
index 7ef9ee8..4145d48 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -32,6 +32,7 @@ use std::process::exit;
 use std::collections::HashMap;
 use std::path::PathBuf;
 use regex::{Regex, Captures};
+use std::time::SystemTime;
 
 pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>,
                   U: UIManager, R: Renderer> {
@@ -42,6 +43,8 @@ pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<
     renderer: &'a R,
 
     enabled: RefCell<bool>,
+    last_action_time: RefCell<SystemTime>,  // Used to block espanso from re-interpreting it's own inputs
+    action_noop_interval: u128,
 }
 
 impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
@@ -50,13 +53,17 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
                config_manager: &'a M, ui_manager: &'a U,
                renderer: &'a R) -> Engine<'a, S, C, M, U, R> {
         let enabled = RefCell::new(true);
+        let last_action_time = RefCell::new(SystemTime::now());
+        let action_noop_interval = config_manager.default_config().action_noop_interval;
 
         Engine{keyboard_manager,
             clipboard_manager,
             config_manager,
             ui_manager,
             renderer,
-            enabled
+            enabled,
+            last_action_time,
+            action_noop_interval,
         }
     }
 
@@ -102,6 +109,20 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
             None
         }
     }
+
+    /// Used to check if the last action has been executed within a specified interval.
+    /// If so, return true (blocking the action), otherwise false.
+    fn check_last_action_and_set(&self, interval: u128) -> bool {
+        let mut last_action_time = self.last_action_time.borrow_mut();
+        if let Ok(elapsed) = last_action_time.elapsed() {
+            if elapsed.as_millis() < interval {
+                return true;
+            }
+        }
+
+        (*last_action_time) = SystemTime::now();
+        return false;
+    }
 }
 
 lazy_static! {
@@ -118,6 +139,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
             return;
         }
 
+        // avoid espanso reinterpreting its own actions
+        if self.check_last_action_and_set(self.action_noop_interval) {
+            return;
+        }
+
         let char_count = if trailing_separator.is_none() {
             m.trigger.chars().count() as i32
         }else{
@@ -218,6 +244,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
     }
 
     fn on_enable_update(&self, status: bool) {
+        // avoid espanso reinterpreting its own actions
+        if self.check_last_action_and_set(self.action_noop_interval) {
+            return;
+        }
+
         let message = if status {
             "espanso enabled"
         }else{
@@ -233,6 +264,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
     }
 
     fn on_passive(&self) {
+        // avoid espanso reinterpreting its own actions
+        if self.check_last_action_and_set(self.action_noop_interval) {
+            return;
+        }
+
         info!("Passive mode activated");
 
         // Trigger a copy shortcut to transfer the content of the selection to the clipboard

From 6e03e7e8e41e88857e777282c0fa3f3f230e7e74 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Mon, 20 Jan 2020 00:03:23 +0100
Subject: [PATCH 07/25] Add passive only matches

---
 src/matcher/mod.rs       | 8 +++++++-
 src/matcher/scrolling.rs | 5 +++++
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs
index a8f0ffa..1c8916b 100644
--- a/src/matcher/mod.rs
+++ b/src/matcher/mod.rs
@@ -32,6 +32,7 @@ pub struct Match {
     pub trigger: String,
     pub content: MatchContentType,
     pub word: bool,
+    pub passive_only: bool,
 
     // Automatically calculated from the trigger, used by the matcher to check for correspondences.
     #[serde(skip_serializing)]
@@ -133,7 +134,8 @@ impl<'a> From<&'a AutoMatch> for Match{
         Self {
             trigger: other.trigger.clone(),
             content,
-            word: other.word.clone(),
+            word: other.word,
+            passive_only: other.passive_only,
             _trigger_sequence: trigger_sequence,
         }
     }
@@ -155,10 +157,14 @@ struct AutoMatch {
 
     #[serde(default = "default_word")]
     pub word: bool,
+
+    #[serde(default = "default_passive_only")]
+    pub passive_only: bool,
 }
 
 fn default_vars() -> Vec<MatchVariable> {Vec::new()}
 fn default_word() -> bool {false}
+fn default_passive_only() -> bool {false}
 fn default_replace() -> Option<String> {None}
 fn default_image_path() -> Option<String> {None}
 
diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs
index 9c70fcd..ce1f951 100644
--- a/src/matcher/scrolling.rs
+++ b/src/matcher/scrolling.rs
@@ -114,6 +114,11 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
 
         let new_matches: Vec<MatchEntry> = active_config.matches.iter()
             .filter(|&x| {
+                // only active-enabled matches are considered
+                if x.passive_only {
+                    return false;
+                }
+
                 let mut result = Self::is_matching(x, c, 0, is_current_word_separator);
 
                 if x.word {

From 6135787eb0f85858174248f0de450ab3a7f21e20 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federico-terzi@users.noreply.github.com>
Date: Tue, 21 Jan 2020 00:22:22 +0100
Subject: [PATCH 08/25] Add passive mode Windows implementation

---
 native/libwinbridge/bridge.cpp | 26 ++++++++++++++++++++++++++
 native/libwinbridge/bridge.h   |  5 +++++
 src/bridge/windows.rs          |  1 +
 src/extension/script.rs        | 25 +++++++------------------
 src/extension/shell.rs         |  2 +-
 src/keyboard/windows.rs        |  6 ++++++
 6 files changed, 46 insertions(+), 19 deletions(-)

diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp
index f83e26b..0621a5a 100644
--- a/native/libwinbridge/bridge.cpp
+++ b/native/libwinbridge/bridge.cpp
@@ -524,6 +524,31 @@ void trigger_paste() {
     SendInput(vec.size(), vec.data(), sizeof(INPUT));
 }
 
+void trigger_copy() {
+    std::vector<INPUT> vec;
+
+    INPUT input = { 0 };
+
+    input.type = INPUT_KEYBOARD;
+    input.ki.wScan = 0;
+    input.ki.time = 0;
+    input.ki.dwExtraInfo = 0;
+    input.ki.wVk = VK_CONTROL;
+    input.ki.dwFlags = 0; // 0 for key press
+    vec.push_back(input);
+
+    input.ki.wVk = 0x43;  // C KEY
+    vec.push_back(input);
+
+    input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
+    vec.push_back(input);
+
+    input.ki.wVk = VK_CONTROL;
+    vec.push_back(input);
+
+    SendInput(vec.size(), vec.data(), sizeof(INPUT));
+}
+
 
 // SYSTEM
 
@@ -699,3 +724,4 @@ int32_t set_clipboard_image(wchar_t *path) {
 
     return result;
 }
+
diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h
index 77fa4d4..a5f1d89 100644
--- a/native/libwinbridge/bridge.h
+++ b/native/libwinbridge/bridge.h
@@ -79,6 +79,11 @@ extern "C" void delete_string(int32_t count);
  */
 extern "C" void trigger_paste();
 
+/*
+ * Send the copy keyboard shortcut (CTRL+C)
+ */
+extern "C" void trigger_copy();
+
 // Detect current application commands
 
 /*
diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs
index ff916fa..f7a4459 100644
--- a/src/bridge/windows.rs
+++ b/src/bridge/windows.rs
@@ -59,4 +59,5 @@ extern {
     pub fn send_multi_vkey(vk: i32, count: i32);
     pub fn delete_string(count: i32);
     pub fn trigger_paste();
+    pub fn trigger_copy();
 }
\ No newline at end of file
diff --git a/src/extension/script.rs b/src/extension/script.rs
index 554fdf5..16bd0ce 100644
--- a/src/extension/script.rs
+++ b/src/extension/script.rs
@@ -62,6 +62,7 @@ impl super::Extension for ScriptExtension {
                     .output()
             };
 
+            println!("{:?}", output);
             match output {
                 Ok(output) => {
                     let output_str = String::from_utf8_lossy(output.stdout.as_slice());
@@ -86,6 +87,7 @@ mod tests {
     use crate::extension::Extension;
 
     #[test]
+    #[cfg(not(target_os = "windows"))]
     fn test_script_basic() {
         let mut params = Mapping::new();
         params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"]));
@@ -94,15 +96,11 @@ mod tests {
         let output = extension.calculate(&params, &vec![]);
 
         assert!(output.is_some());
-
-        if cfg!(target_os = "windows") {
-            assert_eq!(output.unwrap(), "hello world\r\n");
-        }else{
-            assert_eq!(output.unwrap(), "hello world\n");
-        }
+        assert_eq!(output.unwrap(), "hello world\n");
     }
 
     #[test]
+    #[cfg(not(target_os = "windows"))]
     fn test_script_inject_args_off() {
         let mut params = Mapping::new();
         params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"]));
@@ -111,15 +109,11 @@ mod tests {
         let output = extension.calculate(&params, &vec!["jon".to_owned()]);
 
         assert!(output.is_some());
-
-        if cfg!(target_os = "windows") {
-            assert_eq!(output.unwrap(), "hello world\r\n");
-        }else{
-            assert_eq!(output.unwrap(), "hello world\n");
-        }
+        assert_eq!(output.unwrap(), "hello world\n");
     }
 
     #[test]
+    #[cfg(not(target_os = "windows"))]
     fn test_script_inject_args_on() {
         let mut params = Mapping::new();
         params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"]));
@@ -129,11 +123,6 @@ mod tests {
         let output = extension.calculate(&params, &vec!["jon".to_owned()]);
 
         assert!(output.is_some());
-
-        if cfg!(target_os = "windows") {
-            assert_eq!(output.unwrap(), "hello world jon\r\n");
-        }else{
-            assert_eq!(output.unwrap(), "hello world jon\n");
-        }
+        assert_eq!(output.unwrap(), "hello world jon\n");
     }
 }
\ No newline at end of file
diff --git a/src/extension/shell.rs b/src/extension/shell.rs
index f8e014a..8ad9798 100644
--- a/src/extension/shell.rs
+++ b/src/extension/shell.rs
@@ -24,7 +24,7 @@ use regex::{Regex, Captures};
 
 lazy_static! {
     static ref POS_ARG_REGEX: Regex = if cfg!(target_os = "windows") {
-        Regex::new("\\%(?P<pos>\\d+)").unwrap()
+        Regex::new("%(?P<pos>\\d+)").unwrap()
     }else{
         Regex::new("\\$(?P<pos>\\d+)").unwrap()
     };
diff --git a/src/keyboard/windows.rs b/src/keyboard/windows.rs
index c4e110b..c7ee10a 100644
--- a/src/keyboard/windows.rs
+++ b/src/keyboard/windows.rs
@@ -73,4 +73,10 @@ impl super::KeyboardManager for WindowsKeyboardManager {
             send_multi_vkey(0x25, count)
         }
     }
+
+    fn trigger_copy(&self) {
+        unsafe {
+            trigger_copy();
+        }
+    }
 }
\ No newline at end of file

From 86586623cca163129e6ec9fbd9bca797e299e90f Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Wed, 22 Jan 2020 21:51:36 +0100
Subject: [PATCH 09/25] Add trigger_copy implementation on MacOS

---
 native/libmacbridge/bridge.h  |  5 +++++
 native/libmacbridge/bridge.mm | 36 ++++++++++++++++++++++++++++++++++-
 src/bridge/macos.rs           |  1 +
 src/keyboard/macos.rs         |  6 ++++++
 4 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h
index 33848c9..3fc9f9d 100644
--- a/native/libmacbridge/bridge.h
+++ b/native/libmacbridge/bridge.h
@@ -80,6 +80,11 @@ void delete_string(int32_t count);
  */
 void trigger_paste();
 
+/*
+ * Trigger normal copy ( Pressing CMD+C )
+ */
+void trigger_copy();
+
 // UI
 
 /*
diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm
index 8b31e07..0280789 100644
--- a/native/libmacbridge/bridge.mm
+++ b/native/libmacbridge/bridge.mm
@@ -180,6 +180,39 @@ void trigger_paste() {
     });
 }
 
+
+void trigger_copy() {
+    dispatch_async(dispatch_get_main_queue(), ^(void) {
+        CGEventRef keydown;
+        keydown = CGEventCreateKeyboardEvent(NULL, 0x37, true);  // CMD
+        CGEventPost(kCGHIDEventTap, keydown);
+        CFRelease(keydown);
+
+        usleep(2000);
+
+        CGEventRef keydown2;
+        keydown2 = CGEventCreateKeyboardEvent(NULL, 0x08, true);  // C key
+        CGEventPost(kCGHIDEventTap, keydown2);
+        CFRelease(keydown2);
+
+        usleep(2000);
+
+        CGEventRef keyup;
+        keyup = CGEventCreateKeyboardEvent(NULL, 0x08, false);
+        CGEventPost(kCGHIDEventTap, keyup);
+        CFRelease(keyup);
+
+        usleep(2000);
+
+        CGEventRef keyup2;
+        keyup2 = CGEventCreateKeyboardEvent(NULL, 0x37, false);  // CMD
+        CGEventPost(kCGHIDEventTap, keyup2);
+        CFRelease(keyup2);
+
+        usleep(2000);
+    });
+}
+
 int32_t get_active_app_bundle(char * buffer, int32_t size) {
     NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
     NSString *bundlePath = [frontApp bundleURL].path;
@@ -291,4 +324,5 @@ int32_t prompt_accessibility() {
 void open_settings_panel() {
     NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
     [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]];
-}
\ No newline at end of file
+}
+
diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs
index f99a6f8..c698d35 100644
--- a/src/bridge/macos.rs
+++ b/src/bridge/macos.rs
@@ -59,4 +59,5 @@ extern {
     pub fn send_multi_vkey(vk: i32, count: i32);
     pub fn delete_string(count: i32);
     pub fn trigger_paste();
+    pub fn trigger_copy();
 }
\ No newline at end of file
diff --git a/src/keyboard/macos.rs b/src/keyboard/macos.rs
index 6cba0da..ccc4faf 100644
--- a/src/keyboard/macos.rs
+++ b/src/keyboard/macos.rs
@@ -56,6 +56,12 @@ impl super::KeyboardManager for MacKeyboardManager {
         }
     }
 
+    fn trigger_copy(&self) {
+        unsafe {
+            trigger_copy();
+        }
+    }
+
     fn delete_string(&self, count: i32) {
         unsafe {delete_string(count)}
     }

From d8392b4e48e106711d92ca94098ff527e4879702 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sat, 25 Jan 2020 14:58:07 +0100
Subject: [PATCH 10/25] Fix bug that prevented certain applications, such as
 Photoshop, from working correctly with espanso on macOS. Fix #159

---
 native/libmacbridge/bridge.mm | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm
index 0280789..564405b 100644
--- a/native/libmacbridge/bridge.mm
+++ b/native/libmacbridge/bridge.mm
@@ -101,6 +101,14 @@ void send_string(const char * string) {
 
             usleep(2000);
 
+            // Some applications require an explicit release of the space key
+            // For more information: https://github.com/federico-terzi/espanso/issues/159
+            CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x31, false);
+            CGEventPost(kCGHIDEventTap, e2);
+            CFRelease(e2);
+
+            usleep(2000);
+
             i += chunk_size;
         }
     });

From 47340e4a405af1fdcb16d7f37e31e8fc7da912f0 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 26 Jan 2020 00:43:25 +0100
Subject: [PATCH 11/25] Add basic deb packaging metadata

---
 Cargo.toml | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/Cargo.toml b/Cargo.toml
index fccfc68..4998e9a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,4 +38,9 @@ libc = "0.2.62"
 zip = "0.5.3"
 
 [build-dependencies]
-cmake = "0.1.31"
\ No newline at end of file
+cmake = "0.1.31"
+
+[package.metadata.deb]
+maintainer = "Federico Terzi <federicoterzi96@gmail.com>"
+depends = "$auto, systemd, libxtst6, libxdo3, xclip, libnotify-bin"
+section = "utility"

From fa74737d5d03e2ae719003df3282f603478e44e6 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 26 Jan 2020 22:00:44 +0100
Subject: [PATCH 12/25] Add license file to deb package

---
 Cargo.toml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Cargo.toml b/Cargo.toml
index 4998e9a..5d0fccc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,3 +44,4 @@ cmake = "0.1.31"
 maintainer = "Federico Terzi <federicoterzi96@gmail.com>"
 depends = "$auto, systemd, libxtst6, libxdo3, xclip, libnotify-bin"
 section = "utility"
+license-file = ["LICENSE", "1"]
\ No newline at end of file

From 15ad35046aaec5610bfb82c2ec04ce0b38aee94b Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 26 Jan 2020 22:13:07 +0100
Subject: [PATCH 13/25] Add DEB packaging to CI

---
 ci/build-linux.yml | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/ci/build-linux.yml b/ci/build-linux.yml
index a8e2dd8..c0fc49f 100644
--- a/ci/build-linux.yml
+++ b/ci/build-linux.yml
@@ -7,4 +7,12 @@ steps:
       cp target/release/espanso-*.gz .
       sha256sum espanso-*.gz | awk '{ print $1 }' > espanso-linux-sha256.txt
       ls -la
-    displayName: "Cargo build and packaging for Linux"
\ No newline at end of file
+    displayName: "Cargo build and packaging for Linux"
+
+  - script: |
+      cargo install cargo-deb
+      cargo deb
+      cp target/release/debian/espanso*amd64.deb espanso-debian-amd64.deb
+      sha256sum espanso-*amd64.deb | awk '{ print $1 }' > espanso-debian-amd64-sha256.txt
+      ls -la
+    displayName: "Packaging deb package"
\ No newline at end of file

From 1ad5bae62f29264052da807048c0ef077098fd8b Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 26 Jan 2020 22:16:39 +0100
Subject: [PATCH 14/25] Version bump 0.5.0

---
 Cargo.lock | 2 +-
 Cargo.toml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 84de321..ecf6760 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -370,7 +370,7 @@ dependencies = [
 
 [[package]]
 name = "espanso"
-version = "0.4.1"
+version = "0.5.0"
 dependencies = [
  "backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/Cargo.toml b/Cargo.toml
index fccfc68..b037d61 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "espanso"
-version = "0.4.1"
+version = "0.5.0"
 authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
 license = "GPL-3.0"
 description = "Cross-platform Text Expander written in Rust"

From 025d00ad2608d45609fb39a24d9410e28189a9da Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 26 Jan 2020 22:30:54 +0100
Subject: [PATCH 15/25] Add options to selectively disable active and passive
 mode

---
 src/config/mod.rs | 12 ++++++++----
 src/engine.rs     | 10 +++++++---
 2 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/src/config/mod.rs b/src/config/mod.rs
index 073e0f1..154dccf 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -46,7 +46,6 @@ fn default_parent() -> String{ "self".to_owned() }
 fn default_filter_title() -> String{ "".to_owned() }
 fn default_filter_class() -> String{ "".to_owned() }
 fn default_filter_exec() -> String{ "".to_owned() }
-fn default_disabled() -> bool{ false }
 fn default_log_level() -> i32 { 0 }
 fn default_conflict_check() -> bool{ true }
 fn default_ipc_server_port() -> i32 { 34982 }
@@ -60,6 +59,8 @@ fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?
 fn default_passive_arg_delimiter() -> char { '/' }
 fn default_passive_arg_escape() -> char { '\\' }
 fn default_passive_key() -> KeyModifier { KeyModifier::OFF }
+fn default_enable_passive() -> bool { false }
+fn default_enable_active() -> bool { true }
 fn default_action_noop_interval() -> u128 { 500 }
 fn default_backspace_limit() -> i32 { 3 }
 fn default_exclude_default_matches() -> bool {false}
@@ -82,9 +83,6 @@ pub struct Configs {
     #[serde(default = "default_filter_exec")]
     pub filter_exec: String,
 
-    #[serde(default = "default_disabled")]
-    pub disabled: bool,
-
     #[serde(default = "default_log_level")]
     pub log_level: i32,
 
@@ -124,6 +122,12 @@ pub struct Configs {
     #[serde(default = "default_passive_key")]
     pub passive_key: KeyModifier,
 
+    #[serde(default = "default_enable_passive")]
+    pub enable_passive: bool,
+
+    #[serde(default = "default_enable_active")]
+    pub enable_active: bool,
+
     #[serde(default = "default_action_noop_interval")]
     pub action_noop_interval: u128,
 
diff --git a/src/engine.rs b/src/engine.rs
index 4145d48..a971a4f 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -135,7 +135,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
     fn on_match(&self, m: &Match, trailing_separator: Option<char>) {
         let config = self.config_manager.active_config();
 
-        if config.disabled {
+        if !config.enable_active {
             return;
         }
 
@@ -269,6 +269,12 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
             return;
         }
 
+        let config = self.config_manager.active_config();
+
+        if !config.enable_passive {
+            return;
+        }
+
         info!("Passive mode activated");
 
         // Trigger a copy shortcut to transfer the content of the selection to the clipboard
@@ -281,8 +287,6 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
         let clipboard = self.clipboard_manager.get_clipboard();
 
         if let Some(clipboard) = clipboard {
-            let config = self.config_manager.active_config();
-
             let rendered = self.renderer.render_passive(&clipboard,
                                                         &config);
 

From a6282b1a9d961f55506abb9dacd2bfeb09307ec8 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 26 Jan 2020 22:40:32 +0100
Subject: [PATCH 16/25] Add delay to mitigate clipboard restoration race
 condition. Fix #148

---
 src/config/mod.rs | 5 +++++
 src/engine.rs     | 4 ++++
 2 files changed, 9 insertions(+)

diff --git a/src/config/mod.rs b/src/config/mod.rs
index 154dccf..8eb2c2e 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -63,6 +63,7 @@ fn default_enable_passive() -> bool { false }
 fn default_enable_active() -> bool { true }
 fn default_action_noop_interval() -> u128 { 500 }
 fn default_backspace_limit() -> i32 { 3 }
+fn default_restore_clipboard_delay() -> i32 { 300 }
 fn default_exclude_default_matches() -> bool {false}
 fn default_matches() -> Vec<Match> { Vec::new() }
 
@@ -137,6 +138,9 @@ pub struct Configs {
     #[serde(default = "default_backspace_limit")]
     pub backspace_limit: i32,
 
+    #[serde(default = "default_restore_clipboard_delay")]
+    pub restore_clipboard_delay: i32,
+
     #[serde(default)]
     pub backend: BackendType,
 
@@ -185,6 +189,7 @@ impl Configs {
         validate_field!(result, self.passive_arg_escape, default_passive_arg_escape());
         validate_field!(result, self.passive_key, default_passive_key());
         validate_field!(result, self.action_noop_interval, default_action_noop_interval());
+        validate_field!(result, self.restore_clipboard_delay, default_restore_clipboard_delay());
 
         result
     }
diff --git a/src/engine.rs b/src/engine.rs
index a971a4f..ce3faca 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -239,6 +239,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
 
         // Restore previous clipboard content
         if let Some(previous_clipboard_content) = previous_clipboard_content {
+            // Sometimes an expansion gets overwritten before pasting by the previous content
+            // A delay is needed to mitigate the problem
+            std::thread::sleep(std::time::Duration::from_millis(config.restore_clipboard_delay as u64));
+
             self.clipboard_manager.set_clipboard(&previous_clipboard_content);
         }
     }

From 0dfcb9cef725613ac4ffec6df05f9981df02c57b Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 26 Jan 2020 23:56:50 +0100
Subject: [PATCH 17/25] First draft of global variables. Fix #162

---
 src/config/mod.rs      | 143 ++++++++++++++++++++++++++++++++++++-----
 src/extension/dummy.rs |  44 +++++++++++++
 src/extension/mod.rs   |   2 +
 src/render/default.rs  |  79 ++++++++++++++++++++++-
 4 files changed, 249 insertions(+), 19 deletions(-)
 create mode 100644 src/extension/dummy.rs

diff --git a/src/config/mod.rs b/src/config/mod.rs
index 8eb2c2e..9dec411 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -21,7 +21,7 @@ extern crate dirs;
 
 use std::path::{Path, PathBuf};
 use std::{fs};
-use crate::matcher::Match;
+use crate::matcher::{Match, MatchVariable};
 use std::fs::{File, create_dir_all};
 use std::io::Read;
 use serde::{Serialize, Deserialize};
@@ -64,12 +64,13 @@ fn default_enable_active() -> bool { true }
 fn default_action_noop_interval() -> u128 { 500 }
 fn default_backspace_limit() -> i32 { 3 }
 fn default_restore_clipboard_delay() -> i32 { 300 }
-fn default_exclude_default_matches() -> bool {false}
+fn default_exclude_default_entries() -> bool {false}
 fn default_matches() -> Vec<Match> { Vec::new() }
+fn default_global_vars() -> Vec<MatchVariable> { Vec::new() }
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct Configs {
-    #[serde(default = "default_name")]
+#[serde(default = "default_name")]
     pub name: String,
 
     #[serde(default = "default_parent")]
@@ -144,11 +145,15 @@ pub struct Configs {
     #[serde(default)]
     pub backend: BackendType,
 
-    #[serde(default = "default_exclude_default_matches")]
-    pub exclude_default_matches: bool,
+    #[serde(default = "default_exclude_default_entries")]
+    pub exclude_default_entries: bool,
 
     #[serde(default = "default_matches")]
-    pub matches: Vec<Match>
+    pub matches: Vec<Match>,
+
+    #[serde(default = "default_global_vars")]
+    pub global_vars: Vec<MatchVariable>
+
 }
 
 // Macro used to validate config fields
@@ -244,29 +249,56 @@ impl Configs {
     }
 
     fn merge_config(&mut self, new_config: Configs) {
+        // Merge matches
         let mut merged_matches = new_config.matches;
-        let mut trigger_set = HashSet::new();
+        let mut match_trigger_set = HashSet::new();
         merged_matches.iter().for_each(|m| {
-            trigger_set.insert(m.trigger.clone());
+            match_trigger_set.insert(m.trigger.clone());
         });
         let parent_matches : Vec<Match> = self.matches.iter().filter(|&m| {
-            !trigger_set.contains(&m.trigger)
+            !match_trigger_set.contains(&m.trigger)
         }).cloned().collect();
 
         merged_matches.extend(parent_matches);
         self.matches = merged_matches;
+
+        // Merge global variables
+        let mut merged_global_vars = new_config.global_vars;
+        let mut vars_name_set = HashSet::new();
+        merged_global_vars.iter().for_each(|m| {
+            vars_name_set.insert(m.name.clone());
+        });
+        let parent_vars : Vec<MatchVariable> = self.global_vars.iter().filter(|&m| {
+            !vars_name_set.contains(&m.name)
+        }).cloned().collect();
+
+        merged_global_vars.extend(parent_vars);
+        self.global_vars = merged_global_vars;
     }
 
     fn merge_default(&mut self, default: &Configs) {
-        let mut trigger_set = HashSet::new();
+        // Merge matches
+        let mut match_trigger_set = HashSet::new();
         self.matches.iter().for_each(|m| {
-            trigger_set.insert(m.trigger.clone());
+            match_trigger_set.insert(m.trigger.clone());
         });
         let default_matches : Vec<Match> = default.matches.iter().filter(|&m| {
-            !trigger_set.contains(&m.trigger)
+            !match_trigger_set.contains(&m.trigger)
         }).cloned().collect();
 
         self.matches.extend(default_matches);
+
+        // Merge global variables
+        let mut vars_name_set = HashSet::new();
+        self.global_vars.iter().for_each(|m| {
+            vars_name_set.insert(m.name.clone());
+        });
+        let default_vars : Vec<MatchVariable> = default.global_vars.iter().filter(|&m| {
+            !vars_name_set.contains(&m.name)
+        }).cloned().collect();
+
+        self.global_vars.extend(default_vars);
+
     }
 }
 
@@ -357,9 +389,9 @@ impl ConfigSet {
         let default= configs.get(0).unwrap().clone();
         let mut specific = (&configs[1..]).to_vec().clone();
 
-        // Add default matches to specific configs when needed
+        // Add default entries to specific configs when needed
         for config in specific.iter_mut() {
-            if !config.exclude_default_matches {
+            if !config.exclude_default_entries {
                 config.merge_default(&default);
             }
         }
@@ -849,7 +881,7 @@ mod tests {
         let user_defined_path2 = create_user_config_file(data_dir.path(), "specific2.yml", r###"
         name: specific1
 
-        exclude_default_matches: true
+        exclude_default_entries: true
 
         matches:
             - trigger: "hello"
@@ -884,7 +916,7 @@ mod tests {
         let user_defined_path2 = create_user_config_file(data_dir.path(), "specific.zzz", r###"
         name: specific1
 
-        exclude_default_matches: true
+        exclude_default_entries: true
 
         matches:
             - trigger: "hello"
@@ -1201,4 +1233,83 @@ mod tests {
         let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
         assert_eq!(ConfigSet::has_conflicts(&config_set.default, &config_set.specific), false);
     }
+
+    #[test]
+    fn test_config_set_specific_inherits_default_global_vars() {
+        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###"
+        global_vars:
+            - name: testvar
+              type: date
+              params:
+                format: "%m"
+        "###);
+
+        let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###"
+         global_vars:
+            - name: specificvar
+              type: date
+              params:
+                format: "%m"
+        "###);
+
+        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+        assert_eq!(config_set.specific.len(), 1);
+        assert_eq!(config_set.default.global_vars.len(), 1);
+        assert_eq!(config_set.specific[0].global_vars.len(), 2);
+        assert!(config_set.specific[0].global_vars.iter().any(|m| m.name == "testvar"));
+        assert!(config_set.specific[0].global_vars.iter().any(|m| m.name == "specificvar"));
+    }
+
+    #[test]
+    fn test_config_set_default_get_variables_from_specific() {
+        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###"
+        global_vars:
+            - name: testvar
+              type: date
+              params:
+                format: "%m"
+        "###);
+
+        let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###"
+         parent: default
+         global_vars:
+            - name: specificvar
+              type: date
+              params:
+                format: "%m"
+        "###);
+
+        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+        assert_eq!(config_set.specific.len(), 0);
+        assert_eq!(config_set.default.global_vars.len(), 2);
+        assert!(config_set.default.global_vars.iter().any(|m| m.name == "testvar"));
+        assert!(config_set.default.global_vars.iter().any(|m| m.name == "specificvar"));
+    }
+
+    #[test]
+    fn test_config_set_specific_dont_inherits_default_global_vars_when_exclude_is_on() {
+        let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###"
+        global_vars:
+            - name: testvar
+              type: date
+              params:
+                format: "%m"
+        "###);
+
+        let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###"
+         exclude_default_entries: true
+
+         global_vars:
+            - name: specificvar
+              type: date
+              params:
+                format: "%m"
+        "###);
+
+        let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
+        assert_eq!(config_set.specific.len(), 1);
+        assert_eq!(config_set.default.global_vars.len(), 1);
+        assert_eq!(config_set.specific[0].global_vars.len(), 1);
+        assert!(config_set.specific[0].global_vars.iter().any(|m| m.name == "specificvar"));
+    }
 }
\ No newline at end of file
diff --git a/src/extension/dummy.rs b/src/extension/dummy.rs
new file mode 100644
index 0000000..c9932e8
--- /dev/null
+++ b/src/extension/dummy.rs
@@ -0,0 +1,44 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2020 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};
+
+pub struct DummyExtension {}
+
+impl DummyExtension {
+    pub fn new() -> DummyExtension {
+        DummyExtension{}
+    }
+}
+
+impl super::Extension for DummyExtension {
+    fn name(&self) -> String {
+        String::from("dummy")
+    }
+
+    fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {
+        let echo = params.get(&Value::from("echo"));
+
+        if let Some(echo) = echo {
+            Some(echo.as_str().unwrap_or_default().to_owned())
+        }else{
+            None
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/extension/mod.rs b/src/extension/mod.rs
index ccb6934..047060d 100644
--- a/src/extension/mod.rs
+++ b/src/extension/mod.rs
@@ -23,6 +23,7 @@ mod date;
 mod shell;
 mod script;
 mod random;
+mod dummy;
 
 pub trait Extension {
     fn name(&self) -> String;
@@ -35,5 +36,6 @@ pub fn get_extensions() -> Vec<Box<dyn Extension>> {
         Box::new(shell::ShellExtension::new()),
         Box::new(script::ScriptExtension::new()),
         Box::new(random::RandomExtension::new()),
+        Box::new(dummy::DummyExtension::new()),
     ]
 }
\ No newline at end of file
diff --git a/src/render/default.rs b/src/render/default.rs
index 2663c0b..feafac6 100644
--- a/src/render/default.rs
+++ b/src/render/default.rs
@@ -79,10 +79,11 @@ impl super::Renderer for DefaultRenderer {
         match &m.content {
             // Text Match
             MatchContentType::Text(content) => {
-                let target_string = if content._has_vars {
+                let target_string = if content._has_vars || !config.global_vars.is_empty(){
                     let mut output_map = HashMap::new();
 
-                    for variable in content.vars.iter() {
+                    // Cycle through both the local and global variables
+                    for variable in config.global_vars.iter().chain(&content.vars) {
                         // In case of variables of type match, we need to recursively call
                         // the render function
                         if variable.var_type == "match" {
@@ -118,7 +119,6 @@ impl super::Renderer for DefaultRenderer {
                                 },
                             }
                         }else{  // Normal extension variables
-                            // TODO: pass the arguments to the extension
                             let extension = self.extension_map.get(&variable.var_type);
                             if let Some(extension) = extension {
                                 let ext_out = extension.calculate(&variable.params, &args);
@@ -408,4 +408,77 @@ mod tests {
 
         verify_render(rendered, "Hi JonSnow");
     }
+
+    #[test]
+    fn test_render_passive_local_var() {
+        let text = "this is :test";
+
+        let config = get_config_for(r###"
+        matches:
+            - trigger: ':test'
+              replace: "my {{output}}"
+              vars:
+                - name: output
+                  type: dummy
+                  params:
+                    echo: "result"
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "this is my result");
+    }
+
+    #[test]
+    fn test_render_passive_global_var() {
+        let text = "this is :test";
+
+        let config = get_config_for(r###"
+        global_vars:
+            - name: output
+              type: dummy
+              params:
+                echo: "result"
+        matches:
+            - trigger: ':test'
+              replace: "my {{output}}"
+
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "this is my result");
+    }
+
+    #[test]
+    fn test_render_passive_global_var_is_overridden_by_local() {
+        let text = "this is :test";
+
+        let config = get_config_for(r###"
+        global_vars:
+            - name: output
+              type: dummy
+              params:
+                echo: "result"
+        matches:
+            - trigger: ':test'
+              replace: "my {{output}}"
+              vars:
+                - name: "output"
+                  type: dummy
+                  params:
+                    echo: "local"
+
+        "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "this is my local");
+    }
 }
\ No newline at end of file

From 084209155f63b04e5fec56aa324508fec45d4e13 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Sun, 2 Feb 2020 17:10:55 +0100
Subject: [PATCH 18/25] Fix broken windows CI pipeline

---
 ci/install-rust.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ci/install-rust.yml b/ci/install-rust.yml
index ca9e2c4..163eda8 100644
--- a/ci/install-rust.yml
+++ b/ci/install-rust.yml
@@ -15,7 +15,7 @@ steps:
   # Windows.
   - script: |
       curl -sSf -o rustup-init.exe https://win.rustup.rs
-      rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN%
+      rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% --default-host x86_64-pc-windows-msvc
       set PATH=%PATH%;%USERPROFILE%\.cargo\bin
       echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin"
     env:

From b76213b574bf2cf4be66cc53d642fd4b66a01e89 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federico-terzi@users.noreply.github.com>
Date: Wed, 26 Feb 2020 20:54:46 +0100
Subject: [PATCH 19/25] Version bump 0.5.1

---
 Cargo.lock | 2 +-
 Cargo.toml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index ecf6760..357886f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -370,7 +370,7 @@ dependencies = [
 
 [[package]]
 name = "espanso"
-version = "0.5.0"
+version = "0.5.1"
 dependencies = [
  "backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/Cargo.toml b/Cargo.toml
index 6c34967..eae189e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "espanso"
-version = "0.5.0"
+version = "0.5.1"
 authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
 license = "GPL-3.0"
 description = "Cross-platform Text Expander written in Rust"

From f429f58027a93a5697ae5d3d0d83085e55dec6e4 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federico-terzi@users.noreply.github.com>
Date: Wed, 26 Feb 2020 20:55:09 +0100
Subject: [PATCH 20/25] Fix #186

---
 src/render/default.rs | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/src/render/default.rs b/src/render/default.rs
index feafac6..804bb4e 100644
--- a/src/render/default.rs
+++ b/src/render/default.rs
@@ -29,6 +29,7 @@ use crate::extension::Extension;
 
 lazy_static! {
     static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
+    static ref UNKNOWN_VARIABLE : String = "".to_string();
 }
 
 pub struct DefaultRenderer {
@@ -138,7 +139,7 @@ impl super::Renderer for DefaultRenderer {
                     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()
+                        output.unwrap_or(&UNKNOWN_VARIABLE)
                     });
 
                     result.to_string()
@@ -481,4 +482,21 @@ mod tests {
 
         verify_render(rendered, "this is my local");
     }
+
+    #[test]
+    fn test_render_match_with_unknown_variable_does_not_crash() {
+        let text = "this is :test";
+
+        let config = get_config_for(r###"
+            matches:
+                - trigger: ':test'
+                  replace: "my {{unknown}}"
+            "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "this is my ");
+    }
 }
\ No newline at end of file

From ec68fd767ab01c9534afafbb3953a7692c8404ba Mon Sep 17 00:00:00 2001
From: Federico Terzi <federico-terzi@users.noreply.github.com>
Date: Wed, 26 Feb 2020 21:03:52 +0100
Subject: [PATCH 21/25] Add the possibility to escape double brackets in
 replacements. Fix #187

---
 src/render/default.rs | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/src/render/default.rs b/src/render/default.rs
index 804bb4e..55fa5cc 100644
--- a/src/render/default.rs
+++ b/src/render/default.rs
@@ -147,6 +147,11 @@ impl super::Renderer for DefaultRenderer {
                     content.replace.clone()
                 };
 
+                // Unescape any brackets (needed to be able to insert double brackets in replacement
+                // text, without triggering the variable system). See issue #187
+                let target_string = target_string.replace("\\{", "{")
+                                                        .replace("\\}", "}");
+
                 // Render any argument that may be present
                 let target_string = utils::render_args(&target_string, &args);
 
@@ -499,4 +504,21 @@ mod tests {
 
         verify_render(rendered, "this is my ");
     }
+
+    #[test]
+    fn test_render_escaped_double_brackets_should_not_consider_them_variable() {
+        let text = "this is :test";
+
+        let config = get_config_for(r###"
+            matches:
+                - trigger: ':test'
+                  replace: "my \\{\\{unknown\\}\\}"
+            "###);
+
+        let renderer = get_renderer(config.clone());
+
+        let rendered = renderer.render_passive(text, &config);
+
+        verify_render(rendered, "this is my {{unknown}}");
+    }
 }
\ No newline at end of file

From 0ee8ffbcef6efd3b879ebc523a1ab741c4528ebf Mon Sep 17 00:00:00 2001
From: Federico Terzi <federico-terzi@users.noreply.github.com>
Date: Wed, 26 Feb 2020 21:04:24 +0100
Subject: [PATCH 22/25] Make preserve_clipboard option enabled by default

---
 src/config/mod.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/config/mod.rs b/src/config/mod.rs
index 9dec411..8bddc07 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -54,7 +54,7 @@ fn default_config_caching_interval() -> i32 { 800 }
 fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8 as char] }
 fn default_toggle_interval() -> u32 { 230 }
 fn default_toggle_key() -> KeyModifier { KeyModifier::ALT }
-fn default_preserve_clipboard() -> bool {false}
+fn default_preserve_clipboard() -> bool {true}
 fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned() }
 fn default_passive_arg_delimiter() -> char { '/' }
 fn default_passive_arg_escape() -> char { '\\' }

From a89438f3bac140d5ebee9a3b1cb3afeb6b820275 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federico-terzi@users.noreply.github.com>
Date: Wed, 26 Feb 2020 21:24:02 +0100
Subject: [PATCH 23/25] First draft of edit-command on windows

---
 src/edit.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main.rs | 11 ++++++++++
 2 files changed, 72 insertions(+)
 create mode 100644 src/edit.rs

diff --git a/src/edit.rs b/src/edit.rs
new file mode 100644
index 0000000..fcc86a7
--- /dev/null
+++ b/src/edit.rs
@@ -0,0 +1,61 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2020 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 crate::config::ConfigSet;
+
+#[cfg(target_os = "linux")]
+pub fn open_editor(config: &ConfigSet) -> bool {
+    // TODO
+}
+
+#[cfg(target_os = "macos")]
+pub fn open_editor(config: &ConfigSet) -> bool {
+    // TODO
+}
+
+#[cfg(target_os = "windows")]
+pub fn open_editor(config: &ConfigSet) -> bool {
+    use std::process::Command;
+
+    // Get the configuration file path
+    let file_path = crate::context::get_config_dir().join(crate::config::DEFAULT_CONFIG_FILE_NAME);
+
+    // Start the editor and wait for its termination
+    let status = Command::new("cmd")
+        .arg("/C")
+        .arg("start")
+        .arg("/wait")
+        .arg("C:\\Windows\\System32\\notepad.exe")
+        .arg(file_path)
+        .spawn();
+
+    if let Ok(mut child) = status {
+        // Wait for the user to edit the configuration
+        child.wait();
+
+        // TODO: instead of waiting, a file watcher should be started to detect file changes and
+        // after each of them a reload should be issued
+
+        println!("Ok");
+        true
+    }else{
+        println!("Error: could not start editor.");
+        false
+    }
+}
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 12c7561..ac30214 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -46,6 +46,7 @@ use crate::package::default::DefaultPackageManager;
 use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult};
 
 mod ui;
+mod edit;
 mod event;
 mod check;
 mod utils;
@@ -96,6 +97,8 @@ fn main() {
             .subcommand(SubCommand::with_name("toggle")
                 .about("Toggle the status of the espanso replacement engine."))
         )
+        .subcommand(SubCommand::with_name("edit")
+            .about("Open the default text editor to edit config files and reload them automatically when exiting"))
         .subcommand(SubCommand::with_name("dump")
             .about("Prints all current configuration options."))
         .subcommand(SubCommand::with_name("detect")
@@ -163,6 +166,11 @@ fn main() {
         return;
     }
 
+    if matches.subcommand_matches("edit").is_some() {
+        edit_main(config_set);
+        return;
+    }
+
     if matches.subcommand_matches("dump").is_some() {
         println!("{:#?}", config_set);
         return;
@@ -873,6 +881,9 @@ fn path_main(_config_set: ConfigSet, matches: &ArgMatches) {
     }
 }
 
+fn edit_main(config_set: ConfigSet) {
+    crate::edit::open_editor(&config_set);
+}
 
 fn acquire_lock() -> Option<File> {
     let espanso_dir = context::get_data_dir();

From 3e98748c54395856d2bf977f506c3e546e1c558d Mon Sep 17 00:00:00 2001
From: Federico Terzi <federico-terzi@users.noreply.github.com>
Date: Thu, 27 Feb 2020 21:56:20 +0100
Subject: [PATCH 24/25] Add edit subcommand. Fix #171

---
 src/config/mod.rs | 14 ++++++++--
 src/edit.rs       | 48 +++++++++++++++-----------------
 src/main.rs       | 70 +++++++++++++++++++++++++++++++++++++++++++----
 3 files changed, 99 insertions(+), 33 deletions(-)

diff --git a/src/config/mod.rs b/src/config/mod.rs
index 8bddc07..b1f8ea0 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -38,7 +38,7 @@ pub(crate) mod runtime;
 const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yml");
 
 pub const DEFAULT_CONFIG_FILE_NAME : &str = "default.yml";
-const USER_CONFIGS_FOLDER_NAME: &str = "user";
+pub const USER_CONFIGS_FOLDER_NAME: &str = "user";
 
 // Default values for primitives
 fn default_name() -> String{ "default".to_owned() }
@@ -68,9 +68,16 @@ fn default_exclude_default_entries() -> bool {false}
 fn default_matches() -> Vec<Match> { Vec::new() }
 fn default_global_vars() -> Vec<MatchVariable> { Vec::new() }
 
+#[cfg(target_os = "linux")]
+fn default_editor() -> String{ "/bin/nano".to_owned() }
+#[cfg(target_os = "macos")]
+fn default_editor() -> String{ "/usr/bin/nano".to_owned() } // TODO: change
+#[cfg(target_os = "windows")]
+fn default_editor() -> String{ "C:\\Windows\\System32\\notepad.exe".to_owned() }
+
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct Configs {
-#[serde(default = "default_name")]
+    #[serde(default = "default_name")]
     pub name: String,
 
     #[serde(default = "default_parent")]
@@ -148,6 +155,9 @@ pub struct Configs {
     #[serde(default = "default_exclude_default_entries")]
     pub exclude_default_entries: bool,
 
+    #[serde(default = "default_editor")]
+    pub editor: String,
+
     #[serde(default = "default_matches")]
     pub matches: Vec<Match>,
 
diff --git a/src/edit.rs b/src/edit.rs
index fcc86a7..499ceda 100644
--- a/src/edit.rs
+++ b/src/edit.rs
@@ -18,44 +18,40 @@
  */
 
 use crate::config::ConfigSet;
+use std::path::Path;
 
-#[cfg(target_os = "linux")]
-pub fn open_editor(config: &ConfigSet) -> bool {
-    // TODO
-}
-
-#[cfg(target_os = "macos")]
-pub fn open_editor(config: &ConfigSet) -> bool {
-    // TODO
-}
-
-#[cfg(target_os = "windows")]
-pub fn open_editor(config: &ConfigSet) -> bool {
+pub fn open_editor(config: &ConfigSet, file_path: &Path) -> bool {
     use std::process::Command;
 
-    // Get the configuration file path
-    let file_path = crate::context::get_config_dir().join(crate::config::DEFAULT_CONFIG_FILE_NAME);
+    // Check if another editor is defined in the environment variables
+    let editor_var = std::env::var_os("EDITOR");
+    let visual_var = std::env::var_os("VISUAL");
+
+    // Prioritize the editors specified by the environment variable, otherwise use the config
+    let editor : String = if let Some(editor_var) = editor_var {
+        editor_var.to_string_lossy().to_string()
+    }else if let Some(visual_var) = visual_var {
+        visual_var.to_string_lossy().to_string()
+    }else{
+        config.default.editor.clone()
+    };
 
     // Start the editor and wait for its termination
-    let status = Command::new("cmd")
-        .arg("/C")
-        .arg("start")
-        .arg("/wait")
-        .arg("C:\\Windows\\System32\\notepad.exe")
+    let status = Command::new(editor)
         .arg(file_path)
         .spawn();
 
     if let Ok(mut child) = status {
         // Wait for the user to edit the configuration
-        child.wait();
+        let result = child.wait();
 
-        // TODO: instead of waiting, a file watcher should be started to detect file changes and
-        // after each of them a reload should be issued
-
-        println!("Ok");
-        true
+        if let Ok(exit_status) = result {
+            exit_status.success()
+        }else{
+            false
+        }
     }else{
-        println!("Error: could not start editor.");
+        println!("Error: could not start editor at: {}", config.default.editor);
         false
     }
 }
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index ac30214..7422b6f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -98,7 +98,9 @@ fn main() {
                 .about("Toggle the status of the espanso replacement engine."))
         )
         .subcommand(SubCommand::with_name("edit")
-            .about("Open the default text editor to edit config files and reload them automatically when exiting"))
+            .about("Open the default text editor to edit config files and reload them automatically when exiting")
+            .arg(Arg::with_name("config")
+                .help("Defaults to \"default\". The configuration file name to edit (without the .yml extension).")))
         .subcommand(SubCommand::with_name("dump")
             .about("Prints all current configuration options."))
         .subcommand(SubCommand::with_name("detect")
@@ -166,8 +168,8 @@ fn main() {
         return;
     }
 
-    if matches.subcommand_matches("edit").is_some() {
-        edit_main(config_set);
+    if let Some(matches) = matches.subcommand_matches("edit") {
+        edit_main(config_set, matches);
         return;
     }
 
@@ -881,8 +883,66 @@ fn path_main(_config_set: ConfigSet, matches: &ArgMatches) {
     }
 }
 
-fn edit_main(config_set: ConfigSet) {
-    crate::edit::open_editor(&config_set);
+fn edit_main(config_set: ConfigSet, matches: &ArgMatches) {
+    // Determine which is the file to edit
+    let config = matches.value_of("config").unwrap_or("default");
+
+    let config_dir = crate::context::get_config_dir();
+
+    let config_path = match config {
+        "default" => {
+            config_dir.join(crate::config::DEFAULT_CONFIG_FILE_NAME)
+        },
+        name => { // Otherwise, search in the user/ config folder
+            config_dir.join(crate::config::USER_CONFIGS_FOLDER_NAME)
+                .join(name.to_owned() + ".yml")
+        }
+    };
+
+    println!("Editing file: {:?}", &config_path);
+
+    // Based on the fact that the file already exists or not, we should detect in different
+    // ways if a reload is needed
+    let should_reload =if config_path.exists() {
+        // Get the last modified date, so that we can detect if the user actually edits the file
+        // before reloading
+        let metadata = std::fs::metadata(&config_path).expect("cannot gather file metadata");
+        let last_modified = metadata.modified().expect("cannot read file last modified date");
+
+        let result = crate::edit::open_editor(&config_set, &config_path);
+        if result {
+            let new_metadata = std::fs::metadata(&config_path).expect("cannot gather file metadata");
+            let new_last_modified = new_metadata.modified().expect("cannot read file last modified date");
+
+            if last_modified != new_last_modified {
+                println!("File has been modified, reloading configuration");
+                true
+            }else{
+                println!("File has not been modified, avoiding reload");
+                false
+            }
+        }else{
+            false
+        }
+    }else{
+        let result = crate::edit::open_editor(&config_set, &config_path);
+        if result {
+            // If the file has been created, we should reload the espanso config
+            if config_path.exists() {
+                println!("A new file has been created, reloading configuration");
+                true
+            }else{
+                println!("No file has been created, avoiding reload");
+                false
+            }
+        }else{
+            false
+        }
+    };
+
+    if should_reload {
+        restart_main(config_set)
+    }
 }
 
 fn acquire_lock() -> Option<File> {

From 7921e0fcdcb7188914d326d8b56e58dfd7c49377 Mon Sep 17 00:00:00 2001
From: Federico Terzi <federicoterzi96@gmail.com>
Date: Fri, 28 Feb 2020 21:38:15 +0100
Subject: [PATCH 25/25] Change urxvt terminal paste shortcut to CTRL+ALT+V. Fix
 #166

---
 native/liblinuxbridge/bridge.cpp | 8 ++++++--
 native/liblinuxbridge/bridge.h   | 5 +++++
 src/bridge/linux.rs              | 1 +
 src/keyboard/linux.rs            | 5 +++++
 src/keyboard/mod.rs              | 1 +
 5 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp
index 4fcdd58..f353eac 100644
--- a/native/liblinuxbridge/bridge.cpp
+++ b/native/liblinuxbridge/bridge.cpp
@@ -307,6 +307,10 @@ void trigger_alt_shift_ins_paste() {
     xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Alt+Insert", 8000);
 }
 
+void trigger_ctrl_alt_paste() {
+    xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Alt+v", 8000);
+}
+
 void trigger_copy() {
     // Release the other keys, for an explanation, read the 'trigger_paste' method
 
@@ -467,8 +471,8 @@ int32_t is_current_window_special() {
     if (res > 0) {
         if (strstr(class_buffer, "terminal") != NULL) {
             return 1;
-        }else if (strstr(class_buffer, "URxvt") != NULL) {  // Manjaro terminal
-            return 1;
+        }else if (strstr(class_buffer, "URxvt") != NULL) {  // urxvt terminal
+            return 4;
         }else if (strstr(class_buffer, "XTerm") != NULL) {  // XTerm and UXTerm
             return 1;
         }else if (strstr(class_buffer, "Termite") != NULL) {  // Termite
diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h
index 2389cf8..f4b857a 100644
--- a/native/liblinuxbridge/bridge.h
+++ b/native/liblinuxbridge/bridge.h
@@ -92,6 +92,11 @@ extern "C" void trigger_shift_ins_paste();
  */
 extern "C" void trigger_alt_shift_ins_paste();
 
+/*
+ * Trigger CTRL+ALT+V pasting
+ */
+extern "C" void trigger_ctrl_alt_paste();
+
 /*
  * Trigger copy shortcut ( Pressing CTRL+C )
  */
diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs
index 967aa9d..08f65cc 100644
--- a/src/bridge/linux.rs
+++ b/src/bridge/linux.rs
@@ -44,5 +44,6 @@ extern {
     pub fn trigger_terminal_paste();
     pub fn trigger_shift_ins_paste();
     pub fn trigger_alt_shift_ins_paste();
+    pub fn trigger_ctrl_alt_paste();
     pub fn trigger_copy();
 }
\ No newline at end of file
diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs
index 78e0961..04c3c3a 100644
--- a/src/keyboard/linux.rs
+++ b/src/keyboard/linux.rs
@@ -52,6 +52,8 @@ impl super::KeyboardManager for LinuxKeyboardManager {
                         trigger_alt_shift_ins_paste();
                     }else if is_special == 3 {  // Special case for Emacs
                         trigger_shift_ins_paste();
+                    }else if is_special == 4 {  // CTRL+ALT+V used in some terminals (urxvt)
+                        trigger_ctrl_alt_paste();
                     }else{
                         trigger_terminal_paste();
                     }
@@ -65,6 +67,9 @@ impl super::KeyboardManager for LinuxKeyboardManager {
                 PasteShortcut::ShiftInsert=> {
                     trigger_shift_ins_paste();
                 },
+                PasteShortcut::CtrlAltV => {
+                    trigger_ctrl_alt_paste();
+                },
                 _ => {
                     error!("Linux backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
                 }
diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs
index 66ebe77..045be16 100644
--- a/src/keyboard/mod.rs
+++ b/src/keyboard/mod.rs
@@ -43,6 +43,7 @@ pub enum PasteShortcut {
     CtrlV,          // Classic Ctrl+V shortcut
     CtrlShiftV,     // Could be used to paste without formatting in many applications
     ShiftInsert,    // Often used in Linux systems
+    CtrlAltV,       // Used in some Linux terminals (urxvt)
     MetaV,          // Corresponding to Win+V on Windows and Linux, CMD+V on macOS
 }