diff --git a/src/config/mod.rs b/src/config/mod.rs index 3e68ea1..d335f7e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -146,6 +146,9 @@ fn default_matches() -> Vec { fn default_global_vars() -> Vec { Vec::new() } +fn default_modulo_path() -> Option { + None +} #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Configs { @@ -259,6 +262,9 @@ pub struct Configs { #[serde(default = "default_global_vars")] pub global_vars: Vec, + + #[serde(default = "default_modulo_path")] + pub modulo_path: Option } // Macro used to validate config fields diff --git a/src/extension/clipboard.rs b/src/extension/clipboard.rs index 60846ca..1ee8367 100644 --- a/src/extension/clipboard.rs +++ b/src/extension/clipboard.rs @@ -19,6 +19,8 @@ use crate::clipboard::ClipboardManager; use serde_yaml::Mapping; +use crate::extension::ExtensionResult; +use std::collections::HashMap; pub struct ClipboardExtension { clipboard_manager: Box, @@ -35,7 +37,11 @@ impl super::Extension for ClipboardExtension { String::from("clipboard") } - fn calculate(&self, _: &Mapping, _: &Vec) -> Option { - self.clipboard_manager.get_clipboard() + fn calculate(&self, _: &Mapping, _: &Vec, _: &HashMap) -> Option { + if let Some(clipboard) = self.clipboard_manager.get_clipboard() { + Some(ExtensionResult::Single(clipboard)) + } else { + None + } } } diff --git a/src/extension/date.rs b/src/extension/date.rs index ac15295..918a1e9 100644 --- a/src/extension/date.rs +++ b/src/extension/date.rs @@ -19,6 +19,8 @@ use chrono::{DateTime, Local}; use serde_yaml::{Mapping, Value}; +use std::collections::HashMap; +use crate::extension::ExtensionResult; pub struct DateExtension {} @@ -33,7 +35,7 @@ impl super::Extension for DateExtension { String::from("date") } - fn calculate(&self, params: &Mapping, _: &Vec) -> Option { + fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { let now: DateTime = Local::now(); let format = params.get(&Value::from("format")); @@ -44,6 +46,6 @@ impl super::Extension for DateExtension { now.to_rfc2822() }; - Some(date) + Some(ExtensionResult::Single(date)) } } diff --git a/src/extension/dummy.rs b/src/extension/dummy.rs index 6bbb1dd..934fbee 100644 --- a/src/extension/dummy.rs +++ b/src/extension/dummy.rs @@ -18,25 +18,31 @@ */ use serde_yaml::{Mapping, Value}; +use std::collections::HashMap; +use crate::extension::ExtensionResult; -pub struct DummyExtension {} +pub struct DummyExtension { + name: String, +} impl DummyExtension { - pub fn new() -> DummyExtension { - DummyExtension {} + pub fn new(name: &str) -> DummyExtension { + DummyExtension { + name: name.to_owned(), + } } } impl super::Extension for DummyExtension { fn name(&self) -> String { - String::from("dummy") + self.name.clone() } - fn calculate(&self, params: &Mapping, _: &Vec) -> Option { + fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { let echo = params.get(&Value::from("echo")); if let Some(echo) = echo { - Some(echo.as_str().unwrap_or_default().to_owned()) + Some(ExtensionResult::Single(echo.as_str().unwrap_or_default().to_owned())) } else { None } diff --git a/src/extension/mod.rs b/src/extension/mod.rs index 9a363c5..a25406b 100644 --- a/src/extension/mod.rs +++ b/src/extension/mod.rs @@ -19,6 +19,7 @@ use crate::clipboard::ClipboardManager; use serde_yaml::Mapping; +use std::collections::HashMap; mod clipboard; mod date; @@ -26,10 +27,19 @@ pub mod dummy; mod random; mod script; mod shell; +pub mod multiecho; +pub mod vardummy; +mod utils; + +#[derive(Clone, Debug, PartialEq)] +pub enum ExtensionResult { + Single(String), + Multiple(HashMap), +} pub trait Extension { fn name(&self) -> String; - fn calculate(&self, params: &Mapping, args: &Vec) -> Option; + fn calculate(&self, params: &Mapping, args: &Vec, current_vars: &HashMap) -> Option; } pub fn get_extensions(clipboard_manager: Box) -> Vec> { @@ -38,7 +48,9 @@ pub fn get_extensions(clipboard_manager: Box) -> Vec. + */ + +use serde_yaml::{Mapping, Value}; +use std::collections::HashMap; +use crate::extension::ExtensionResult; + +pub struct MultiEchoExtension {} + +impl MultiEchoExtension { + pub fn new() -> MultiEchoExtension { + MultiEchoExtension {} + } +} + +impl super::Extension for MultiEchoExtension { + fn name(&self) -> String { + "multiecho".to_owned() + } + + fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { + let mut output: HashMap = HashMap::new(); + for (key, value) in params.iter() { + if let Some(key) = key.as_str() { + if let Some(value) = value.as_str() { + output.insert(key.to_owned(), value.to_owned()); + } + } + } + Some(ExtensionResult::Multiple(output)) + } +} diff --git a/src/extension/random.rs b/src/extension/random.rs index 5d168c9..200c738 100644 --- a/src/extension/random.rs +++ b/src/extension/random.rs @@ -20,6 +20,8 @@ use log::{error, warn}; use rand::seq::SliceRandom; use serde_yaml::{Mapping, Value}; +use std::collections::HashMap; +use crate::extension::ExtensionResult; pub struct RandomExtension {} @@ -34,7 +36,7 @@ impl super::Extension for RandomExtension { String::from("random") } - fn calculate(&self, params: &Mapping, args: &Vec) -> Option { + fn calculate(&self, params: &Mapping, args: &Vec, _: &HashMap) -> Option { let choices = params.get(&Value::from("choices")); if choices.is_none() { warn!("No 'choices' parameter specified for random variable"); @@ -55,7 +57,7 @@ impl super::Extension for RandomExtension { // Render arguments let output = crate::render::utils::render_args(output, args); - return Some(output); + return Some(ExtensionResult::Single(output)); } None => { error!("Could not select a random choice."); @@ -81,13 +83,13 @@ mod tests { params.insert(Value::from("choices"), Value::from(choices.clone())); let extension = RandomExtension::new(); - let output = extension.calculate(¶ms, &vec![]); + let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); let output = output.unwrap(); - assert!(choices.iter().any(|x| x == &output)); + assert!(choices.into_iter().any(|x| ExtensionResult::Single(x.to_owned()) == output)); } #[test] @@ -97,7 +99,7 @@ mod tests { params.insert(Value::from("choices"), Value::from(choices.clone())); let extension = RandomExtension::new(); - let output = extension.calculate(¶ms, &vec!["test".to_owned()]); + let output = extension.calculate(¶ms, &vec!["test".to_owned()], &HashMap::new()); assert!(output.is_some()); @@ -105,6 +107,6 @@ mod tests { let rendered_choices = vec!["first test", "second test", "test third"]; - assert!(rendered_choices.iter().any(|x| x == &output)); + assert!(rendered_choices.into_iter().any(|x| ExtensionResult::Single(x.to_owned()) == output)); } } diff --git a/src/extension/script.rs b/src/extension/script.rs index 6144854..2b1791d 100644 --- a/src/extension/script.rs +++ b/src/extension/script.rs @@ -21,6 +21,8 @@ use log::{error, warn}; use serde_yaml::{Mapping, Value}; use std::path::PathBuf; use std::process::Command; +use std::collections::HashMap; +use crate::extension::ExtensionResult; pub struct ScriptExtension {} @@ -35,7 +37,7 @@ impl super::Extension for ScriptExtension { String::from("script") } - fn calculate(&self, params: &Mapping, user_args: &Vec) -> Option { + fn calculate(&self, params: &Mapping, user_args: &Vec, vars: &HashMap) -> Option { let args = params.get(&Value::from("args")); if args.is_none() { warn!("No 'args' parameter specified for script variable"); @@ -80,6 +82,12 @@ impl super::Extension for ScriptExtension { // Inject the $CONFIG variable command.env("CONFIG", crate::context::get_config_dir()); + // Inject all the env variables + let env_variables = super::utils::convert_to_env_variables(&vars); + for (key, value) in env_variables.iter() { + command.env(key, value); + } + let output = if str_args.len() > 1 { command.args(&str_args[1..]).output() } else { @@ -112,7 +120,7 @@ impl super::Extension for ScriptExtension { output_str = output_str.trim().to_owned() } - return Some(output_str); + return Some(ExtensionResult::Single(output_str)); } Err(e) => { error!("Could not execute script '{:?}', error: {}", args, e); @@ -141,10 +149,10 @@ mod tests { ); let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec![]); + let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello world"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); } #[test] @@ -158,10 +166,10 @@ mod tests { params.insert(Value::from("trim"), Value::from(false)); let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec![]); + let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello world\n"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world\n".to_owned())); } #[test] @@ -174,10 +182,10 @@ mod tests { ); let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec!["jon".to_owned()]); + let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello world"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); } #[test] @@ -191,9 +199,31 @@ mod tests { params.insert(Value::from("inject_args"), Value::from(true)); let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec!["jon".to_owned()]); + let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello world jon"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world jon".to_owned())); + } + + #[test] + #[cfg(not(target_os = "windows"))] + fn test_script_var_injection() { + let mut params = Mapping::new(); + params.insert( + Value::from("args"), + Value::from(vec!["echo", "$ESPANSO_VAR1 $ESPANSO_FORM1_NAME"]), + ); + + let mut vars: HashMap = HashMap::new(); + let mut subvars = HashMap::new(); + subvars.insert("name".to_owned(), "John".to_owned()); + vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars)); + vars.insert("var1".to_owned(), ExtensionResult::Single("hello".to_owned())); + + let extension = ScriptExtension::new(); + let output = extension.calculate(¶ms, &vec![]); + + assert!(output.is_some()); + assert_eq!(output.unwrap(), "hello Jon"); } } diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 1907513..2a77aff 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -21,6 +21,8 @@ use log::{error, info, warn}; use regex::{Captures, Regex}; use serde_yaml::{Mapping, Value}; use std::process::{Command, Output}; +use std::collections::HashMap; +use crate::extension::ExtensionResult; lazy_static! { static ref POS_ARG_REGEX: Regex = if cfg!(target_os = "windows") { @@ -40,7 +42,7 @@ pub enum Shell { } impl Shell { - fn execute_cmd(&self, cmd: &str) -> std::io::Result { + fn execute_cmd(&self, cmd: &str, vars: &HashMap) -> std::io::Result { let mut command = match self { Shell::Cmd => { let mut command = Command::new("cmd"); @@ -77,6 +79,11 @@ impl Shell { // Inject the $CONFIG variable command.env("CONFIG", crate::context::get_config_dir()); + // Inject all the previous variables + for (key, value) in vars.iter() { + command.env(key, value); + } + command.output() } @@ -120,7 +127,7 @@ impl super::Extension for ShellExtension { String::from("shell") } - fn calculate(&self, params: &Mapping, args: &Vec) -> Option { + fn calculate(&self, params: &Mapping, args: &Vec, vars: &HashMap) -> Option { let cmd = params.get(&Value::from("cmd")); if cmd.is_none() { warn!("No 'cmd' parameter specified for shell variable"); @@ -157,7 +164,9 @@ impl super::Extension for ShellExtension { Shell::default() }; - let output = shell.execute_cmd(&cmd); + let env_variables = super::utils::convert_to_env_variables(&vars); + + let output = shell.execute_cmd(&cmd, &env_variables); match output { Ok(output) => { @@ -201,7 +210,7 @@ impl super::Extension for ShellExtension { output_str = output_str.trim().to_owned() } - Some(output_str) + Some(ExtensionResult::Single(output_str)) } Err(e) => { error!("Could not execute cmd '{}', error: {}", cmd, e); @@ -223,14 +232,14 @@ mod tests { params.insert(Value::from("trim"), Value::from(false)); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms, &vec![]); + let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); if cfg!(target_os = "windows") { - assert_eq!(output.unwrap(), "hello world\r\n"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world\r\n".to_owned())); } else { - assert_eq!(output.unwrap(), "hello world\n"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world\n".to_owned())); } } @@ -240,10 +249,10 @@ mod tests { params.insert(Value::from("cmd"), Value::from("echo \"hello world\"")); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms, &vec![]); + let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello world"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); } #[test] @@ -255,10 +264,10 @@ mod tests { ); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms, &vec![]); + let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello world"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); } #[test] @@ -268,10 +277,10 @@ mod tests { params.insert(Value::from("trim"), Value::from("error")); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms, &vec![]); + let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello world"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); } #[test] @@ -282,10 +291,10 @@ mod tests { params.insert(Value::from("trim"), Value::from(true)); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms, &vec![]); + let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello world"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); } #[test] @@ -295,11 +304,11 @@ mod tests { params.insert(Value::from("cmd"), Value::from("echo $0")); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms, &vec!["hello".to_owned()]); + let output = extension.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned())); } #[test] @@ -309,10 +318,50 @@ mod tests { params.insert(Value::from("cmd"), Value::from("echo %0")); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms, &vec!["hello".to_owned()]); + let output = extension.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned())); + } + + #[test] + fn test_shell_vars_single_injection() { + let mut params = Mapping::new(); + if cfg!(target_os = "windows") { + params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_VAR1%")); + params.insert(Value::from("shell"), Value::from("cmd")); + }else{ + params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_VAR1")); + } + + let extension = ShellExtension::new(); + let mut vars: HashMap = HashMap::new(); + vars.insert("var1".to_owned(), ExtensionResult::Single("hello".to_owned())); + let output = extension.calculate(¶ms, &vec![], &vars); + + assert!(output.is_some()); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned())); + } + + #[test] + fn test_shell_vars_multiple_injection() { + let mut params = Mapping::new(); + if cfg!(target_os = "windows") { + params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_FORM1_NAME%")); + params.insert(Value::from("shell"), Value::from("cmd")); + }else{ + params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_FORM1_NAME")); + } + + let extension = ShellExtension::new(); + let mut vars: HashMap = HashMap::new(); + let mut subvars = HashMap::new(); + subvars.insert("name".to_owned(), "John".to_owned()); + vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars)); + let output = extension.calculate(¶ms, &vec![], &vars); + + assert!(output.is_some()); + assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned())); } } diff --git a/src/extension/utils.rs b/src/extension/utils.rs new file mode 100644 index 0000000..d963a79 --- /dev/null +++ b/src/extension/utils.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; +use crate::extension::ExtensionResult; + +pub fn convert_to_env_variables(original_vars: &HashMap) -> HashMap { + let mut output = HashMap::new(); + + for (key, result) in original_vars.iter() { + match result { + ExtensionResult::Single(value) => { + let name = format!("ESPANSO_{}", key.to_uppercase()); + output.insert(name, value.clone()); + }, + ExtensionResult::Multiple(values) => { + for (sub_key, sub_value) in values.iter() { + let name = format!("ESPANSO_{}_{}", key.to_uppercase(), sub_key.to_uppercase()); + output.insert(name, sub_value.clone()); + } + }, + } + } + + output +} + + + +#[cfg(test)] +mod tests { + use super::*; + use crate::extension::Extension; + + #[test] + fn test_convert_to_env_variables() { + let mut vars: HashMap = HashMap::new(); + let mut subvars = HashMap::new(); + subvars.insert("name".to_owned(), "John".to_owned()); + subvars.insert("lastname".to_owned(), "Snow".to_owned()); + vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars)); + vars.insert("var1".to_owned(), ExtensionResult::Single("test".to_owned())); + + let output = convert_to_env_variables(&vars); + assert_eq!(output.get("ESPANSO_FORM1_NAME").unwrap(), "John"); + assert_eq!(output.get("ESPANSO_FORM1_LASTNAME").unwrap(), "Snow"); + assert_eq!(output.get("ESPANSO_VAR1").unwrap(), "test"); + } +} \ No newline at end of file diff --git a/src/extension/vardummy.rs b/src/extension/vardummy.rs new file mode 100644 index 0000000..8274737 --- /dev/null +++ b/src/extension/vardummy.rs @@ -0,0 +1,47 @@ +/* + * 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 . + */ + +use serde_yaml::{Mapping, Value}; +use std::collections::HashMap; +use crate::extension::ExtensionResult; + +pub struct VarDummyExtension {} + +impl VarDummyExtension { + pub fn new() -> Self { + Self {} + } +} + +impl super::Extension for VarDummyExtension { + fn name(&self) -> String { + "vardummy".to_owned() + } + + fn calculate(&self, params: &Mapping, _: &Vec, vars: &HashMap) -> Option { + let target = params.get(&Value::from("target")); + + if let Some(target) = target { + let value = vars.get(target.as_str().unwrap_or_default()); + Some(value.unwrap().clone()) + } else { + None + } + } +} diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 6914f97..27e2336 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -74,7 +74,7 @@ impl<'de> serde::Deserialize<'de> for Match { impl<'a> From<&'a AutoMatch> for Match { fn from(other: &'a AutoMatch) -> Self { lazy_static! { - static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)\\s*\\}\\}").unwrap(); + static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap(); }; let mut triggers = if !other.triggers.is_empty() { diff --git a/src/render/default.rs b/src/render/default.rs index 6c9fcbc..af8ee13 100644 --- a/src/render/default.rs +++ b/src/render/default.rs @@ -19,15 +19,15 @@ use super::*; use crate::config::Configs; -use crate::extension::Extension; -use crate::matcher::{Match, MatchContentType}; +use crate::extension::{Extension, ExtensionResult}; +use crate::matcher::{Match, MatchContentType, MatchVariable}; use log::{error, warn}; use regex::{Captures, Regex}; use serde_yaml::Value; use std::collections::{HashMap, HashSet}; lazy_static! { - static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P\\w+)\\s*\\}\\}").unwrap(); + static ref VAR_REGEX: Regex = Regex::new(r"\{\{\s*((?P\w+)(\.(?P(\w+)))?)\s*\}\}").unwrap(); static ref UNKNOWN_VARIABLE: String = "".to_string(); } @@ -86,24 +86,56 @@ impl super::Renderer for DefaultRenderer { match &m.content { // Text Match MatchContentType::Text(content) => { - // Find all the variables that are required by the current match - let mut target_vars = HashSet::new(); + let target_string = if content._has_vars { + // Find all the variables that are required by the current match + let mut target_vars: HashSet = HashSet::new(); - for caps in VAR_REGEX.captures_iter(&content.replace) { - let var_name = caps.name("name").unwrap().as_str(); - target_vars.insert(var_name.to_owned()); - } + for caps in VAR_REGEX.captures_iter(&content.replace) { + let var_name = caps.name("name").unwrap().as_str(); + target_vars.insert(var_name.to_owned()); + } - let target_string = if target_vars.len() > 0 { - let mut output_map = HashMap::new(); + let match_variables: HashSet<&String> = content.vars.iter().map(|var| { + &var.name + }).collect(); - // Cycle through both the local and global variables - for variable in config.global_vars.iter().chain(&content.vars) { - // Skip all non-required variables - if !target_vars.contains(&variable.name) { - continue; + // Find the global variables that are not specified in the var list + let mut missing_globals = Vec::new(); + let mut specified_globals: HashMap = HashMap::new(); + for global_var in config.global_vars.iter() { + if target_vars.contains(&global_var.name) { + if match_variables.contains(&global_var.name) { + specified_globals.insert(global_var.name.clone(), &global_var); + }else { + missing_globals.push(global_var); + } } + } + // Determine the variable evaluation order + let mut variables: Vec<&MatchVariable> = Vec::new(); + // First place the global that are not explicitly specified + variables.extend(missing_globals); + // Then the ones explicitly specified, in the given order + variables.extend(&content.vars); + + println!("{:?}", variables); + + // Replace variable type "global" with the actual reference + let variables: Vec<&MatchVariable> = variables.into_iter().map(|variable| { + if variable.var_type == "global" { + if let Some(actual_variable) = specified_globals.get(&variable.name) { + return actual_variable.clone(); + } + } + variable + }).collect(); + + println!("{:?}", variables); + + let mut output_map: HashMap = HashMap::new(); + + for variable in variables.into_iter() { // In case of variables of type match, we need to recursively call // the render function if variable.var_type == "match" { @@ -140,7 +172,7 @@ impl super::Renderer for DefaultRenderer { // 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); + output_map.insert(variable.name.clone(), ExtensionResult::Single(inner_content)); }, _ => { warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.") @@ -150,11 +182,11 @@ impl super::Renderer for DefaultRenderer { // Normal extension variables let extension = self.extension_map.get(&variable.var_type); if let Some(extension) = extension { - let ext_out = extension.calculate(&variable.params, &args); + let ext_out = extension.calculate(&variable.params, &args, &output_map); if let Some(output) = ext_out { output_map.insert(variable.name.clone(), output); } else { - output_map.insert(variable.name.clone(), "".to_owned()); + output_map.insert(variable.name.clone(), ExtensionResult::Single("".to_owned())); warn!( "Could not generate output for variable: {}", variable.name @@ -172,8 +204,31 @@ impl super::Renderer for DefaultRenderer { // 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_or(&UNKNOWN_VARIABLE) + let var_subname = caps.name("subname"); + match output_map.get(var_name) { + Some(result) => { + match result { + ExtensionResult::Single(output) => { + output + }, + ExtensionResult::Multiple(results) => { + match var_subname { + Some(var_subname) => { + let var_subname = var_subname.as_str(); + results.get(var_subname).unwrap_or(&UNKNOWN_VARIABLE) + }, + None => { + error!("nested name missing from multi-value variable: {}", var_name); + &UNKNOWN_VARIABLE + }, + } + }, + } + }, + None => { + &UNKNOWN_VARIABLE + }, + } }); result.to_string() @@ -311,7 +366,11 @@ mod tests { fn get_renderer(config: Configs) -> DefaultRenderer { DefaultRenderer::new( - vec![Box::new(crate::extension::dummy::DummyExtension::new())], + vec![ + Box::new(crate::extension::dummy::DummyExtension::new("dummy")), + Box::new(crate::extension::vardummy::VarDummyExtension::new()), + Box::new(crate::extension::multiecho::MultiEchoExtension::new()), + ], config, ) } @@ -737,4 +796,84 @@ mod tests { verify_render(rendered, "RESULT"); } + + + + #[test] + fn test_render_variable_order() { + let config = get_config_for( + r###" + matches: + - trigger: 'test' + replace: "{{output}}" + vars: + - name: first + type: dummy + params: + echo: "hello" + - name: output + type: vardummy + params: + target: "first" + "###, + ); + + let renderer = get_renderer(config.clone()); + let m = config.matches[0].clone(); + let rendered = renderer.render_match(&m, 0, &config, vec![]); + verify_render(rendered, "hello"); + } + + #[test] + fn test_render_global_variable_order() { + let config = get_config_for( + r###" + global_vars: + - name: hello + type: dummy + params: + echo: "hello" + matches: + - trigger: 'test' + replace: "{{hello}} {{output}}" + vars: + - name: first + type: dummy + params: + echo: "world" + - name: output + type: vardummy + params: + target: "first" + - name: hello + type: global + "###, + ); + + let renderer = get_renderer(config.clone()); + let m = config.matches[0].clone(); + let rendered = renderer.render_match(&m, 0, &config, vec![]); + verify_render(rendered, "hello world"); + } + + #[test] + fn test_render_multiple_results() { + let config = get_config_for( + r###" + matches: + - trigger: 'test' + replace: "hello {{var1.name}}" + vars: + - name: var1 + type: multiecho + params: + name: "world" + "###, + ); + + let renderer = get_renderer(config.clone()); + let m = config.matches[0].clone(); + let rendered = renderer.render_match(&m, 0, &config, vec![]); + verify_render(rendered, "hello world"); + } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1400b19..f25491c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -26,6 +26,8 @@ mod linux; #[cfg(target_os = "macos")] mod macos; +pub mod modulo; + pub trait UIManager { fn notify(&self, message: &str); fn notify_delay(&self, message: &str, duration: i32); diff --git a/src/ui/modulo/form.rs b/src/ui/modulo/form.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/modulo/mod.rs b/src/ui/modulo/mod.rs new file mode 100644 index 0000000..93c7921 --- /dev/null +++ b/src/ui/modulo/mod.rs @@ -0,0 +1,113 @@ +use crate::config::Configs; +use std::process::{Command, Child, Output}; +use log::{error}; +use std::io::{Error, Write}; + +pub mod form; + +pub struct ModuloManager { + modulo_path: Option, +} + +impl ModuloManager { + pub fn new(config: &Configs) -> Self { + let mut modulo_path: Option = None; + if let Some(ref _modulo_path) = config.modulo_path { + modulo_path = Some(_modulo_path.to_owned()); + }else{ + // First check in the same directory of espanso + if let Ok(exe_path) = std::env::current_exe() { + if let Some(parent) = exe_path.parent() { + let possible_path = parent.join("modulo"); + let possible_path = possible_path.to_string_lossy().to_string(); + + if let Ok(output) = Command::new(&possible_path).arg("--version").output() { + if output.status.success() { + modulo_path = Some(possible_path); + } + } + } + } + + // Otherwise check if present in the PATH + if modulo_path.is_none() { + if let Ok(output) = Command::new("modulo").arg("--version").output() { + if output.status.success() { + modulo_path = Some("modulo".to_owned()); + } + } + } + } + + Self { + modulo_path, + } + } + + pub fn is_valid(&self) -> bool { + self.modulo_path.is_some() + } + + pub fn get_version(&self) -> Option { + if let Some(ref modulo_path) = self.modulo_path { + if let Ok(output) = Command::new(modulo_path).arg("--version").output() { + let version = String::from_utf8_lossy(&output.stdout); + return Some(version.to_string()); + } + } + + None + } + + fn invoke(&self, args: &[&str], body: &str) -> Option { + if self.modulo_path.is_none() { + error!("Attempt to invoke modulo even though it's not configured"); + return None; + } + + if let Some(ref modulo_path) = self.modulo_path { + let mut child = Command::new(modulo_path) + .args(args) + .stdin(std::process::Stdio::piped()) + .spawn(); + + match child { + Ok(mut child) => { + if let Some(stdin) = child.stdin.as_mut() { + match stdin.write_all(body.as_bytes()) { + Ok(_) => { + // Get the output + match child.wait_with_output() { + Ok(child_output) => { + let output = String::from_utf8_lossy(&child_output.stdout); + + // Check also if the program reports an error + let error = String::from_utf8_lossy(&child_output.stderr); + if !error.is_empty() { + error!("modulo reported an error: {}", error); + } + + return Some(output.to_string()); + }, + Err(error) => { + error!("error while getting output from modulo: {}", error); + }, + } + }, + Err(error) => { + error!("error while sending body to modulo"); + }, + } + }else{ + error!("unable to open stdin to modulo"); + } + }, + Err(error) => { + error!("error reported when invoking modulo: {}", error); + }, + } + } + + None + } +} \ No newline at end of file