From aa366cb9166be70d0d5470094a492c57117c1bbc Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 26 May 2020 18:53:56 +0200 Subject: [PATCH 1/6] Version bump 0.6.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- snapcraft.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c33e2f..0943f5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,7 +366,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.6.0" +version = "0.6.1" dependencies = [ "backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index ee80ccb..f13de11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.6.0" +version = "0.6.1" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" diff --git a/snapcraft.yaml b/snapcraft.yaml index 1c1c225..967e543 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: espanso -version: 0.6.0 +version: 0.6.1 summary: A Cross-platform Text Expander written in Rust description: | espanso is a Cross-platform, Text Expander written in Rust. From 921c39ba4e169180b6c0615af99f4784d24b6bce Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 26 May 2020 19:15:58 +0200 Subject: [PATCH 2/6] Add worker monitor. Fix #284 --- src/main.rs | 21 +++++++++++++++++++-- src/process.rs | 9 +++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index c727040..d472795 100644 --- a/src/main.rs +++ b/src/main.rs @@ -381,10 +381,27 @@ fn daemon_main(config_set: ConfigSet) { info!("spawning worker process..."); let espanso_path = std::env::current_exe().expect("unable to obtain espanso path location"); - crate::process::spawn_process( + let mut child = crate::process::spawn_process( &espanso_path.to_string_lossy().to_string(), &vec!["worker".to_owned()], - ); + ).expect("unable to create worker process"); + + // Create a monitor thread that will exit with the same non-zero code if + // the worker thread exits + thread::Builder::new() + .name("worker monitor".to_string()) + .spawn(move || { + let result = child.wait(); + if let Ok(status) = result { + if let Some(code) = status.code() { + if code != 0 { + error!("worker process exited with non-zero code: {}, exiting", code); + std::process::exit(code); + } + } + } + }) + .expect("Unable to spawn worker monitor thread"); std::thread::sleep(Duration::from_millis(200)); diff --git a/src/process.rs b/src/process.rs index fdf7930..6737b07 100644 --- a/src/process.rs +++ b/src/process.rs @@ -18,10 +18,13 @@ */ use log::warn; +use std::process::{Command, Stdio, Child}; use widestring::WideCString; +use std::io; #[cfg(target_os = "windows")] pub fn spawn_process(cmd: &str, args: &Vec) { + // TODO: modify with https://doc.rust-lang.org/std/os/windows/process/trait.CommandExt.html let quoted_args: Vec = args.iter().map(|arg| format!("\"{}\"", arg)).collect(); let quoted_args = quoted_args.join(" "); let final_cmd = format!("\"{}\" {}", cmd, quoted_args); @@ -39,8 +42,6 @@ pub fn spawn_process(cmd: &str, args: &Vec) { } #[cfg(not(target_os = "windows"))] -pub fn spawn_process(cmd: &str, args: &Vec) { - use std::process::{Command, Stdio}; - - Command::new(cmd).args(args).spawn(); +pub fn spawn_process(cmd: &str, args: &Vec) -> io::Result { + Command::new(cmd).args(args).spawn() } From 7677615bae5b22a2deff675b623910fbd1c28b0e Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 26 May 2020 20:17:47 +0200 Subject: [PATCH 3/6] Adapt new process API to windows. --- src/main.rs | 8 ++++++-- src/process.rs | 25 +++++++------------------ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/main.rs b/src/main.rs index d472795..78e3267 100644 --- a/src/main.rs +++ b/src/main.rs @@ -384,7 +384,8 @@ fn daemon_main(config_set: ConfigSet) { let mut child = crate::process::spawn_process( &espanso_path.to_string_lossy().to_string(), &vec!["worker".to_owned()], - ).expect("unable to create worker process"); + ) + .expect("unable to create worker process"); // Create a monitor thread that will exit with the same non-zero code if // the worker thread exits @@ -395,7 +396,10 @@ fn daemon_main(config_set: ConfigSet) { if let Ok(status) = result { if let Some(code) = status.code() { if code != 0 { - error!("worker process exited with non-zero code: {}, exiting", code); + error!( + "worker process exited with non-zero code: {}, exiting", + code + ); std::process::exit(code); } } diff --git a/src/process.rs b/src/process.rs index 6737b07..7a03222 100644 --- a/src/process.rs +++ b/src/process.rs @@ -18,27 +18,16 @@ */ use log::warn; -use std::process::{Command, Stdio, Child}; -use widestring::WideCString; use std::io; +use std::process::{Child, Command, Stdio}; #[cfg(target_os = "windows")] -pub fn spawn_process(cmd: &str, args: &Vec) { - // TODO: modify with https://doc.rust-lang.org/std/os/windows/process/trait.CommandExt.html - let quoted_args: Vec = args.iter().map(|arg| format!("\"{}\"", arg)).collect(); - let quoted_args = quoted_args.join(" "); - let final_cmd = format!("\"{}\" {}", cmd, quoted_args); - unsafe { - let cmd_wstr = WideCString::from_str(&final_cmd); - if let Ok(string) = cmd_wstr { - let res = crate::bridge::windows::start_process(string.as_ptr()); - if res < 0 { - warn!("unable to start process: {}", final_cmd); - } - } else { - warn!("unable to convert process string into wide format") - } - } +pub fn spawn_process(cmd: &str, args: &Vec) -> io::Result { + use std::os::windows::process::CommandExt; + Command::new(cmd) + .creation_flags(0x00000008) // Detached Process + .args(args) + .spawn() } #[cfg(not(target_os = "windows"))] From 8e563b6327917eb60920b31f7f6a3519d3700cd3 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 26 May 2020 21:18:27 +0200 Subject: [PATCH 4/6] Make user defined matches higher priority than packages. Fix #273 --- src/config/mod.rs | 203 +++++++++++++++++++++++++++++++--------------- 1 file changed, 136 insertions(+), 67 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index a2e6d7e..351b639 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -31,7 +31,7 @@ use std::fs; use std::fs::{create_dir_all, File}; use std::io::Read; use std::path::{Path, PathBuf}; -use walkdir::WalkDir; +use walkdir::{DirEntry, WalkDir}; pub(crate) mod runtime; @@ -409,7 +409,7 @@ impl Configs { } } - fn merge_config(&mut self, new_config: Configs) { + fn merge_overwrite(&mut self, new_config: Configs) { // Merge matches let mut merged_matches = new_config.matches; let mut match_trigger_set = HashSet::new(); @@ -447,7 +447,7 @@ impl Configs { self.global_vars = merged_global_vars; } - fn merge_default(&mut self, default: &Configs) { + fn merge_no_overwrite(&mut self, default: &Configs) { // Merge matches let mut match_trigger_set = HashSet::new(); self.matches.iter().for_each(|m| { @@ -503,7 +503,7 @@ impl ConfigSet { eprintln!("Warning: Using Auto backend is only supported on Linux, falling back to Inject backend."); } - // Analyze which config files has to be loaded + // Analyze which config files have to be loaded let mut target_files = Vec::new(); @@ -513,82 +513,108 @@ impl ConfigSet { target_files.extend(dir_entry); } - if package_dir.exists() { + let package_files = if package_dir.exists() { let dir_entry = WalkDir::new(package_dir); - target_files.extend(dir_entry); - } + dir_entry.into_iter().collect() + } else { + vec![] + }; // Load the user defined config files let mut name_set = HashSet::new(); let mut children_map: HashMap> = HashMap::new(); + let mut package_map: HashMap> = HashMap::new(); let mut root_configs = Vec::new(); root_configs.push(default); - for entry in target_files { - if let Ok(entry) = entry { - let path = entry.path(); + let mut file_loader = |entry: walkdir::Result, + dest_map: &mut HashMap>| + -> Result<(), ConfigLoadError> { + match entry { + Ok(entry) => { + let path = entry.path(); - // Skip non-yaml config files - if path - .extension() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - != "yml" - { - continue; + // Skip non-yaml config files + if path + .extension() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + != "yml" + { + return Ok(()); + } + + // Skip hidden files + if path + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .starts_with(".") + { + return Ok(()); + } + + let mut config = Configs::load_config(&path)?; + + // Make sure the config does not contain reserved fields + if !config.validate_user_defined_config() { + return Err(ConfigLoadError::InvalidParameter(path.to_owned())); + } + + // No name specified, defaulting to the path name + if config.name == "default" { + config.name = path.to_str().unwrap_or_default().to_owned(); + } + + if name_set.contains(&config.name) { + return Err(ConfigLoadError::NameDuplicate(path.to_owned())); + } + + name_set.insert(config.name.clone()); + + if config.parent == "self" { + // No parent, root config + root_configs.push(config); + } else { + // Children config + let children_vec = dest_map.entry(config.parent.clone()).or_default(); + children_vec.push(config); + } } - - // Skip hidden files - if path - .file_name() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - .starts_with(".") - { - continue; + Err(e) => { + eprintln!("Warning: Unable to read config file: {}", e); } - - let mut config = Configs::load_config(&path)?; - - // Make sure the config does not contain reserved fields - if !config.validate_user_defined_config() { - return Err(ConfigLoadError::InvalidParameter(path.to_owned())); - } - - // No name specified, defaulting to the path name - if config.name == "default" { - config.name = path.to_str().unwrap_or_default().to_owned(); - } - - if name_set.contains(&config.name) { - return Err(ConfigLoadError::NameDuplicate(path.to_owned())); - } - - name_set.insert(config.name.clone()); - - if config.parent == "self" { - // No parent, root config - root_configs.push(config); - } else { - // Children config - let children_vec = children_map.entry(config.parent.clone()).or_default(); - children_vec.push(config); - } - } else { - eprintln!( - "Warning: Unable to read config file: {}", - entry.unwrap_err() - ) } + + Ok(()) + }; + + // Load the default and user specific configs + for entry in target_files { + file_loader(entry, &mut children_map)?; + } + + // Load the package related configs + for entry in package_files { + file_loader(entry, &mut package_map)?; } // Merge the children config files - let mut configs = Vec::new(); + let mut configs_without_packages = Vec::new(); for root_config in root_configs { - let config = ConfigSet::reduce_configs(root_config, &children_map); + let config = ConfigSet::reduce_configs(root_config, &children_map, true); + configs_without_packages.push(config); + } + + // Merge package files + // Note: we need two different steps as the packages have a lower priority + // than configs. + let mut configs = Vec::new(); + for root_config in configs_without_packages { + let config = ConfigSet::reduce_configs(root_config, &package_map, false); configs.push(config); } @@ -599,7 +625,7 @@ impl ConfigSet { // Add default entries to specific configs when needed for config in specific.iter_mut() { if !config.exclude_default_entries { - config.merge_default(&default); + config.merge_no_overwrite(&default); } } @@ -618,12 +644,21 @@ impl ConfigSet { Ok(ConfigSet { default, specific }) } - fn reduce_configs(target: Configs, children_map: &HashMap>) -> Configs { + fn reduce_configs( + target: Configs, + children_map: &HashMap>, + higher_priority: bool, + ) -> Configs { if children_map.contains_key(&target.name) { let mut target = target; for children in children_map.get(&target.name).unwrap() { - let children = Self::reduce_configs(children.clone(), children_map); - target.merge_config(children); + let children = + Self::reduce_configs(children.clone(), children_map, higher_priority); + if higher_priority { + target.merge_overwrite(children); + } else { + target.merge_no_overwrite(&children); + } } target } else { @@ -1480,6 +1515,40 @@ mod tests { .any(|m| m.triggers[0] == "harry")); } + #[test] + fn test_config_set_package_configs_lower_priority_than_user() { + let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( + r###" + matches: + - trigger: hasta + replace: Hasta la vista + "###, + ); + + create_package_file( + package_dir.path(), + "package1", + "package.yml", + r###" + parent: default + + matches: + - trigger: "hasta" + replace: "potter" + "###, + ); + + let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); + assert_eq!(config_set.specific.len(), 0); + assert_eq!(config_set.default.matches.len(), 1); + if let MatchContentType::Text(content) = config_set.default.matches[0].content.clone() { + assert_eq!(config_set.default.matches[0].triggers[0], "hasta"); + assert_eq!(content.replace, "Hasta la vista") + } else { + panic!("invalid content"); + } + } + #[test] fn test_config_set_package_configs_without_merge() { let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content( From a37c588e26caa587fba06ea90fde3ff5e1abc522 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 29 May 2020 21:54:13 +0200 Subject: [PATCH 5/6] Set trim option default to true in shell extension. Fix #272 --- src/extension/shell.rs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/extension/shell.rs b/src/extension/shell.rs index d769a0a..b06c1ff 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -143,13 +143,15 @@ impl super::Extension for ShellExtension { // If specified, trim the output let trim_opt = params.get(&Value::from("trim")); - if let Some(value) = trim_opt { + let should_trim = if let Some(value) = trim_opt { let val = value.as_bool(); - if let Some(val) = val { - if val { - output_str = output_str.trim().to_owned() - } - } + val.unwrap_or(true) + }else{ + true + }; + + if should_trim { + output_str = output_str.trim().to_owned() } Some(output_str) @@ -168,9 +170,10 @@ mod tests { use crate::extension::Extension; #[test] - fn test_shell_basic() { + fn test_shell_not_trimmed() { let mut params = Mapping::new(); params.insert(Value::from("cmd"), Value::from("echo \"hello world\"")); + params.insert(Value::from("trim"), Value::from(false)); let extension = ShellExtension::new(); let output = extension.calculate(¶ms, &vec![]); @@ -185,10 +188,9 @@ mod tests { } #[test] - fn test_shell_trimmed() { + fn test_shell_basic() { let mut params = Mapping::new(); params.insert(Value::from("cmd"), Value::from("echo \"hello world\"")); - params.insert(Value::from("trim"), Value::from(true)); let extension = ShellExtension::new(); let output = extension.calculate(¶ms, &vec![]); @@ -205,8 +207,6 @@ mod tests { Value::from("echo \" hello world \""), ); - params.insert(Value::from("trim"), Value::from(true)); - let extension = ShellExtension::new(); let output = extension.calculate(¶ms, &vec![]); @@ -224,11 +224,7 @@ mod tests { let output = extension.calculate(¶ms, &vec![]); assert!(output.is_some()); - if cfg!(target_os = "windows") { - assert_eq!(output.unwrap(), "hello world\r\n"); - } else { - assert_eq!(output.unwrap(), "hello world\n"); - } + assert_eq!(output.unwrap(), "hello world"); } #[test] @@ -256,7 +252,7 @@ mod tests { assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello\n"); + assert_eq!(output.unwrap(), "hello"); } #[test] @@ -270,6 +266,6 @@ mod tests { assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello\r\n"); + assert_eq!(output.unwrap(), "hello"); } } From a57f8286527d3a5de2dc3874305feab3f8727d8c Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 29 May 2020 22:07:00 +0200 Subject: [PATCH 6/6] Inject CONFIG env variable when executing Shell and Script extensions. Fix #277 --- src/extension/script.rs | 9 +++++++-- src/extension/shell.rs | 41 ++++++++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/extension/script.rs b/src/extension/script.rs index 875abe6..e25a6b1 100644 --- a/src/extension/script.rs +++ b/src/extension/script.rs @@ -75,10 +75,15 @@ impl super::Extension for ScriptExtension { } }); + let mut command = Command::new(&str_args[0]); + + // Inject the $CONFIG variable + command.env("CONFIG", crate::context::get_config_dir()); + let output = if str_args.len() > 1 { - Command::new(&str_args[0]).args(&str_args[1..]).output() + command.args(&str_args[1..]).output() } else { - Command::new(&str_args[0]).output() + command.output() }; match output { diff --git a/src/extension/shell.rs b/src/extension/shell.rs index b06c1ff..7ffcffa 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -40,15 +40,38 @@ pub enum Shell { impl Shell { fn execute_cmd(&self, cmd: &str) -> std::io::Result { - match self { - Shell::Cmd => Command::new("cmd").args(&["/C", &cmd]).output(), - Shell::Powershell => Command::new("powershell") - .args(&["-Command", &cmd]) - .output(), - Shell::WSL => Command::new("wsl").args(&["bash", "-c", &cmd]).output(), - Shell::Bash => Command::new("bash").args(&["-c", &cmd]).output(), - Shell::Sh => Command::new("sh").args(&["-c", &cmd]).output(), - } + let mut command = match self { + Shell::Cmd => { + let mut command = Command::new("cmd"); + command.args(&["/C", &cmd]); + command + }, + Shell::Powershell => { + let mut command = Command::new("powershell"); + command.args(&["-Command", &cmd]); + command + }, + Shell::WSL => { + let mut command = Command::new("wsl"); + command.args(&["bash", "-c", &cmd]); + command + }, + Shell::Bash => { + let mut command = Command::new("bash"); + command.args(&["-c", &cmd]); + command + }, + Shell::Sh => { + let mut command = Command::new("sh"); + command.args(&["-c", &cmd]); + command + }, + }; + + // Inject the $CONFIG variable + command.env("CONFIG", crate::context::get_config_dir()); + + command.output() } fn from_string(shell: &str) -> Option {