commit
195edbe9d2
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -366,7 +366,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
description = "Cross-platform Text Expander written in Rust"
|
description = "Cross-platform Text Expander written in Rust"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: espanso
|
name: espanso
|
||||||
version: 0.6.0
|
version: 0.6.1
|
||||||
summary: A Cross-platform Text Expander written in Rust
|
summary: A Cross-platform Text Expander written in Rust
|
||||||
description: |
|
description: |
|
||||||
espanso is a Cross-platform, Text Expander written in Rust.
|
espanso is a Cross-platform, Text Expander written in Rust.
|
||||||
|
|
|
@ -31,7 +31,7 @@ use std::fs;
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{create_dir_all, File};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use walkdir::WalkDir;
|
use walkdir::{DirEntry, WalkDir};
|
||||||
|
|
||||||
pub(crate) mod runtime;
|
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
|
// Merge matches
|
||||||
let mut merged_matches = new_config.matches;
|
let mut merged_matches = new_config.matches;
|
||||||
let mut match_trigger_set = HashSet::new();
|
let mut match_trigger_set = HashSet::new();
|
||||||
|
@ -447,7 +447,7 @@ impl Configs {
|
||||||
self.global_vars = merged_global_vars;
|
self.global_vars = merged_global_vars;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_default(&mut self, default: &Configs) {
|
fn merge_no_overwrite(&mut self, default: &Configs) {
|
||||||
// Merge matches
|
// Merge matches
|
||||||
let mut match_trigger_set = HashSet::new();
|
let mut match_trigger_set = HashSet::new();
|
||||||
self.matches.iter().for_each(|m| {
|
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.");
|
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();
|
let mut target_files = Vec::new();
|
||||||
|
|
||||||
|
@ -513,20 +513,26 @@ impl ConfigSet {
|
||||||
target_files.extend(dir_entry);
|
target_files.extend(dir_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if package_dir.exists() {
|
let package_files = if package_dir.exists() {
|
||||||
let dir_entry = WalkDir::new(package_dir);
|
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
|
// Load the user defined config files
|
||||||
|
|
||||||
let mut name_set = HashSet::new();
|
let mut name_set = HashSet::new();
|
||||||
let mut children_map: HashMap<String, Vec<Configs>> = HashMap::new();
|
let mut children_map: HashMap<String, Vec<Configs>> = HashMap::new();
|
||||||
|
let mut package_map: HashMap<String, Vec<Configs>> = HashMap::new();
|
||||||
let mut root_configs = Vec::new();
|
let mut root_configs = Vec::new();
|
||||||
root_configs.push(default);
|
root_configs.push(default);
|
||||||
|
|
||||||
for entry in target_files {
|
let mut file_loader = |entry: walkdir::Result<DirEntry>,
|
||||||
if let Ok(entry) = entry {
|
dest_map: &mut HashMap<String, Vec<Configs>>|
|
||||||
|
-> Result<(), ConfigLoadError> {
|
||||||
|
match entry {
|
||||||
|
Ok(entry) => {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
// Skip non-yaml config files
|
// Skip non-yaml config files
|
||||||
|
@ -537,7 +543,7 @@ impl ConfigSet {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
!= "yml"
|
!= "yml"
|
||||||
{
|
{
|
||||||
continue;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip hidden files
|
// Skip hidden files
|
||||||
|
@ -548,7 +554,7 @@ impl ConfigSet {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.starts_with(".")
|
.starts_with(".")
|
||||||
{
|
{
|
||||||
continue;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut config = Configs::load_config(&path)?;
|
let mut config = Configs::load_config(&path)?;
|
||||||
|
@ -574,21 +580,41 @@ impl ConfigSet {
|
||||||
root_configs.push(config);
|
root_configs.push(config);
|
||||||
} else {
|
} else {
|
||||||
// Children config
|
// Children config
|
||||||
let children_vec = children_map.entry(config.parent.clone()).or_default();
|
let children_vec = dest_map.entry(config.parent.clone()).or_default();
|
||||||
children_vec.push(config);
|
children_vec.push(config);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
eprintln!(
|
Err(e) => {
|
||||||
"Warning: Unable to read config file: {}",
|
eprintln!("Warning: Unable to read config file: {}", e);
|
||||||
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
|
// Merge the children config files
|
||||||
let mut configs = Vec::new();
|
let mut configs_without_packages = Vec::new();
|
||||||
for root_config in root_configs {
|
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);
|
configs.push(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -599,7 +625,7 @@ impl ConfigSet {
|
||||||
// Add default entries to specific configs when needed
|
// Add default entries to specific configs when needed
|
||||||
for config in specific.iter_mut() {
|
for config in specific.iter_mut() {
|
||||||
if !config.exclude_default_entries {
|
if !config.exclude_default_entries {
|
||||||
config.merge_default(&default);
|
config.merge_no_overwrite(&default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -618,12 +644,21 @@ impl ConfigSet {
|
||||||
Ok(ConfigSet { default, specific })
|
Ok(ConfigSet { default, specific })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reduce_configs(target: Configs, children_map: &HashMap<String, Vec<Configs>>) -> Configs {
|
fn reduce_configs(
|
||||||
|
target: Configs,
|
||||||
|
children_map: &HashMap<String, Vec<Configs>>,
|
||||||
|
higher_priority: bool,
|
||||||
|
) -> Configs {
|
||||||
if children_map.contains_key(&target.name) {
|
if children_map.contains_key(&target.name) {
|
||||||
let mut target = target;
|
let mut target = target;
|
||||||
for children in children_map.get(&target.name).unwrap() {
|
for children in children_map.get(&target.name).unwrap() {
|
||||||
let children = Self::reduce_configs(children.clone(), children_map);
|
let children =
|
||||||
target.merge_config(children);
|
Self::reduce_configs(children.clone(), children_map, higher_priority);
|
||||||
|
if higher_priority {
|
||||||
|
target.merge_overwrite(children);
|
||||||
|
} else {
|
||||||
|
target.merge_no_overwrite(&children);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
target
|
target
|
||||||
} else {
|
} else {
|
||||||
|
@ -1480,6 +1515,40 @@ mod tests {
|
||||||
.any(|m| m.triggers[0] == "harry"));
|
.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]
|
#[test]
|
||||||
fn test_config_set_package_configs_without_merge() {
|
fn test_config_set_package_configs_without_merge() {
|
||||||
let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
|
let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(
|
||||||
|
|
|
@ -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 {
|
let output = if str_args.len() > 1 {
|
||||||
Command::new(&str_args[0]).args(&str_args[1..]).output()
|
command.args(&str_args[1..]).output()
|
||||||
} else {
|
} else {
|
||||||
Command::new(&str_args[0]).output()
|
command.output()
|
||||||
};
|
};
|
||||||
|
|
||||||
match output {
|
match output {
|
||||||
|
|
|
@ -40,15 +40,38 @@ pub enum Shell {
|
||||||
|
|
||||||
impl Shell {
|
impl Shell {
|
||||||
fn execute_cmd(&self, cmd: &str) -> std::io::Result<Output> {
|
fn execute_cmd(&self, cmd: &str) -> std::io::Result<Output> {
|
||||||
match self {
|
let mut command = match self {
|
||||||
Shell::Cmd => Command::new("cmd").args(&["/C", &cmd]).output(),
|
Shell::Cmd => {
|
||||||
Shell::Powershell => Command::new("powershell")
|
let mut command = Command::new("cmd");
|
||||||
.args(&["-Command", &cmd])
|
command.args(&["/C", &cmd]);
|
||||||
.output(),
|
command
|
||||||
Shell::WSL => Command::new("wsl").args(&["bash", "-c", &cmd]).output(),
|
},
|
||||||
Shell::Bash => Command::new("bash").args(&["-c", &cmd]).output(),
|
Shell::Powershell => {
|
||||||
Shell::Sh => Command::new("sh").args(&["-c", &cmd]).output(),
|
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<Shell> {
|
fn from_string(shell: &str) -> Option<Shell> {
|
||||||
|
@ -143,14 +166,16 @@ impl super::Extension for ShellExtension {
|
||||||
|
|
||||||
// If specified, trim the output
|
// If specified, trim the output
|
||||||
let trim_opt = params.get(&Value::from("trim"));
|
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();
|
let val = value.as_bool();
|
||||||
if let Some(val) = val {
|
val.unwrap_or(true)
|
||||||
if val {
|
}else{
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_trim {
|
||||||
output_str = output_str.trim().to_owned()
|
output_str = output_str.trim().to_owned()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(output_str)
|
Some(output_str)
|
||||||
}
|
}
|
||||||
|
@ -168,9 +193,10 @@ mod tests {
|
||||||
use crate::extension::Extension;
|
use crate::extension::Extension;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_shell_basic() {
|
fn test_shell_not_trimmed() {
|
||||||
let mut params = Mapping::new();
|
let mut params = Mapping::new();
|
||||||
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
||||||
|
params.insert(Value::from("trim"), Value::from(false));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![]);
|
||||||
|
@ -185,10 +211,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_shell_trimmed() {
|
fn test_shell_basic() {
|
||||||
let mut params = Mapping::new();
|
let mut params = Mapping::new();
|
||||||
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
||||||
params.insert(Value::from("trim"), Value::from(true));
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![]);
|
||||||
|
@ -205,8 +230,6 @@ mod tests {
|
||||||
Value::from("echo \" hello world \""),
|
Value::from("echo \" hello world \""),
|
||||||
);
|
);
|
||||||
|
|
||||||
params.insert(Value::from("trim"), Value::from(true));
|
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![]);
|
||||||
|
|
||||||
|
@ -224,11 +247,7 @@ mod tests {
|
||||||
let output = extension.calculate(¶ms, &vec![]);
|
let output = extension.calculate(¶ms, &vec![]);
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
if cfg!(target_os = "windows") {
|
assert_eq!(output.unwrap(), "hello world");
|
||||||
assert_eq!(output.unwrap(), "hello world\r\n");
|
|
||||||
} else {
|
|
||||||
assert_eq!(output.unwrap(), "hello world\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -256,7 +275,7 @@ mod tests {
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
assert_eq!(output.unwrap(), "hello\n");
|
assert_eq!(output.unwrap(), "hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -270,6 +289,6 @@ mod tests {
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
assert_eq!(output.unwrap(), "hello\r\n");
|
assert_eq!(output.unwrap(), "hello");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
23
src/main.rs
23
src/main.rs
|
@ -381,10 +381,31 @@ fn daemon_main(config_set: ConfigSet) {
|
||||||
info!("spawning worker process...");
|
info!("spawning worker process...");
|
||||||
|
|
||||||
let espanso_path = std::env::current_exe().expect("unable to obtain espanso path location");
|
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(),
|
&espanso_path.to_string_lossy().to_string(),
|
||||||
&vec!["worker".to_owned()],
|
&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));
|
std::thread::sleep(Duration::from_millis(200));
|
||||||
|
|
||||||
|
|
|
@ -18,29 +18,19 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use widestring::WideCString;
|
use std::io;
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn spawn_process(cmd: &str, args: &Vec<String>) {
|
pub fn spawn_process(cmd: &str, args: &Vec<String>) -> io::Result<Child> {
|
||||||
let quoted_args: Vec<String> = args.iter().map(|arg| format!("\"{}\"", arg)).collect();
|
use std::os::windows::process::CommandExt;
|
||||||
let quoted_args = quoted_args.join(" ");
|
Command::new(cmd)
|
||||||
let final_cmd = format!("\"{}\" {}", cmd, quoted_args);
|
.creation_flags(0x00000008) // Detached Process
|
||||||
unsafe {
|
.args(args)
|
||||||
let cmd_wstr = WideCString::from_str(&final_cmd);
|
.spawn()
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub fn spawn_process(cmd: &str, args: &Vec<String>) {
|
pub fn spawn_process(cmd: &str, args: &Vec<String>) -> io::Result<Child> {
|
||||||
use std::process::{Command, Stdio};
|
Command::new(cmd).args(args).spawn()
|
||||||
|
|
||||||
Command::new(cmd).args(args).spawn();
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user