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. 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( 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 d769a0a..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 { @@ -143,13 +166,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 +193,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 +211,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 +230,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 +247,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 +275,7 @@ mod tests { assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello\n"); + assert_eq!(output.unwrap(), "hello"); } #[test] @@ -270,6 +289,6 @@ mod tests { assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello\r\n"); + assert_eq!(output.unwrap(), "hello"); } } diff --git a/src/main.rs b/src/main.rs index c727040..78e3267 100644 --- a/src/main.rs +++ b/src/main.rs @@ -381,10 +381,31 @@ 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..7a03222 100644 --- a/src/process.rs +++ b/src/process.rs @@ -18,29 +18,19 @@ */ use log::warn; -use widestring::WideCString; +use std::io; +use std::process::{Child, Command, Stdio}; #[cfg(target_os = "windows")] -pub fn spawn_process(cmd: &str, args: &Vec) { - 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"))] -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() }