Refactor variable system
This commit is contained in:
parent
2a49231fbf
commit
a6b78e7142
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
]
|
||||
}
|
||||
|
|
48
src/extension/multiecho.rs
Normal file
48
src/extension/multiecho.rs
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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(¶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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¶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<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(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
assert_eq!(output.unwrap(), "hello Jon");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¶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<String, ExtensionResult> = 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<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(¶ms, &vec![], &vars);
|
||||
|
||||
assert!(output.is_some());
|
||||
assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned()));
|
||||
}
|
||||
}
|
||||
|
|
46
src/extension/utils.rs
Normal file
46
src/extension/utils.rs
Normal 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
47
src/extension/vardummy.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
0
src/ui/modulo/form.rs
Normal file
113
src/ui/modulo/mod.rs
Normal file
113
src/ui/modulo/mod.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user