Refactor variable system

This commit is contained in:
Federico Terzi 2020-08-03 22:03:39 +02:00
parent 2a49231fbf
commit a6b78e7142
16 changed files with 578 additions and 70 deletions

View File

@ -146,6 +146,9 @@ fn default_matches() -> Vec<Match> {
fn default_global_vars() -> Vec<MatchVariable> {
Vec::new()
}
fn default_modulo_path() -> Option<String> {
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<MatchVariable>,
#[serde(default = "default_modulo_path")]
pub modulo_path: Option<String>
}
// Macro used to validate config fields

View File

@ -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<dyn ClipboardManager>,
@ -35,7 +37,11 @@ impl super::Extension for ClipboardExtension {
String::from("clipboard")
}
fn calculate(&self, _: &Mapping, _: &Vec<String>) -> Option<String> {
self.clipboard_manager.get_clipboard()
fn calculate(&self, _: &Mapping, _: &Vec<String>, _: &HashMap<String, ExtensionResult>) -> Option<ExtensionResult> {
if let Some(clipboard) = self.clipboard_manager.get_clipboard() {
Some(ExtensionResult::Single(clipboard))
} else {
None
}
}
}

View File

@ -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<String>) -> Option<String> {
fn calculate(&self, params: &Mapping, _: &Vec<String>, _: &HashMap<String, ExtensionResult>) -> Option<ExtensionResult> {
let now: DateTime<Local> = 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))
}
}

View File

@ -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<String>) -> Option<String> {
fn calculate(&self, params: &Mapping, _: &Vec<String>, _: &HashMap<String, ExtensionResult>) -> Option<ExtensionResult> {
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
}

View File

@ -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<String, String>),
}
pub trait Extension {
fn name(&self) -> String;
fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String>;
fn calculate(&self, params: &Mapping, args: &Vec<String>, current_vars: &HashMap<String, ExtensionResult>) -> Option<ExtensionResult>;
}
pub fn get_extensions(clipboard_manager: Box<dyn ClipboardManager>) -> Vec<Box<dyn Extension>> {
@ -38,7 +48,9 @@ pub fn get_extensions(clipboard_manager: Box<dyn ClipboardManager>) -> Vec<Box<d
Box::new(shell::ShellExtension::new()),
Box::new(script::ScriptExtension::new()),
Box::new(random::RandomExtension::new()),
Box::new(dummy::DummyExtension::new()),
Box::new(multiecho::MultiEchoExtension::new()),
Box::new(dummy::DummyExtension::new("dummy")),
Box::new(dummy::DummyExtension::new("echo")),
Box::new(clipboard::ClipboardExtension::new(clipboard_manager)),
]
}

View File

@ -0,0 +1,48 @@
/*
* 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};
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<String>, _: &HashMap<String, ExtensionResult>) -> Option<ExtensionResult> {
let mut output: HashMap<String, String> = 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))
}
}

View File

@ -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<String>) -> Option<String> {
fn calculate(&self, params: &Mapping, args: &Vec<String>, _: &HashMap<String, ExtensionResult>) -> Option<ExtensionResult> {
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(&params, &vec![]);
let output = extension.calculate(&params, &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(&params, &vec!["test".to_owned()]);
let output = extension.calculate(&params, &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));
}
}

View File

@ -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<String>) -> Option<String> {
fn calculate(&self, params: &Mapping, user_args: &Vec<String>, vars: &HashMap<String, ExtensionResult>) -> Option<ExtensionResult> {
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(&params, &vec![]);
let output = extension.calculate(&params, &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(&params, &vec![]);
let output = extension.calculate(&params, &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(&params, &vec!["jon".to_owned()]);
let output = extension.calculate(&params, &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(&params, &vec!["jon".to_owned()]);
let output = extension.calculate(&params, &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<String, ExtensionResult> = 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(&params, &vec![]);
assert!(output.is_some());
assert_eq!(output.unwrap(), "hello Jon");
}
}

View File

@ -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<Output> {
fn execute_cmd(&self, cmd: &str, vars: &HashMap<String, String>) -> std::io::Result<Output> {
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<String>) -> Option<String> {
fn calculate(&self, params: &Mapping, args: &Vec<String>, vars: &HashMap<String, ExtensionResult>) -> Option<ExtensionResult> {
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(&params, &vec![]);
let output = extension.calculate(&params, &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(&params, &vec![]);
let output = extension.calculate(&params, &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(&params, &vec![]);
let output = extension.calculate(&params, &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(&params, &vec![]);
let output = extension.calculate(&params, &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(&params, &vec![]);
let output = extension.calculate(&params, &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(&params, &vec!["hello".to_owned()]);
let output = extension.calculate(&params, &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(&params, &vec!["hello".to_owned()]);
let output = extension.calculate(&params, &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<String, ExtensionResult> = HashMap::new();
vars.insert("var1".to_owned(), ExtensionResult::Single("hello".to_owned()));
let output = extension.calculate(&params, &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<String, ExtensionResult> = 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(&params, &vec![], &vars);
assert!(output.is_some());
assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned()));
}
}

46
src/extension/utils.rs Normal file
View File

@ -0,0 +1,46 @@
use std::collections::HashMap;
use crate::extension::ExtensionResult;
pub fn convert_to_env_variables(original_vars: &HashMap<String, ExtensionResult>) -> HashMap<String, String> {
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<String, ExtensionResult> = 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");
}
}

47
src/extension/vardummy.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>, vars: &HashMap<String, ExtensionResult>) -> Option<ExtensionResult> {
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
}
}
}

View File

@ -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() {

View File

@ -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<name>\\w+)\\s*\\}\\}").unwrap();
static ref VAR_REGEX: Regex = Regex::new(r"\{\{\s*((?P<name>\w+)(\.(?P<subname>(\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<String> = 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<String, &MatchVariable> = 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<String, ExtensionResult> = 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");
}
}

View File

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

0
src/ui/modulo/form.rs Normal file
View File

113
src/ui/modulo/mod.rs Normal file
View File

@ -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<String>,
}
impl ModuloManager {
pub fn new(config: &Configs) -> Self {
let mut modulo_path: Option<String> = 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<String> {
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<String> {
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
}
}