From 8030cf16ca676e20840618f1f623f5caf43dcd3c Mon Sep 17 00:00:00 2001 From: Ralph Caraveo Date: Thu, 9 Jul 2020 20:45:17 -0700 Subject: [PATCH 01/28] Adds a debug option for espanso when executing shell commands for greater context --- src/extension/shell.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/extension/shell.rs b/src/extension/shell.rs index f1b9da4..82186ea 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -123,7 +123,7 @@ impl super::Extension for ShellExtension { fn calculate(&self, params: &Mapping, args: &Vec) -> Option { let cmd = params.get(&Value::from("cmd")); if cmd.is_none() { - warn!("No 'cmd' parameter specified for shell variable"); + warn!("No 'cmd' parameter specified for shell `iable"); return None; } let cmd = cmd.unwrap().as_str().unwrap(); @@ -171,6 +171,19 @@ impl super::Extension for ShellExtension { warn!("Shell command reported error: \n{}", error_str); } + // Check if debug flag set, provide additional context when an error occurs. + let debug_opt = params.get(&Value::from("debug")); + let with_debug = if let Some(value) = debug_opt { + let val = value.as_bool(); + val.unwrap_or(false) + }else{ + false + }; + + if with_debug { + error!("debug for shell cmd '{}', exit_status '{}', stdout '{}', stderr '{}'", cmd, output.status, output_str, error_str); + } + // If specified, trim the output let trim_opt = params.get(&Value::from("trim")); let should_trim = if let Some(value) = trim_opt { From a9043e3c5bb48fefda6a4d52cbf9bd7bc7163583 Mon Sep 17 00:00:00 2001 From: Ralph Caraveo Date: Thu, 9 Jul 2020 20:50:33 -0700 Subject: [PATCH 02/28] fix typo --- src/extension/shell.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 82186ea..05a51b0 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -123,7 +123,7 @@ impl super::Extension for ShellExtension { fn calculate(&self, params: &Mapping, args: &Vec) -> Option { let cmd = params.get(&Value::from("cmd")); if cmd.is_none() { - warn!("No 'cmd' parameter specified for shell `iable"); + warn!("No 'cmd' parameter specified for shell variable"); return None; } let cmd = cmd.unwrap().as_str().unwrap(); From 0c49adcc129d38372a1c04cb9dfb5f3afe5a43bf Mon Sep 17 00:00:00 2001 From: Ralph Caraveo Date: Fri, 10 Jul 2020 21:28:20 -0700 Subject: [PATCH 03/28] change error to info --- src/extension/shell.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 05a51b0..ad3dad5 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use log::{error, warn}; +use log::{info, error, warn}; use regex::{Captures, Regex}; use serde_yaml::{Mapping, Value}; use std::process::{Command, Output}; @@ -181,7 +181,7 @@ impl super::Extension for ShellExtension { }; if with_debug { - error!("debug for shell cmd '{}', exit_status '{}', stdout '{}', stderr '{}'", cmd, output.status, output_str, error_str); + info!("debug for shell cmd '{}', exit_status '{}', stdout '{}', stderr '{}'", cmd, output.status, output_str, error_str); } // If specified, trim the output From 239461e520ae7524d1cde468c1a71cba5e2cbd46 Mon Sep 17 00:00:00 2001 From: Ralph Caraveo Date: Fri, 10 Jul 2020 21:47:10 -0700 Subject: [PATCH 04/28] just render the original command --- src/extension/shell.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/extension/shell.rs b/src/extension/shell.rs index ad3dad5..1907513 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use log::{info, error, warn}; +use log::{error, info, warn}; use regex::{Captures, Regex}; use serde_yaml::{Mapping, Value}; use std::process::{Command, Output}; @@ -126,11 +126,12 @@ impl super::Extension for ShellExtension { warn!("No 'cmd' parameter specified for shell variable"); return None; } - let cmd = cmd.unwrap().as_str().unwrap(); + + let original_cmd = cmd.unwrap().as_str().unwrap(); // Render positional parameters in args let cmd = POS_ARG_REGEX - .replace_all(&cmd, |caps: &Captures| { + .replace_all(&original_cmd, |caps: &Captures| { let position_str = caps.name("pos").unwrap().as_str(); let position = position_str.parse::().unwrap_or(-1); if position >= 0 && position < args.len() as i32 { @@ -176,12 +177,15 @@ impl super::Extension for ShellExtension { let with_debug = if let Some(value) = debug_opt { let val = value.as_bool(); val.unwrap_or(false) - }else{ + } else { false }; - + if with_debug { - info!("debug for shell cmd '{}', exit_status '{}', stdout '{}', stderr '{}'", cmd, output.status, output_str, error_str); + info!( + "debug for shell cmd '{}', exit_status '{}', stdout '{}', stderr '{}'", + original_cmd, output.status, output_str, error_str + ); } // If specified, trim the output From 29644ac97dc6141f1e55797c46b4726d255732e0 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 14 Jul 2020 18:44:12 +0200 Subject: [PATCH 05/28] Version bump 0.6.4 --- 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 9a891de..1dc4c2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,7 +371,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.6.3" +version = "0.6.4" 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 6e49975..6debe4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.6.3" +version = "0.6.4" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" diff --git a/snapcraft.yaml b/snapcraft.yaml index 4d783d1..d66e1f5 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: espanso -version: 0.6.3 +version: 0.6.4 summary: A Cross-platform Text Expander written in Rust description: | espanso is a Cross-platform, Text Expander written in Rust. From e784b9479296da42a6546268184956200266982c Mon Sep 17 00:00:00 2001 From: Andy Kluger Date: Wed, 15 Jul 2020 00:08:47 -0400 Subject: [PATCH 06/28] Tighten passive expansion process: - Don't abort if original clipboard is None (why should we?) - Don't abort if original clipboard has the same text as that selected - If original clipboard has text, restore that text before render - If original clipboard has text, restore that text in case of aborted expansion Note: will unfortunately remove non-text data (image) from clipboard if no text is selected when passive expansion is triggered Fixes #372 Fixes #365 (supersedes #368) --- src/engine.rs | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index f48c810..6a30364 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -346,11 +346,14 @@ impl< // In order to avoid pasting previous clipboard contents, we need to check if // a new clipboard was effectively copied. // See issue: https://github.com/federico-terzi/espanso/issues/213 - let previous_clipboard = self.clipboard_manager.get_clipboard(); + let previous_clipboard = self.clipboard_manager.get_clipboard().unwrap_or_default(); // Sleep for a while, giving time to effectively copy the text std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding + // Clear the clipboard, for new-content detection later + self.clipboard_manager.set_clipboard(""); + // Trigger a copy shortcut to transfer the content of the selection to the clipboard self.keyboard_manager.trigger_copy(&config); @@ -360,31 +363,29 @@ impl< // Then get the text from the clipboard and render the match output let clipboard = self.clipboard_manager.get_clipboard(); + // Restore original clipboard now, in case expansion doesn't happen at all + self.clipboard_manager.set_clipboard(&previous_clipboard); + if let Some(clipboard) = clipboard { // Don't expand empty clipboards, as usually they are the result of an empty passive selection if clipboard.trim().is_empty() { info!("Avoiding passive expansion, as the user didn't select anything"); } else { - if let Some(previous_content) = previous_clipboard { - // Because of issue #213, we need to make sure the user selected something. - if clipboard == previous_content { - info!("Avoiding passive expansion, as the user didn't select anything"); - } else { - info!("Passive mode activated"); + info!("Passive mode activated"); - let rendered = self.renderer.render_passive(&clipboard, &config); + let rendered = self.renderer.render_passive(&clipboard, &config); - match rendered { - RenderResult::Text(payload) => { - // Paste back the result in the field - self.clipboard_manager.set_clipboard(&payload); + match rendered { + RenderResult::Text(payload) => { + // Paste back the result in the field + self.clipboard_manager.set_clipboard(&payload); - std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding - self.keyboard_manager.trigger_paste(&config); - } - _ => warn!("Cannot expand passive match"), - } + std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding + self.keyboard_manager.trigger_paste(&config); + + self.clipboard_manager.set_clipboard(&previous_clipboard); } + _ => warn!("Cannot expand passive match"), } } } From a4e30fdc640ad1b4f4f9e60e655bb53ccf77868b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 15 Jul 2020 20:18:25 +0200 Subject: [PATCH 07/28] Add delay and remove hardcoded amount --- src/config/mod.rs | 6 ++++++ src/engine.rs | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 3e68ea1..f1a2914 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -95,6 +95,9 @@ fn default_passive_arg_delimiter() -> char { fn default_passive_arg_escape() -> char { '\\' } +fn default_passive_delay() -> u64 { + 100 +} fn default_passive_key() -> KeyModifier { KeyModifier::OFF } @@ -206,6 +209,9 @@ pub struct Configs { #[serde(default = "default_passive_key")] pub passive_key: KeyModifier, + #[serde(default = "default_passive_delay")] + pub passive_delay: u64, + #[serde(default = "default_enable_passive")] pub enable_passive: bool, diff --git a/src/engine.rs b/src/engine.rs index 6a30364..28e8cf5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -349,23 +349,23 @@ impl< let previous_clipboard = self.clipboard_manager.get_clipboard().unwrap_or_default(); // Sleep for a while, giving time to effectively copy the text - std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding + std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); // Clear the clipboard, for new-content detection later self.clipboard_manager.set_clipboard(""); + // Sleep for a while, giving time to effectively copy the text + std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); + // Trigger a copy shortcut to transfer the content of the selection to the clipboard self.keyboard_manager.trigger_copy(&config); // Sleep for a while, giving time to effectively copy the text - std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding + std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); // Then get the text from the clipboard and render the match output let clipboard = self.clipboard_manager.get_clipboard(); - // Restore original clipboard now, in case expansion doesn't happen at all - self.clipboard_manager.set_clipboard(&previous_clipboard); - if let Some(clipboard) = clipboard { // Don't expand empty clipboards, as usually they are the result of an empty passive selection if clipboard.trim().is_empty() { @@ -380,16 +380,19 @@ impl< // Paste back the result in the field self.clipboard_manager.set_clipboard(&payload); - std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding + std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); self.keyboard_manager.trigger_paste(&config); - - self.clipboard_manager.set_clipboard(&previous_clipboard); } _ => warn!("Cannot expand passive match"), } } } + std::thread::sleep(std::time::Duration::from_millis(config.passive_delay)); + + // Restore original clipboard + self.clipboard_manager.set_clipboard(&previous_clipboard); + // Re-allow espanso to interpret actions self.is_injecting.store(false, Release); } From 8428457be1714694ed82fe26c7cbecc600410a2e Mon Sep 17 00:00:00 2001 From: Andy Kluger Date: Wed, 15 Jul 2020 16:47:18 -0400 Subject: [PATCH 08/28] Restore original clipboard before render, in case it's used during render --- src/engine.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/engine.rs b/src/engine.rs index 28e8cf5..6a760e8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -373,6 +373,9 @@ impl< } else { info!("Passive mode activated"); + // Restore original clipboard in case it's used during render + self.clipboard_manager.set_clipboard(&previous_clipboard); + let rendered = self.renderer.render_passive(&clipboard, &config); match rendered { From 2a49231fbf90a8187699e9ab921354abdfdca383 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 2 Aug 2020 18:31:24 +0200 Subject: [PATCH 09/28] Version bump 0.7.0 --- 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 1dc4c2f..be4ac70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,7 +371,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.6.4" +version = "0.7.0" 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 6debe4d..629a8d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.6.4" +version = "0.7.0" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" diff --git a/snapcraft.yaml b/snapcraft.yaml index d66e1f5..11a2593 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: espanso -version: 0.6.4 +version: 0.7.0 summary: A Cross-platform Text Expander written in Rust description: | espanso is a Cross-platform, Text Expander written in Rust. From a6b78e714253a60c38266a339c95d99e0cc1257d Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 3 Aug 2020 22:03:39 +0200 Subject: [PATCH 10/28] Refactor variable system --- src/config/mod.rs | 6 ++ src/extension/clipboard.rs | 10 +- src/extension/date.rs | 6 +- src/extension/dummy.rs | 18 ++-- src/extension/mod.rs | 16 +++- src/extension/multiecho.rs | 48 ++++++++++ src/extension/random.rs | 14 +-- src/extension/script.rs | 50 ++++++++-- src/extension/shell.rs | 87 ++++++++++++++---- src/extension/utils.rs | 46 ++++++++++ src/extension/vardummy.rs | 47 ++++++++++ src/matcher/mod.rs | 2 +- src/render/default.rs | 183 ++++++++++++++++++++++++++++++++----- src/ui/mod.rs | 2 + src/ui/modulo/form.rs | 0 src/ui/modulo/mod.rs | 113 +++++++++++++++++++++++ 16 files changed, 578 insertions(+), 70 deletions(-) create mode 100644 src/extension/multiecho.rs create mode 100644 src/extension/utils.rs create mode 100644 src/extension/vardummy.rs create mode 100644 src/ui/modulo/form.rs create mode 100644 src/ui/modulo/mod.rs diff --git a/src/config/mod.rs b/src/config/mod.rs index 3e68ea1..d335f7e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -146,6 +146,9 @@ fn default_matches() -> Vec { fn default_global_vars() -> Vec { Vec::new() } +fn default_modulo_path() -> Option { + 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, + + #[serde(default = "default_modulo_path")] + pub modulo_path: Option } // Macro used to validate config fields diff --git a/src/extension/clipboard.rs b/src/extension/clipboard.rs index 60846ca..1ee8367 100644 --- a/src/extension/clipboard.rs +++ b/src/extension/clipboard.rs @@ -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, @@ -35,7 +37,11 @@ impl super::Extension for ClipboardExtension { String::from("clipboard") } - fn calculate(&self, _: &Mapping, _: &Vec) -> Option { - self.clipboard_manager.get_clipboard() + fn calculate(&self, _: &Mapping, _: &Vec, _: &HashMap) -> Option { + if let Some(clipboard) = self.clipboard_manager.get_clipboard() { + Some(ExtensionResult::Single(clipboard)) + } else { + None + } } } diff --git a/src/extension/date.rs b/src/extension/date.rs index ac15295..918a1e9 100644 --- a/src/extension/date.rs +++ b/src/extension/date.rs @@ -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) -> Option { + fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { let now: DateTime = 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)) } } diff --git a/src/extension/dummy.rs b/src/extension/dummy.rs index 6bbb1dd..934fbee 100644 --- a/src/extension/dummy.rs +++ b/src/extension/dummy.rs @@ -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) -> Option { + fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { 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 } diff --git a/src/extension/mod.rs b/src/extension/mod.rs index 9a363c5..a25406b 100644 --- a/src/extension/mod.rs +++ b/src/extension/mod.rs @@ -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), +} pub trait Extension { fn name(&self) -> String; - fn calculate(&self, params: &Mapping, args: &Vec) -> Option; + fn calculate(&self, params: &Mapping, args: &Vec, current_vars: &HashMap) -> Option; } pub fn get_extensions(clipboard_manager: Box) -> Vec> { @@ -38,7 +48,9 @@ pub fn get_extensions(clipboard_manager: Box) -> Vec. + */ + +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, _: &HashMap) -> Option { + let mut output: HashMap = 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)) + } +} diff --git a/src/extension/random.rs b/src/extension/random.rs index 5d168c9..200c738 100644 --- a/src/extension/random.rs +++ b/src/extension/random.rs @@ -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) -> Option { + fn calculate(&self, params: &Mapping, args: &Vec, _: &HashMap) -> Option { 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)); } } diff --git a/src/extension/script.rs b/src/extension/script.rs index 6144854..2b1791d 100644 --- a/src/extension/script.rs +++ b/src/extension/script.rs @@ -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) -> Option { + fn calculate(&self, params: &Mapping, user_args: &Vec, vars: &HashMap) -> Option { 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 = 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"); } } diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 1907513..2a77aff 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -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 { + fn execute_cmd(&self, cmd: &str, vars: &HashMap) -> std::io::Result { 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) -> Option { + fn calculate(&self, params: &Mapping, args: &Vec, vars: &HashMap) -> Option { 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 = 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 = 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())); } } diff --git a/src/extension/utils.rs b/src/extension/utils.rs new file mode 100644 index 0000000..d963a79 --- /dev/null +++ b/src/extension/utils.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; +use crate::extension::ExtensionResult; + +pub fn convert_to_env_variables(original_vars: &HashMap) -> HashMap { + 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 = 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"); + } +} \ No newline at end of file diff --git a/src/extension/vardummy.rs b/src/extension/vardummy.rs new file mode 100644 index 0000000..8274737 --- /dev/null +++ b/src/extension/vardummy.rs @@ -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 . + */ + +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, vars: &HashMap) -> Option { + 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 + } + } +} diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 6914f97..27e2336 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -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() { diff --git a/src/render/default.rs b/src/render/default.rs index 6c9fcbc..af8ee13 100644 --- a/src/render/default.rs +++ b/src/render/default.rs @@ -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\\w+)\\s*\\}\\}").unwrap(); + static ref VAR_REGEX: Regex = Regex::new(r"\{\{\s*((?P\w+)(\.(?P(\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 = 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 = 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 = 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"); + } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1400b19..f25491c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -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); diff --git a/src/ui/modulo/form.rs b/src/ui/modulo/form.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/modulo/mod.rs b/src/ui/modulo/mod.rs new file mode 100644 index 0000000..93c7921 --- /dev/null +++ b/src/ui/modulo/mod.rs @@ -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, +} + +impl ModuloManager { + pub fn new(config: &Configs) -> Self { + let mut modulo_path: Option = 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 { + 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 { + 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 + } +} \ No newline at end of file From 61c17b26a4570e19355a68422587ec2dea04b9e6 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 8 Aug 2020 22:25:13 +0200 Subject: [PATCH 11/28] Add experimental backspace undo feature --- src/config/mod.rs | 6 ++ src/engine.rs | 132 ++++++++++++++++++++++++--------------- src/matcher/mod.rs | 1 + src/matcher/scrolling.rs | 26 +++++++- 4 files changed, 112 insertions(+), 53 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index d335f7e..a11e212 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -131,6 +131,9 @@ fn default_show_notifications() -> bool { fn default_auto_restart() -> bool { true } +fn default_undo_backspace() -> bool { + true +} fn default_show_icon() -> bool { true } @@ -215,6 +218,9 @@ pub struct Configs { #[serde(default = "default_enable_active")] pub enable_active: bool, + #[serde(default = "default_undo_backspace")] + pub undo_backspace: bool, + #[serde(default)] pub paste_shortcut: PasteShortcut, diff --git a/src/engine.rs b/src/engine.rs index f48c810..b8c3b6a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -19,7 +19,7 @@ use crate::clipboard::ClipboardManager; use crate::config::BackendType; -use crate::config::ConfigManager; +use crate::config::{Configs, ConfigManager}; use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver}; use crate::keyboard::KeyboardManager; use crate::matcher::{Match, MatchReceiver}; @@ -50,6 +50,8 @@ pub struct Engine< is_injecting: Arc, enabled: RefCell, + // Trigger string and injected text len pair + last_expansion_data: RefCell>, } impl< @@ -70,6 +72,7 @@ impl< is_injecting: Arc, ) -> Engine<'a, S, C, M, U, R> { let enabled = RefCell::new(true); + let last_expansion_data = RefCell::new(None); Engine { keyboard_manager, @@ -79,6 +82,7 @@ impl< renderer, is_injecting, enabled, + last_expansion_data, } } @@ -147,17 +151,63 @@ impl< } } + fn inject_text(&self, config: &Configs, target_string: &str, force_clipboard: bool) { + let backend = if force_clipboard { + &BackendType::Clipboard + } else if config.backend == BackendType::Auto { + if cfg!(target_os = "linux") { + let all_ascii = target_string.chars().all(|c| c.is_ascii()); + if all_ascii { + debug!( + "All elements of the replacement are ascii, using Inject backend" + ); + &BackendType::Inject + } else { + debug!("There are non-ascii characters, using Clipboard backend"); + &BackendType::Clipboard + } + } else { + &BackendType::Inject + } + } else { + &config.backend + }; + + match backend { + BackendType::Inject => { + // To handle newlines, substitute each "\n" char with an Enter key press. + let splits = target_string.split('\n'); + + for (i, split) in splits.enumerate() { + if i > 0 { + self.keyboard_manager.send_enter(&config); + } + + self.keyboard_manager.send_string(&config, split); + } + } + BackendType::Clipboard => { + self.clipboard_manager.set_clipboard(&target_string); + self.keyboard_manager.trigger_paste(&config); + } + _ => { + error!("Unsupported backend type evaluation."); + return; + } + } + } + fn inject_match( &self, m: &Match, trailing_separator: Option, trigger_offset: usize, skip_delete: bool, - ) { + ) -> Option<(String, i32)> { let config = self.config_manager.active_config(); if !config.enable_active { - return; + return None; } // Block espanso from reinterpreting its own actions @@ -179,6 +229,8 @@ impl< .renderer .render_match(m, trigger_offset, config, vec![]); + let mut expansion_data: Option<(String, i32)> = None; + match rendered { RenderResult::Text(mut target_string) => { // If a trailing separator was counted in the match, add it back to the target string @@ -213,54 +265,13 @@ impl< None }; - let backend = if m.force_clipboard { - &BackendType::Clipboard - } else if config.backend == BackendType::Auto { - if cfg!(target_os = "linux") { - let all_ascii = target_string.chars().all(|c| c.is_ascii()); - if all_ascii { - debug!( - "All elements of the replacement are ascii, using Inject backend" - ); - &BackendType::Inject - } else { - debug!("There are non-ascii characters, using Clipboard backend"); - &BackendType::Clipboard - } - } else { - &BackendType::Inject - } - } else { - &config.backend - }; + // If the preserve_clipboard option is enabled, save the current + // clipboard content to restore it later. + previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); - match backend { - BackendType::Inject => { - // To handle newlines, substitute each "\n" char with an Enter key press. - let splits = target_string.split('\n'); + self.inject_text(&config, &target_string, m.force_clipboard); - for (i, split) in splits.enumerate() { - if i > 0 { - self.keyboard_manager.send_enter(&config); - } - - self.keyboard_manager.send_string(&config, split); - } - } - BackendType::Clipboard => { - // If the preserve_clipboard option is enabled, save the current - // clipboard content to restore it later. - previous_clipboard_content = - self.return_content_if_preserve_clipboard_is_enabled(); - - self.clipboard_manager.set_clipboard(&target_string); - self.keyboard_manager.trigger_paste(&config); - } - _ => { - error!("Unsupported backend type evaluation."); - return; - } - } + expansion_data = Some((m.triggers[trigger_offset].clone(), target_string.chars().count() as i32)); if let Some(moves) = cursor_rewind { // Simulate left arrow key presses to bring the cursor into the desired position @@ -294,6 +305,8 @@ impl< // Re-allow espanso to interpret actions self.is_injecting.store(false, Release); + + expansion_data } } @@ -311,7 +324,26 @@ impl< > MatchReceiver for Engine<'a, S, C, M, U, R> { fn on_match(&self, m: &Match, trailing_separator: Option, trigger_offset: usize) { - self.inject_match(m, trailing_separator, trigger_offset, false); + let expansion_data = self.inject_match(m, trailing_separator, trigger_offset, false); + let mut last_expansion_data = self.last_expansion_data.borrow_mut(); + (*last_expansion_data) = expansion_data; + } + + fn on_undo(&self) { + let config = self.config_manager.active_config(); + + if !config.undo_backspace { + return; + } + + let last_expansion_data = self.last_expansion_data.borrow(); + if let Some(ref last_expansion_data) = *last_expansion_data { + let (trigger_string, injected_text_len) = last_expansion_data; + // Delete the previously injected text, minus one character as it has been consumed by the backspace + self.keyboard_manager.delete_string(&config, *injected_text_len - 1); + // Restore previous text + self.inject_text(&config, trigger_string, false); + } } fn on_enable_update(&self, status: bool) { diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 27e2336..00b602c 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -273,6 +273,7 @@ pub trait MatchReceiver { fn on_match(&self, m: &Match, trailing_separator: Option, trigger_offset: usize); fn on_enable_update(&self, status: bool); fn on_passive(&self); + fn on_undo(&self); } pub trait Matcher: KeyEventReceiver { diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 0dd38a4..c070c4a 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -33,6 +33,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { passive_press_time: RefCell, is_enabled: RefCell, was_previous_char_word_separator: RefCell, + was_previous_char_a_match: RefCell, } #[derive(Clone)] @@ -57,6 +58,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { passive_press_time, is_enabled: RefCell::new(true), was_previous_char_word_separator: RefCell::new(true), + was_previous_char_a_match: RefCell::new(true), } } @@ -111,6 +113,9 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat } } + let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); + (*was_previous_char_a_match) = false; + let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut(); let mut current_set_queue = self.current_set_queue.borrow_mut(); @@ -192,9 +197,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat if let Some(entry) = found_entry { let mtc = entry._match; - if let Some(last) = current_set_queue.back_mut() { - last.clear(); - } + current_set_queue.clear(); let trailing_separator = if !mtc.word { // If it's not a word match, it cannot have a trailing separator @@ -216,12 +219,17 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat self.receiver .on_match(mtc, trailing_separator, entry.trigger_offset); + + + (*was_previous_char_a_match) = true; } } fn handle_modifier(&self, m: KeyModifier) { let config = self.config_manager.default_config(); + let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); + // TODO: at the moment, activating the passive key triggers the toggle key // study a mechanism to avoid this problem @@ -253,8 +261,16 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat if m == BACKSPACE { let mut current_set_queue = self.current_set_queue.borrow_mut(); current_set_queue.pop_back(); + + if (*was_previous_char_a_match) { + current_set_queue.clear(); + self.receiver.on_undo(); + } } + // Disable the "backspace undo" feature + (*was_previous_char_a_match) = false; + // Consider modifiers as separators to improve word matches reliability if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK { let mut was_previous_char_word_separator = @@ -269,6 +285,10 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat let mut was_previous_char_word_separator = self.was_previous_char_word_separator.borrow_mut(); *was_previous_char_word_separator = true; + + // Disable the "backspace undo" feature + let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); + (*was_previous_char_a_match) = false; } } From 400d8cf9d8b60433f0ed522bde0f84bac1175991 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 8 Aug 2020 22:47:11 +0200 Subject: [PATCH 12/28] Disallow backspace undo if cursor hints are used --- src/engine.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/engine.rs b/src/engine.rs index b8c3b6a..eb8fb65 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -271,7 +271,10 @@ impl< self.inject_text(&config, &target_string, m.force_clipboard); - expansion_data = Some((m.triggers[trigger_offset].clone(), target_string.chars().count() as i32)); + // Disallow undo backspace if cursor positioning is used + if cursor_rewind.is_none() { + expansion_data = Some((m.triggers[trigger_offset].clone(), target_string.chars().count() as i32)); + } if let Some(moves) = cursor_rewind { // Simulate left arrow key presses to bring the cursor into the desired position From 45f90c87edeb4f2281b946b70f20ef790267a3a6 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 9 Aug 2020 11:48:23 +0200 Subject: [PATCH 13/28] Add form extension --- src/extension/form.rs | 78 +++++++++++++++++++++++++++++++++++++++++++ src/extension/mod.rs | 6 ++-- src/main.rs | 2 +- src/render/default.rs | 4 --- src/ui/modulo/mod.rs | 13 +++++--- 5 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 src/extension/form.rs diff --git a/src/extension/form.rs b/src/extension/form.rs new file mode 100644 index 0000000..b5b8e83 --- /dev/null +++ b/src/extension/form.rs @@ -0,0 +1,78 @@ +/* + * 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 . + */ + +use serde_yaml::{Mapping, Value}; +use std::collections::HashMap; +use crate::{ui::modulo::ModuloManager, extension::ExtensionResult, config::Configs}; +use log::error; + +pub struct FormExtension { + manager: ModuloManager, +} + +impl FormExtension { + pub fn new(config: &Configs) -> FormExtension { + let manager = ModuloManager::new(config); + FormExtension { + manager, + } + } +} + +impl super::Extension for FormExtension { + fn name(&self) -> String { + "form".to_owned() + } + + fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { + let layout = params.get(&Value::from("layout")); + let layout = if let Some(value) = layout { + value.as_str().unwrap_or_default().to_string() + } else { + error!("invoking form extension without specifying a layout"); + return None; + }; + + let mut form_config = Mapping::new(); + form_config.insert(Value::from("layout"), Value::from(layout)); + + if let Some(fields) = params.get(&Value::from("fields")) { + form_config.insert(Value::from("fields"), fields.clone()); + } + + let serialized_config: String = serde_yaml::to_string(&form_config).expect("unable to serialize form config"); + + let output = self.manager.invoke(&["form", "-i", "-"], &serialized_config); + if let Some(output) = output { + let json: Result, _> = serde_json::from_str(&output); + match json { + Ok(json) => { + return Some(ExtensionResult::Multiple(json)); + } + Err(error) => { + error!("modulo json parsing error: {}", error); + return None; + } + } + } else { + error!("modulo form didn't return any output"); + return None; + } + } +} diff --git a/src/extension/mod.rs b/src/extension/mod.rs index a25406b..a6a27a4 100644 --- a/src/extension/mod.rs +++ b/src/extension/mod.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use crate::clipboard::ClipboardManager; +use crate::{config::Configs, clipboard::ClipboardManager}; use serde_yaml::Mapping; use std::collections::HashMap; @@ -30,6 +30,7 @@ mod shell; pub mod multiecho; pub mod vardummy; mod utils; +mod form; #[derive(Clone, Debug, PartialEq)] pub enum ExtensionResult { @@ -42,7 +43,7 @@ pub trait Extension { fn calculate(&self, params: &Mapping, args: &Vec, current_vars: &HashMap) -> Option; } -pub fn get_extensions(clipboard_manager: Box) -> Vec> { +pub fn get_extensions(config: &Configs, clipboard_manager: Box) -> Vec> { vec![ Box::new(date::DateExtension::new()), Box::new(shell::ShellExtension::new()), @@ -52,5 +53,6 @@ pub fn get_extensions(clipboard_manager: Box) -> Vec = variables.into_iter().map(|variable| { if variable.var_type == "global" { @@ -131,8 +129,6 @@ impl super::Renderer for DefaultRenderer { variable }).collect(); - println!("{:?}", variables); - let mut output_map: HashMap = HashMap::new(); for variable in variables.into_iter() { diff --git a/src/ui/modulo/mod.rs b/src/ui/modulo/mod.rs index 93c7921..93c523c 100644 --- a/src/ui/modulo/mod.rs +++ b/src/ui/modulo/mod.rs @@ -12,10 +12,13 @@ pub struct ModuloManager { impl ModuloManager { pub fn new(config: &Configs) -> Self { let mut modulo_path: Option = None; - if let Some(ref _modulo_path) = config.modulo_path { + // Check if the `MODULO_PATH` env variable is configured + if let Some(_modulo_path) = std::env::var_os("MODULO_PATH") { + modulo_path = Some(_modulo_path.to_string_lossy().to_string()) + } else if let Some(ref _modulo_path) = config.modulo_path { // Check the configs modulo_path = Some(_modulo_path.to_owned()); }else{ - // First check in the same directory of espanso + // 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"); @@ -59,16 +62,18 @@ impl ModuloManager { None } - fn invoke(&self, args: &[&str], body: &str) -> Option { + pub fn invoke(&self, args: &[&str], body: &str) -> Option { 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) + let child = Command::new(modulo_path) .args(args) .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) .spawn(); match child { From 316ebbb502819af43f7bbd66d062573d92ecf37b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 9 Aug 2020 19:20:33 +0200 Subject: [PATCH 14/28] Inject Env Variables in WSL shell command --- src/extension/shell.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 2a77aff..0c6a30b 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -43,6 +43,8 @@ pub enum Shell { impl Shell { fn execute_cmd(&self, cmd: &str, vars: &HashMap) -> std::io::Result { + let mut is_wsl = false; + let mut command = match self { Shell::Cmd => { let mut command = Command::new("cmd"); @@ -55,11 +57,13 @@ impl Shell { command } Shell::WSL => { + is_wsl = true; let mut command = Command::new("bash"); command.args(&["-c", &cmd]); command } Shell::WSL2 => { + is_wsl = true; let mut command = Command::new("wsl"); command.args(&["bash", "-c", &cmd]); command @@ -84,6 +88,22 @@ impl Shell { command.env(key, value); } + // In WSL environment, we have to specify which ENV variables + // should be passed to linux. + // For more information: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/ + if is_wsl { + let mut tokens: Vec<&str> = Vec::new(); + tokens.push("CONFIG/p"); + + // Add all the previous variables + for (key, _) in vars.iter() { + tokens.push(key); + } + + let wsl_env = tokens.join(":"); + command.env("WSLENV", wsl_env); + } + command.output() } From 34e08e6ec8fbe658901499e009fb03e4096cfffc Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 11 Aug 2020 22:04:44 +0200 Subject: [PATCH 15/28] Add form translation --- src/extension/script.rs | 6 +- src/matcher/mod.rs | 123 ++++++++++++++++++++++++++++++++++++++-- src/ui/modulo/mod.rs | 6 +- 3 files changed, 125 insertions(+), 10 deletions(-) diff --git a/src/extension/script.rs b/src/extension/script.rs index 2b1791d..e62caea 100644 --- a/src/extension/script.rs +++ b/src/extension/script.rs @@ -211,7 +211,7 @@ mod tests { let mut params = Mapping::new(); params.insert( Value::from("args"), - Value::from(vec!["echo", "$ESPANSO_VAR1 $ESPANSO_FORM1_NAME"]), + Value::from(vec!["bash", "-c", "echo $ESPANSO_VAR1 $ESPANSO_FORM1_NAME"]), ); let mut vars: HashMap = HashMap::new(); @@ -221,9 +221,9 @@ mod tests { vars.insert("var1".to_owned(), ExtensionResult::Single("hello".to_owned())); let extension = ScriptExtension::new(); - let output = extension.calculate(¶ms, &vec![]); + let output = extension.calculate(¶ms, &vec![], &vars); assert!(output.is_some()); - assert_eq!(output.unwrap(), "hello Jon"); + assert_eq!(output.unwrap(), ExtensionResult::Single("hello John".to_owned())); } } diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 27e2336..571b773 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -1,5 +1,5 @@ /* - * This file is part of espanso. + * This file is part of espans{ name: (), var_type: (), params: ()} * * Copyright (C) 2019 Federico Terzi * @@ -19,9 +19,10 @@ use crate::event::KeyEventReceiver; use crate::event::{KeyEvent, KeyModifier}; -use regex::Regex; +use regex::{Captures, Regex}; use serde::{Deserialize, Deserializer, Serialize}; -use serde_yaml::Mapping; +use serde_yaml::{Mapping, Value}; +use std::collections::HashMap; use std::fs; use std::path::PathBuf; @@ -47,7 +48,7 @@ pub enum MatchContentType { Image(ImageContent), } -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Serialize, Clone, PartialEq)] pub struct TextContent { pub replace: String, pub vars: Vec, @@ -143,6 +144,40 @@ impl<'a> From<&'a AutoMatch> for Match { _has_vars: has_vars, }; + MatchContentType::Text(content) + } else if let Some(form) = &other.form { // Form shorthand + // Replace all the form fields with actual variables + let new_replace = VAR_REGEX.replace_all(&form, |caps: &Captures| { + let var_name = caps.get(1).unwrap().as_str(); + format!("{{{{form1.{}}}}}", var_name) + }); + let new_replace = new_replace.to_string(); + + // Convert the form data to valid variables + let mut params = Mapping::new(); + if let Some(fields) = &other.form_fields { + let mut mapping_fields = Mapping::new(); + fields.iter().for_each(|(key, value)| { + mapping_fields.insert(Value::from(key.to_owned()), Value::from(value.clone())); + }); + params.insert(Value::from("fields"), Value::from(mapping_fields)); + } + params.insert(Value::from("layout"), Value::from(form.to_owned())); + + let vars = vec![ + MatchVariable { + name: "form1".to_owned(), + var_type: "form".to_owned(), + params, + } + ]; + + let content = TextContent { + replace: new_replace, + vars, + _has_vars: true, + }; + MatchContentType::Text(content) } else if let Some(image_path) = &other.image_path { // Image match @@ -173,7 +208,7 @@ impl<'a> From<&'a AutoMatch> for Match { MatchContentType::Image(content) } else { - eprintln!("ERROR: no action specified for match {}, please specify either 'replace' or 'image_path'", other.trigger); + eprintln!("ERROR: no action specified for match {}, please specify either 'replace', 'image_path' or 'form'", other.trigger); std::process::exit(2); }; @@ -204,6 +239,12 @@ struct AutoMatch { #[serde(default = "default_image_path")] pub image_path: Option, + #[serde(default = "default_form")] + pub form: Option, + + #[serde(default = "default_form_fields")] + pub form_fields: Option>, + #[serde(default = "default_vars")] pub vars: Vec, @@ -238,6 +279,12 @@ fn default_passive_only() -> bool { fn default_replace() -> Option { None } +fn default_form() -> Option { + None +} +fn default_form_fields() -> Option> { + None +} fn default_image_path() -> Option { None } @@ -248,7 +295,7 @@ fn default_force_clipboard() -> bool { false } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct MatchVariable { pub name: String, @@ -543,4 +590,68 @@ mod tests { assert_eq!(_match.triggers, vec![":..", ":..", ":.."]) } + + #[test] + fn test_match_form_translated_correctly() { + let match_str = r###" + trigger: ":test" + form: "Hey {{name}}, how are you? {{greet}}" + "###; + + let _match: Match = serde_yaml::from_str(match_str).unwrap(); + match _match.content { + MatchContentType::Text(content) => { + let mut mapping = Mapping::new(); + mapping.insert(Value::from("layout"), Value::from("Hey {{name}}, how are you? {{greet}}")); + assert_eq!(content, TextContent { + replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(), + _has_vars: true, + vars: vec![ + MatchVariable { + name: "form1".to_owned(), + var_type: "form".to_owned(), + params: mapping, + } + ] + }); + }, + _ => panic!("wrong content") + } + } + + #[test] + fn test_match_form_with_fields_translated_correctly() { + let match_str = r###" + trigger: ":test" + form: "Hey {{name}}, how are you? {{greet}}" + form_fields: + name: + multiline: true + "###; + + let _match: Match = serde_yaml::from_str(match_str).unwrap(); + match _match.content { + MatchContentType::Text(content) => { + let mut name_mapping = Mapping::new(); + name_mapping.insert(Value::from("multiline"), Value::Bool(true)); + let mut submapping = Mapping::new(); + submapping.insert(Value::from("name"), Value::from(name_mapping)); + let mut mapping = Mapping::new(); + mapping.insert(Value::from("fields"), Value::from(submapping)); + mapping.insert(Value::from("layout"), Value::from("Hey {{name}}, how are you? {{greet}}")); + assert_eq!(content, TextContent { + replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(), + _has_vars: true, + vars: vec![ + MatchVariable { + name: "form1".to_owned(), + var_type: "form".to_owned(), + params: mapping, + } + ] + }); + }, + _ => panic!("wrong content") + } + } } diff --git a/src/ui/modulo/mod.rs b/src/ui/modulo/mod.rs index 93c523c..83b1b48 100644 --- a/src/ui/modulo/mod.rs +++ b/src/ui/modulo/mod.rs @@ -1,6 +1,6 @@ use crate::config::Configs; use std::process::{Command, Child, Output}; -use log::{error}; +use log::{error, info}; use std::io::{Error, Write}; pub mod form; @@ -42,6 +42,10 @@ impl ModuloManager { } } + if let Some(ref modulo_path) = modulo_path { + info!("Using modulo at {:?}", modulo_path); + } + Self { modulo_path, } From 64dd7b90741df44d9cbc648d17aaebe3ecd1fe8b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 12 Aug 2020 20:37:15 +0200 Subject: [PATCH 16/28] Fix undo backspace on macOS --- src/config/mod.rs | 6 ++++++ src/engine.rs | 7 +++++++ src/matcher/scrolling.rs | 7 ------- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index a11e212..a2adafe 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -152,6 +152,9 @@ fn default_global_vars() -> Vec { fn default_modulo_path() -> Option { None } +fn default_mac_post_inject_delay() -> u64 { + 100 +} #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Configs { @@ -235,6 +238,9 @@ pub struct Configs { #[serde(default = "default_secure_input_watcher_interval")] pub secure_input_watcher_interval: i32, + + #[serde(default = "default_mac_post_inject_delay")] + pub mac_post_inject_delay: u64, #[serde(default = "default_secure_input_notification")] pub secure_input_notification: bool, diff --git a/src/engine.rs b/src/engine.rs index eb8fb65..9c2680d 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -306,6 +306,13 @@ impl< .set_clipboard(&previous_clipboard_content); } + // On macOS, because the keyinjection is async, we need to wait a bit before + // giving back the control. Otherwise, the injected actions will be handled back + // by espanso itself. + if cfg!(target_os = "macos") { + std::thread::sleep(std::time::Duration::from_millis(config.mac_post_inject_delay)); + } + // Re-allow espanso to interpret actions self.is_injecting.store(false, Release); diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index c070c4a..38c6d58 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -106,13 +106,6 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat .word_separators .contains(&c.chars().nth(0).unwrap_or_default()); - // Workaround needed on macos to consider espanso replacement key presses as separators. - if cfg!(target_os = "macos") { - if c.len() > 1 { - is_current_word_separator = true; - } - } - let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); (*was_previous_char_a_match) = false; From 97d11fd34e00b4aff2e182d459b2de21ca4f619a Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 12 Aug 2020 22:04:02 +0200 Subject: [PATCH 17/28] Improve macOS form handling --- native/libmacbridge/bridge.h | 5 +++++ native/libmacbridge/bridge.mm | 8 ++++++++ src/bridge/macos.rs | 1 + src/extension/form.rs | 19 ++++++++++++++++++- src/keyboard/macos.rs | 14 +++++++++++++- src/keyboard/mod.rs | 2 +- 6 files changed, 46 insertions(+), 3 deletions(-) diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h index 4a0f1d8..169c3cf 100644 --- a/native/libmacbridge/bridge.h +++ b/native/libmacbridge/bridge.h @@ -76,6 +76,11 @@ void send_multi_vkey(int32_t vk, int32_t count); */ void delete_string(int32_t count); +/* + * Check whether keyboard modifiers (CTRL, CMD, SHIFT, ecc) are pressed + */ +int32_t are_modifiers_pressed(); + /* * Trigger normal paste ( Pressing CMD+V ) */ diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm index 1ddd549..bafe1ae 100644 --- a/native/libmacbridge/bridge.mm +++ b/native/libmacbridge/bridge.mm @@ -235,6 +235,14 @@ void trigger_copy() { }); } +int32_t are_modifiers_pressed() { + if ((NSEventModifierFlagControl | NSEventModifierFlagOption | + NSEventModifierFlagCommand | NSEventModifierFlagShift) & [NSEvent modifierFlags]) { + return 1; + } + return 0; +} + int32_t get_active_app_bundle(char * buffer, int32_t size) { NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; NSString *bundlePath = [frontApp bundleURL].path; diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs index cb87675..c9f8c9f 100644 --- a/src/bridge/macos.rs +++ b/src/bridge/macos.rs @@ -63,4 +63,5 @@ extern "C" { pub fn delete_string(count: i32); pub fn trigger_paste(); pub fn trigger_copy(); + pub fn are_modifiers_pressed() -> i32; } diff --git a/src/extension/form.rs b/src/extension/form.rs index b5b8e83..6a2e2d2 100644 --- a/src/extension/form.rs +++ b/src/extension/form.rs @@ -20,7 +20,7 @@ use serde_yaml::{Mapping, Value}; use std::collections::HashMap; use crate::{ui::modulo::ModuloManager, extension::ExtensionResult, config::Configs}; -use log::error; +use log::{error, warn}; pub struct FormExtension { manager: ModuloManager, @@ -59,6 +59,10 @@ impl super::Extension for FormExtension { let serialized_config: String = serde_yaml::to_string(&form_config).expect("unable to serialize form config"); let output = self.manager.invoke(&["form", "-i", "-"], &serialized_config); + + // On macOS, after the form closes we have to wait until the user releases the modifier keys + on_form_close(); + if let Some(output) = output { let json: Result, _> = serde_json::from_str(&output); match json { @@ -76,3 +80,16 @@ impl super::Extension for FormExtension { } } } + +#[cfg(not(target_os = "macos"))] +fn on_form_close() { + // NOOP on Windows and Linux +} + +#[cfg(target_os = "macos")] +fn on_form_close() { + let released = crate::keyboard::macos::wait_for_modifiers_release(); + if !released { + warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)"); + } +} \ No newline at end of file diff --git a/src/keyboard/macos.rs b/src/keyboard/macos.rs index a5ea03e..284091a 100644 --- a/src/keyboard/macos.rs +++ b/src/keyboard/macos.rs @@ -20,7 +20,7 @@ use super::PasteShortcut; use crate::bridge::macos::*; use crate::config::Configs; -use log::error; +use log::{error}; use std::ffi::CString; pub struct MacKeyboardManager {} @@ -75,3 +75,15 @@ impl super::KeyboardManager for MacKeyboardManager { } } } + +pub fn wait_for_modifiers_release() -> bool { + let start = std::time::SystemTime::now(); + while start.elapsed().unwrap_or_default().as_millis() < 3000 { + let pressed = unsafe { crate::bridge::macos::are_modifiers_pressed() }; + if pressed == 0 { + return true + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + false +} \ No newline at end of file diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs index add9499..dece480 100644 --- a/src/keyboard/mod.rs +++ b/src/keyboard/mod.rs @@ -27,7 +27,7 @@ mod windows; mod linux; #[cfg(target_os = "macos")] -mod macos; +pub mod macos; pub trait KeyboardManager { fn send_string(&self, active_config: &Configs, s: &str); From 9383264952e19caeddd5fbff9f0fad0950a0d08d Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Thu, 13 Aug 2020 19:02:35 +0200 Subject: [PATCH 18/28] Update gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f4ff355..3619f48 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ DerivedData *.snap -venv/ \ No newline at end of file +venv/ + +.vscode/ \ No newline at end of file From 67bd2fefe334d8dae20afc8b378e410faca61028 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Thu, 13 Aug 2020 19:16:09 +0200 Subject: [PATCH 19/28] Avoid reloading espanso with hidden files. Fix #393 --- src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1a7accb..bb6b47e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -566,8 +566,9 @@ fn watcher_background(sender: Sender) { }; if let Some(path) = path { - if path.extension().unwrap_or_default() == "yml" { - // Only load yml files + if path.extension().unwrap_or_default() == "yml" && + !path.file_name().unwrap_or_default().to_string_lossy().starts_with("."){ + // Only load non-hidden yml files true } else { false From 537e1360bb26f02e70b26b63ff10bc738b7fd8fe Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 15 Aug 2020 18:11:23 +0200 Subject: [PATCH 20/28] Add offset parameter to date extension. Fix #311 --- src/extension/date.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/extension/date.rs b/src/extension/date.rs index 918a1e9..98aa62b 100644 --- a/src/extension/date.rs +++ b/src/extension/date.rs @@ -1,7 +1,7 @@ /* * This file is part of espanso. * - * Copyright (C) 2019 Federico Terzi + * Copyright (C) 2019-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 @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use chrono::{DateTime, Local}; +use chrono::{DateTime, Local, Duration}; use serde_yaml::{Mapping, Value}; use std::collections::HashMap; use crate::extension::ExtensionResult; @@ -36,7 +36,15 @@ impl super::Extension for DateExtension { } fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { - let now: DateTime = Local::now(); + let mut now: DateTime = Local::now(); + + // Compute the given offset + let offset = params.get(&Value::from("offset")); + if let Some(offset) = offset { + let seconds = offset.as_i64().unwrap_or_else(|| { 0 }); + let offset = Duration::seconds(seconds); + now = now + offset; + } let format = params.get(&Value::from("format")); From fbb2eb663bd0d3616ee9625443c88d3165b385d4 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 15 Aug 2020 19:02:32 +0200 Subject: [PATCH 21/28] Avoid showing shell window on Windows. Fix #249. Improve cross-platform script paths. Fix #380 --- Cargo.lock | 49 +++++++++++++++++++++-------------------- Cargo.toml | 1 + src/extension/script.rs | 10 +++++++++ src/extension/shell.rs | 3 +++ src/extension/utils.rs | 13 +++++++++++ src/main.rs | 16 ++++++++++++++ 6 files changed, 68 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be4ac70..08ff7f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,7 +18,7 @@ name = "ansi_term" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -45,7 +45,7 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -181,7 +181,7 @@ dependencies = [ "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -212,7 +212,7 @@ dependencies = [ "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -334,7 +334,7 @@ dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "redox_users 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -397,6 +397,7 @@ dependencies = [ "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "zip 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -428,7 +429,7 @@ dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -465,7 +466,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -808,7 +809,7 @@ name = "named_pipe" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -835,7 +836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -857,7 +858,7 @@ dependencies = [ "mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)", "mio-extras 2.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -941,7 +942,7 @@ dependencies = [ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1028,7 +1029,7 @@ dependencies = [ "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1113,7 +1114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1126,7 +1127,7 @@ dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1191,7 +1192,7 @@ name = "remove_dir_all" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1269,7 +1270,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1461,7 +1462,7 @@ dependencies = [ "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1470,7 +1471,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1504,7 +1505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1731,7 +1732,7 @@ version = "2.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1762,7 +1763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "winapi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1784,7 +1785,7 @@ name = "winapi-util" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1797,7 +1798,7 @@ name = "winreg" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2028,7 +2029,7 @@ dependencies = [ "checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" "checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" -"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" diff --git a/Cargo.toml b/Cargo.toml index 629a8d2..aea476f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ signal-hook = "0.1.15" [target.'cfg(windows)'.dependencies] named_pipe = "0.4.1" +winapi = { version = "0.3.9", features = ["wincon"] } [build-dependencies] cmake = "0.1.31" diff --git a/src/extension/script.rs b/src/extension/script.rs index e62caea..8fec252 100644 --- a/src/extension/script.rs +++ b/src/extension/script.rs @@ -62,11 +62,18 @@ impl super::Extension for ScriptExtension { // Replace %HOME% with current user home directory to // create cross-platform paths. See issue #265 + // Also replace %CONFIG% and %PACKAGES% path. See issue #380 let home_dir = dirs::home_dir().unwrap_or_default(); str_args.iter_mut().for_each(|arg| { if arg.contains("%HOME%") { *arg = arg.replace("%HOME%", &home_dir.to_string_lossy().to_string()); } + if arg.contains("%CONFIG%") { + *arg = arg.replace("%CONFIG%", &crate::context::get_config_dir().to_string_lossy().to_string()); + } + if arg.contains("%PACKAGES%") { + *arg = arg.replace("%PACKAGES%", &crate::context::get_package_dir().to_string_lossy().to_string()); + } // On Windows, correct paths separators if cfg!(target_os = "windows") { @@ -79,6 +86,9 @@ impl super::Extension for ScriptExtension { let mut command = Command::new(&str_args[0]); + // Set the OS-specific flags + super::utils::set_command_flags(&mut command); + // Inject the $CONFIG variable command.env("CONFIG", crate::context::get_config_dir()); diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 0c6a30b..bac9748 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -80,6 +80,9 @@ impl Shell { } }; + // Set the OS-specific flags + super::utils::set_command_flags(&mut command); + // Inject the $CONFIG variable command.env("CONFIG", crate::context::get_config_dir()); diff --git a/src/extension/utils.rs b/src/extension/utils.rs index d963a79..c5578f4 100644 --- a/src/extension/utils.rs +++ b/src/extension/utils.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::process::Command; use crate::extension::ExtensionResult; pub fn convert_to_env_variables(original_vars: &HashMap) -> HashMap { @@ -22,6 +23,18 @@ pub fn convert_to_env_variables(original_vars: &HashMap output } +#[cfg(target_os = "windows")] +pub fn set_command_flags(command: &mut Command) { + use std::os::windows::process::CommandExt; + // Avoid showing the shell window + // See: https://github.com/federico-terzi/espanso/issues/249 + command.creation_flags(0x08000000); +} + +#[cfg(not(target_os = "windows"))] +pub fn set_command_flags(command: &mut Command) { + // NOOP on Linux and macOS +} #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index bb6b47e..aada978 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,8 @@ * along with espanso. If not, see . */ +#![windows_subsystem = "windows"] + #[macro_use] extern crate lazy_static; @@ -76,6 +78,8 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); const LOG_FILE: &str = "espanso.log"; fn main() { + attach_console(); + let install_subcommand = SubCommand::with_name("install") .about("Install a package. Equivalent to 'espanso package install'") .arg( @@ -344,6 +348,18 @@ fn main() { println!(); } +#[cfg(target_os = "windows")] +fn attach_console() { + // When using the windows subsystem we loose the terminal output. + // Therefore we try to attach to the current console if available. + unsafe {winapi::um::wincon::AttachConsole(0xFFFFFFFF)}; +} + +#[cfg(not(target_os = "windows"))] +fn attach_console() { + // Not necessary on Linux and macOS +} + fn init_logger(config_set: &ConfigSet, reset: bool) { // Initialize log let log_level = match config_set.default.log_level { From 161017f0240d567240e5b10b7ea21866b20c1a92 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 15 Aug 2020 19:34:30 +0200 Subject: [PATCH 22/28] Fix bug that prevented test runs on Windows --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index aada978..714a2ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -#![windows_subsystem = "windows"] + #![cfg_attr(not(test), windows_subsystem = "windows")] #[macro_use] extern crate lazy_static; From 411078a5034e595a6ca874d32f27e1b7fa964bae Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 15 Aug 2020 19:35:14 +0200 Subject: [PATCH 23/28] Fix formatting --- src/config/mod.rs | 4 +- src/engine.rs | 18 +++++---- src/extension/clipboard.rs | 9 ++++- src/extension/date.rs | 13 +++++-- src/extension/dummy.rs | 13 +++++-- src/extension/form.rs | 42 ++++++++++++--------- src/extension/mod.rs | 20 +++++++--- src/extension/multiecho.rs | 9 ++++- src/extension/random.rs | 17 +++++++-- src/extension/script.rs | 55 +++++++++++++++++++++------ src/extension/shell.rs | 50 +++++++++++++++++++------ src/extension/utils.rs | 18 +++++---- src/extension/vardummy.rs | 9 ++++- src/keyboard/macos.rs | 8 ++-- src/main.rs | 18 ++++++--- src/matcher/mod.rs | 76 +++++++++++++++++++++----------------- src/matcher/scrolling.rs | 9 ++--- src/render/default.rs | 75 ++++++++++++++++++------------------- src/ui/modulo/form.rs | 1 + src/ui/modulo/mod.rs | 27 +++++++------- 20 files changed, 312 insertions(+), 179 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 2f20e47..13dea99 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -244,7 +244,7 @@ pub struct Configs { #[serde(default = "default_secure_input_watcher_interval")] pub secure_input_watcher_interval: i32, - + #[serde(default = "default_mac_post_inject_delay")] pub mac_post_inject_delay: u64, @@ -282,7 +282,7 @@ pub struct Configs { pub global_vars: Vec, #[serde(default = "default_modulo_path")] - pub modulo_path: Option + pub modulo_path: Option, } // Macro used to validate config fields diff --git a/src/engine.rs b/src/engine.rs index 762e488..2042fcd 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -19,7 +19,7 @@ use crate::clipboard::ClipboardManager; use crate::config::BackendType; -use crate::config::{Configs, ConfigManager}; +use crate::config::{ConfigManager, Configs}; use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver}; use crate::keyboard::KeyboardManager; use crate::matcher::{Match, MatchReceiver}; @@ -158,9 +158,7 @@ impl< if cfg!(target_os = "linux") { let all_ascii = target_string.chars().all(|c| c.is_ascii()); if all_ascii { - debug!( - "All elements of the replacement are ascii, using Inject backend" - ); + debug!("All elements of the replacement are ascii, using Inject backend"); &BackendType::Inject } else { debug!("There are non-ascii characters, using Clipboard backend"); @@ -273,7 +271,10 @@ impl< // Disallow undo backspace if cursor positioning is used if cursor_rewind.is_none() { - expansion_data = Some((m.triggers[trigger_offset].clone(), target_string.chars().count() as i32)); + expansion_data = Some(( + m.triggers[trigger_offset].clone(), + target_string.chars().count() as i32, + )); } if let Some(moves) = cursor_rewind { @@ -310,7 +311,9 @@ impl< // giving back the control. Otherwise, the injected actions will be handled back // by espanso itself. if cfg!(target_os = "macos") { - std::thread::sleep(std::time::Duration::from_millis(config.mac_post_inject_delay)); + std::thread::sleep(std::time::Duration::from_millis( + config.mac_post_inject_delay, + )); } // Re-allow espanso to interpret actions @@ -350,7 +353,8 @@ impl< if let Some(ref last_expansion_data) = *last_expansion_data { let (trigger_string, injected_text_len) = last_expansion_data; // Delete the previously injected text, minus one character as it has been consumed by the backspace - self.keyboard_manager.delete_string(&config, *injected_text_len - 1); + self.keyboard_manager + .delete_string(&config, *injected_text_len - 1); // Restore previous text self.inject_text(&config, trigger_string, false); } diff --git a/src/extension/clipboard.rs b/src/extension/clipboard.rs index 1ee8367..9b5477b 100644 --- a/src/extension/clipboard.rs +++ b/src/extension/clipboard.rs @@ -18,8 +18,8 @@ */ use crate::clipboard::ClipboardManager; -use serde_yaml::Mapping; use crate::extension::ExtensionResult; +use serde_yaml::Mapping; use std::collections::HashMap; pub struct ClipboardExtension { @@ -37,7 +37,12 @@ impl super::Extension for ClipboardExtension { String::from("clipboard") } - fn calculate(&self, _: &Mapping, _: &Vec, _: &HashMap) -> Option { + fn calculate( + &self, + _: &Mapping, + _: &Vec, + _: &HashMap, + ) -> Option { if let Some(clipboard) = self.clipboard_manager.get_clipboard() { Some(ExtensionResult::Single(clipboard)) } else { diff --git a/src/extension/date.rs b/src/extension/date.rs index 98aa62b..2e4324b 100644 --- a/src/extension/date.rs +++ b/src/extension/date.rs @@ -17,10 +17,10 @@ * along with espanso. If not, see . */ -use chrono::{DateTime, Local, Duration}; +use crate::extension::ExtensionResult; +use chrono::{DateTime, Duration, Local}; use serde_yaml::{Mapping, Value}; use std::collections::HashMap; -use crate::extension::ExtensionResult; pub struct DateExtension {} @@ -35,13 +35,18 @@ impl super::Extension for DateExtension { String::from("date") } - fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { + fn calculate( + &self, + params: &Mapping, + _: &Vec, + _: &HashMap, + ) -> Option { let mut now: DateTime = Local::now(); // Compute the given offset let offset = params.get(&Value::from("offset")); if let Some(offset) = offset { - let seconds = offset.as_i64().unwrap_or_else(|| { 0 }); + let seconds = offset.as_i64().unwrap_or_else(|| 0); let offset = Duration::seconds(seconds); now = now + offset; } diff --git a/src/extension/dummy.rs b/src/extension/dummy.rs index 934fbee..e810823 100644 --- a/src/extension/dummy.rs +++ b/src/extension/dummy.rs @@ -17,9 +17,9 @@ * along with espanso. If not, see . */ +use crate::extension::ExtensionResult; use serde_yaml::{Mapping, Value}; use std::collections::HashMap; -use crate::extension::ExtensionResult; pub struct DummyExtension { name: String, @@ -38,11 +38,18 @@ impl super::Extension for DummyExtension { self.name.clone() } - fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { + fn calculate( + &self, + params: &Mapping, + _: &Vec, + _: &HashMap, + ) -> Option { let echo = params.get(&Value::from("echo")); if let Some(echo) = echo { - Some(ExtensionResult::Single(echo.as_str().unwrap_or_default().to_owned())) + Some(ExtensionResult::Single( + echo.as_str().unwrap_or_default().to_owned(), + )) } else { None } diff --git a/src/extension/form.rs b/src/extension/form.rs index 6a2e2d2..5b9abcd 100644 --- a/src/extension/form.rs +++ b/src/extension/form.rs @@ -17,10 +17,10 @@ * along with espanso. If not, see . */ +use crate::{config::Configs, extension::ExtensionResult, ui::modulo::ModuloManager}; +use log::{error, warn}; use serde_yaml::{Mapping, Value}; use std::collections::HashMap; -use crate::{ui::modulo::ModuloManager, extension::ExtensionResult, config::Configs}; -use log::{error, warn}; pub struct FormExtension { manager: ModuloManager, @@ -29,9 +29,7 @@ pub struct FormExtension { impl FormExtension { pub fn new(config: &Configs) -> FormExtension { let manager = ModuloManager::new(config); - FormExtension { - manager, - } + FormExtension { manager } } } @@ -40,7 +38,12 @@ impl super::Extension for FormExtension { "form".to_owned() } - fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { + fn calculate( + &self, + params: &Mapping, + _: &Vec, + _: &HashMap, + ) -> Option { let layout = params.get(&Value::from("layout")); let layout = if let Some(value) = layout { value.as_str().unwrap_or_default().to_string() @@ -48,21 +51,24 @@ impl super::Extension for FormExtension { error!("invoking form extension without specifying a layout"); return None; }; - + let mut form_config = Mapping::new(); form_config.insert(Value::from("layout"), Value::from(layout)); - - if let Some(fields) = params.get(&Value::from("fields")) { - form_config.insert(Value::from("fields"), fields.clone()); - } - - let serialized_config: String = serde_yaml::to_string(&form_config).expect("unable to serialize form config"); - let output = self.manager.invoke(&["form", "-i", "-"], &serialized_config); - + if let Some(fields) = params.get(&Value::from("fields")) { + form_config.insert(Value::from("fields"), fields.clone()); + } + + let serialized_config: String = + serde_yaml::to_string(&form_config).expect("unable to serialize form config"); + + let output = self + .manager + .invoke(&["form", "-i", "-"], &serialized_config); + // On macOS, after the form closes we have to wait until the user releases the modifier keys on_form_close(); - + if let Some(output) = output { let json: Result, _> = serde_json::from_str(&output); match json { @@ -77,7 +83,7 @@ impl super::Extension for FormExtension { } else { error!("modulo form didn't return any output"); return None; - } + } } } @@ -92,4 +98,4 @@ fn on_form_close() { if !released { warn!("Wait for modifiers release timed out! Please after closing the form, release your modifiers keys (CTRL, CMD, ALT, SHIFT)"); } -} \ No newline at end of file +} diff --git a/src/extension/mod.rs b/src/extension/mod.rs index a6a27a4..7ea9546 100644 --- a/src/extension/mod.rs +++ b/src/extension/mod.rs @@ -17,20 +17,20 @@ * along with espanso. If not, see . */ -use crate::{config::Configs, clipboard::ClipboardManager}; +use crate::{clipboard::ClipboardManager, config::Configs}; use serde_yaml::Mapping; use std::collections::HashMap; mod clipboard; mod date; pub mod dummy; +mod form; +pub mod multiecho; mod random; mod script; mod shell; -pub mod multiecho; -pub mod vardummy; mod utils; -mod form; +pub mod vardummy; #[derive(Clone, Debug, PartialEq)] pub enum ExtensionResult { @@ -40,10 +40,18 @@ pub enum ExtensionResult { pub trait Extension { fn name(&self) -> String; - fn calculate(&self, params: &Mapping, args: &Vec, current_vars: &HashMap) -> Option; + fn calculate( + &self, + params: &Mapping, + args: &Vec, + current_vars: &HashMap, + ) -> Option; } -pub fn get_extensions(config: &Configs, clipboard_manager: Box) -> Vec> { +pub fn get_extensions( + config: &Configs, + clipboard_manager: Box, +) -> Vec> { vec![ Box::new(date::DateExtension::new()), Box::new(shell::ShellExtension::new()), diff --git a/src/extension/multiecho.rs b/src/extension/multiecho.rs index f2ac32e..e48eaf8 100644 --- a/src/extension/multiecho.rs +++ b/src/extension/multiecho.rs @@ -17,9 +17,9 @@ * along with espanso. If not, see . */ +use crate::extension::ExtensionResult; use serde_yaml::{Mapping, Value}; use std::collections::HashMap; -use crate::extension::ExtensionResult; pub struct MultiEchoExtension {} @@ -34,7 +34,12 @@ impl super::Extension for MultiEchoExtension { "multiecho".to_owned() } - fn calculate(&self, params: &Mapping, _: &Vec, _: &HashMap) -> Option { + fn calculate( + &self, + params: &Mapping, + _: &Vec, + _: &HashMap, + ) -> Option { let mut output: HashMap = HashMap::new(); for (key, value) in params.iter() { if let Some(key) = key.as_str() { diff --git a/src/extension/random.rs b/src/extension/random.rs index 200c738..3a13d3f 100644 --- a/src/extension/random.rs +++ b/src/extension/random.rs @@ -17,11 +17,11 @@ * along with espanso. If not, see . */ +use crate::extension::ExtensionResult; use log::{error, warn}; use rand::seq::SliceRandom; use serde_yaml::{Mapping, Value}; use std::collections::HashMap; -use crate::extension::ExtensionResult; pub struct RandomExtension {} @@ -36,7 +36,12 @@ impl super::Extension for RandomExtension { String::from("random") } - fn calculate(&self, params: &Mapping, args: &Vec, _: &HashMap) -> Option { + fn calculate( + &self, + params: &Mapping, + args: &Vec, + _: &HashMap, + ) -> Option { let choices = params.get(&Value::from("choices")); if choices.is_none() { warn!("No 'choices' parameter specified for random variable"); @@ -89,7 +94,9 @@ mod tests { let output = output.unwrap(); - assert!(choices.into_iter().any(|x| ExtensionResult::Single(x.to_owned()) == output)); + assert!(choices + .into_iter() + .any(|x| ExtensionResult::Single(x.to_owned()) == output)); } #[test] @@ -107,6 +114,8 @@ mod tests { let rendered_choices = vec!["first test", "second test", "test third"]; - assert!(rendered_choices.into_iter().any(|x| ExtensionResult::Single(x.to_owned()) == output)); + assert!(rendered_choices + .into_iter() + .any(|x| ExtensionResult::Single(x.to_owned()) == output)); } } diff --git a/src/extension/script.rs b/src/extension/script.rs index 8fec252..7261ed2 100644 --- a/src/extension/script.rs +++ b/src/extension/script.rs @@ -17,12 +17,12 @@ * along with espanso. If not, see . */ +use crate::extension::ExtensionResult; use log::{error, warn}; use serde_yaml::{Mapping, Value}; +use std::collections::HashMap; use std::path::PathBuf; use std::process::Command; -use std::collections::HashMap; -use crate::extension::ExtensionResult; pub struct ScriptExtension {} @@ -37,7 +37,12 @@ impl super::Extension for ScriptExtension { String::from("script") } - fn calculate(&self, params: &Mapping, user_args: &Vec, vars: &HashMap) -> Option { + fn calculate( + &self, + params: &Mapping, + user_args: &Vec, + vars: &HashMap, + ) -> Option { let args = params.get(&Value::from("args")); if args.is_none() { warn!("No 'args' parameter specified for script variable"); @@ -69,10 +74,20 @@ impl super::Extension for ScriptExtension { *arg = arg.replace("%HOME%", &home_dir.to_string_lossy().to_string()); } if arg.contains("%CONFIG%") { - *arg = arg.replace("%CONFIG%", &crate::context::get_config_dir().to_string_lossy().to_string()); + *arg = arg.replace( + "%CONFIG%", + &crate::context::get_config_dir() + .to_string_lossy() + .to_string(), + ); } if arg.contains("%PACKAGES%") { - *arg = arg.replace("%PACKAGES%", &crate::context::get_package_dir().to_string_lossy().to_string()); + *arg = arg.replace( + "%PACKAGES%", + &crate::context::get_package_dir() + .to_string_lossy() + .to_string(), + ); } // On Windows, correct paths separators @@ -162,7 +177,10 @@ mod tests { let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world".to_owned()) + ); } #[test] @@ -179,7 +197,10 @@ mod tests { let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world\n".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world\n".to_owned()) + ); } #[test] @@ -195,7 +216,10 @@ mod tests { let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world".to_owned()) + ); } #[test] @@ -212,7 +236,10 @@ mod tests { let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world jon".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world jon".to_owned()) + ); } #[test] @@ -228,12 +255,18 @@ mod tests { 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())); + vars.insert( + "var1".to_owned(), + ExtensionResult::Single("hello".to_owned()), + ); let extension = ScriptExtension::new(); let output = extension.calculate(¶ms, &vec![], &vars); assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello John".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello John".to_owned()) + ); } } diff --git a/src/extension/shell.rs b/src/extension/shell.rs index bac9748..3beac0b 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -17,12 +17,12 @@ * along with espanso. If not, see . */ +use crate::extension::ExtensionResult; 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; +use std::process::{Command, Output}; lazy_static! { static ref POS_ARG_REGEX: Regex = if cfg!(target_os = "windows") { @@ -150,7 +150,12 @@ impl super::Extension for ShellExtension { String::from("shell") } - fn calculate(&self, params: &Mapping, args: &Vec, vars: &HashMap) -> Option { + fn calculate( + &self, + params: &Mapping, + args: &Vec, + vars: &HashMap, + ) -> Option { let cmd = params.get(&Value::from("cmd")); if cmd.is_none() { warn!("No 'cmd' parameter specified for shell variable"); @@ -260,9 +265,15 @@ mod tests { assert!(output.is_some()); if cfg!(target_os = "windows") { - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world\r\n".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world\r\n".to_owned()) + ); } else { - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world\n".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world\n".to_owned()) + ); } } @@ -275,7 +286,10 @@ mod tests { let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world".to_owned()) + ); } #[test] @@ -290,7 +304,10 @@ mod tests { let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world".to_owned()) + ); } #[test] @@ -303,7 +320,10 @@ mod tests { let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world".to_owned()) + ); } #[test] @@ -317,7 +337,10 @@ mod tests { let output = extension.calculate(¶ms, &vec![], &HashMap::new()); assert!(output.is_some()); - assert_eq!(output.unwrap(), ExtensionResult::Single("hello world".to_owned())); + assert_eq!( + output.unwrap(), + ExtensionResult::Single("hello world".to_owned()) + ); } #[test] @@ -354,13 +377,16 @@ mod tests { if cfg!(target_os = "windows") { params.insert(Value::from("cmd"), Value::from("echo %ESPANSO_VAR1%")); params.insert(Value::from("shell"), Value::from("cmd")); - }else{ + } else { params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_VAR1")); } let extension = ShellExtension::new(); let mut vars: HashMap = HashMap::new(); - vars.insert("var1".to_owned(), ExtensionResult::Single("hello".to_owned())); + vars.insert( + "var1".to_owned(), + ExtensionResult::Single("hello".to_owned()), + ); let output = extension.calculate(¶ms, &vec![], &vars); assert!(output.is_some()); @@ -373,7 +399,7 @@ mod tests { 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{ + } else { params.insert(Value::from("cmd"), Value::from("echo $ESPANSO_FORM1_NAME")); } diff --git a/src/extension/utils.rs b/src/extension/utils.rs index c5578f4..0844cc4 100644 --- a/src/extension/utils.rs +++ b/src/extension/utils.rs @@ -1,8 +1,10 @@ +use crate::extension::ExtensionResult; use std::collections::HashMap; use std::process::Command; -use crate::extension::ExtensionResult; -pub fn convert_to_env_variables(original_vars: &HashMap) -> HashMap { +pub fn convert_to_env_variables( + original_vars: &HashMap, +) -> HashMap { let mut output = HashMap::new(); for (key, result) in original_vars.iter() { @@ -10,13 +12,13 @@ pub fn convert_to_env_variables(original_vars: &HashMap 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()); } - }, + } } } @@ -36,7 +38,6 @@ pub fn set_command_flags(command: &mut Command) { // NOOP on Linux and macOS } - #[cfg(test)] mod tests { use super::*; @@ -49,11 +50,14 @@ mod tests { 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())); + 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"); } -} \ No newline at end of file +} diff --git a/src/extension/vardummy.rs b/src/extension/vardummy.rs index 8274737..4108ae1 100644 --- a/src/extension/vardummy.rs +++ b/src/extension/vardummy.rs @@ -17,9 +17,9 @@ * along with espanso. If not, see . */ +use crate::extension::ExtensionResult; use serde_yaml::{Mapping, Value}; use std::collections::HashMap; -use crate::extension::ExtensionResult; pub struct VarDummyExtension {} @@ -34,7 +34,12 @@ impl super::Extension for VarDummyExtension { "vardummy".to_owned() } - fn calculate(&self, params: &Mapping, _: &Vec, vars: &HashMap) -> Option { + fn calculate( + &self, + params: &Mapping, + _: &Vec, + vars: &HashMap, + ) -> Option { let target = params.get(&Value::from("target")); if let Some(target) = target { diff --git a/src/keyboard/macos.rs b/src/keyboard/macos.rs index 284091a..b7d9699 100644 --- a/src/keyboard/macos.rs +++ b/src/keyboard/macos.rs @@ -20,7 +20,7 @@ use super::PasteShortcut; use crate::bridge::macos::*; use crate::config::Configs; -use log::{error}; +use log::error; use std::ffi::CString; pub struct MacKeyboardManager {} @@ -78,12 +78,12 @@ impl super::KeyboardManager for MacKeyboardManager { pub fn wait_for_modifiers_release() -> bool { let start = std::time::SystemTime::now(); - while start.elapsed().unwrap_or_default().as_millis() < 3000 { + while start.elapsed().unwrap_or_default().as_millis() < 3000 { let pressed = unsafe { crate::bridge::macos::are_modifiers_pressed() }; if pressed == 0 { - return true + return true; } std::thread::sleep(std::time::Duration::from_millis(100)); } false -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 714a2ca..bb9c0ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ - #![cfg_attr(not(test), windows_subsystem = "windows")] +#![cfg_attr(not(test), windows_subsystem = "windows")] #[macro_use] extern crate lazy_static; @@ -352,7 +352,7 @@ fn main() { fn attach_console() { // When using the windows subsystem we loose the terminal output. // Therefore we try to attach to the current console if available. - unsafe {winapi::um::wincon::AttachConsole(0xFFFFFFFF)}; + unsafe { winapi::um::wincon::AttachConsole(0xFFFFFFFF) }; } #[cfg(not(target_os = "windows"))] @@ -582,8 +582,13 @@ fn watcher_background(sender: Sender) { }; if let Some(path) = path { - if path.extension().unwrap_or_default() == "yml" && - !path.file_name().unwrap_or_default().to_string_lossy().starts_with("."){ + if path.extension().unwrap_or_default() == "yml" + && !path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .starts_with(".") + { // Only load non-hidden yml files true } else { @@ -688,7 +693,10 @@ fn worker_background( let keyboard_manager = keyboard::get_manager(); - let extensions = extension::get_extensions(config_manager.default_config(), Box::new(clipboard::get_manager())); + let extensions = extension::get_extensions( + config_manager.default_config(), + Box::new(clipboard::get_manager()), + ); let renderer = render::default::DefaultRenderer::new(extensions, config_manager.default_config().clone()); diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 03b85fa..4b64c4b 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -75,7 +75,8 @@ 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+)(\\.\\w+)?\\s*\\}\\}").unwrap(); + static ref VAR_REGEX: Regex = + Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap(); }; let mut triggers = if !other.triggers.is_empty() { @@ -145,14 +146,15 @@ impl<'a> From<&'a AutoMatch> for Match { }; MatchContentType::Text(content) - } else if let Some(form) = &other.form { // Form shorthand + } else if let Some(form) = &other.form { + // Form shorthand // Replace all the form fields with actual variables let new_replace = VAR_REGEX.replace_all(&form, |caps: &Captures| { let var_name = caps.get(1).unwrap().as_str(); format!("{{{{form1.{}}}}}", var_name) }); let new_replace = new_replace.to_string(); - + // Convert the form data to valid variables let mut params = Mapping::new(); if let Some(fields) = &other.form_fields { @@ -163,14 +165,12 @@ impl<'a> From<&'a AutoMatch> for Match { params.insert(Value::from("fields"), Value::from(mapping_fields)); } params.insert(Value::from("layout"), Value::from(form.to_owned())); - - let vars = vec![ - MatchVariable { - name: "form1".to_owned(), - var_type: "form".to_owned(), - params, - } - ]; + + let vars = vec![MatchVariable { + name: "form1".to_owned(), + var_type: "form".to_owned(), + params, + }]; let content = TextContent { replace: new_replace, @@ -241,7 +241,7 @@ struct AutoMatch { #[serde(default = "default_form")] pub form: Option, - + #[serde(default = "default_form_fields")] pub form_fields: Option>, @@ -603,20 +603,24 @@ mod tests { match _match.content { MatchContentType::Text(content) => { let mut mapping = Mapping::new(); - mapping.insert(Value::from("layout"), Value::from("Hey {{name}}, how are you? {{greet}}")); - assert_eq!(content, TextContent { - replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(), - _has_vars: true, - vars: vec![ - MatchVariable { + mapping.insert( + Value::from("layout"), + Value::from("Hey {{name}}, how are you? {{greet}}"), + ); + assert_eq!( + content, + TextContent { + replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(), + _has_vars: true, + vars: vec![MatchVariable { name: "form1".to_owned(), var_type: "form".to_owned(), params: mapping, - } - ] - }); - }, - _ => panic!("wrong content") + }] + } + ); + } + _ => panic!("wrong content"), } } @@ -639,20 +643,24 @@ mod tests { submapping.insert(Value::from("name"), Value::from(name_mapping)); let mut mapping = Mapping::new(); mapping.insert(Value::from("fields"), Value::from(submapping)); - mapping.insert(Value::from("layout"), Value::from("Hey {{name}}, how are you? {{greet}}")); - assert_eq!(content, TextContent { - replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(), - _has_vars: true, - vars: vec![ - MatchVariable { + mapping.insert( + Value::from("layout"), + Value::from("Hey {{name}}, how are you? {{greet}}"), + ); + assert_eq!( + content, + TextContent { + replace: "Hey {{form1.name}}, how are you? {{form1.greet}}".to_owned(), + _has_vars: true, + vars: vec![MatchVariable { name: "form1".to_owned(), var_type: "form".to_owned(), params: mapping, - } - ] - }); - }, - _ => panic!("wrong content") + }] + } + ); + } + _ => panic!("wrong content"), } } } diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 38c6d58..d78cb48 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -106,7 +106,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat .word_separators .contains(&c.chars().nth(0).unwrap_or_default()); - let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); + let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); (*was_previous_char_a_match) = false; let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut(); @@ -212,8 +212,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat self.receiver .on_match(mtc, trailing_separator, entry.trigger_offset); - - + (*was_previous_char_a_match) = true; } } @@ -221,7 +220,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat fn handle_modifier(&self, m: KeyModifier) { let config = self.config_manager.default_config(); - let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); + let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); // TODO: at the moment, activating the passive key triggers the toggle key // study a mechanism to avoid this problem @@ -280,7 +279,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat *was_previous_char_word_separator = true; // Disable the "backspace undo" feature - let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); + let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); (*was_previous_char_a_match) = false; } } diff --git a/src/render/default.rs b/src/render/default.rs index 55e1eb2..40d1857 100644 --- a/src/render/default.rs +++ b/src/render/default.rs @@ -27,7 +27,8 @@ use serde_yaml::Value; use std::collections::{HashMap, HashSet}; lazy_static! { - static ref VAR_REGEX: Regex = Regex::new(r"\{\{\s*((?P\w+)(\.(?P(\w+)))?)\s*\}\}").unwrap(); + static ref VAR_REGEX: Regex = + Regex::new(r"\{\{\s*((?P\w+)(\.(?P(\w+)))?)\s*\}\}").unwrap(); static ref UNKNOWN_VARIABLE: String = "".to_string(); } @@ -95,9 +96,8 @@ impl super::Renderer for DefaultRenderer { target_vars.insert(var_name.to_owned()); } - let match_variables: HashSet<&String> = content.vars.iter().map(|var| { - &var.name - }).collect(); + let match_variables: HashSet<&String> = + content.vars.iter().map(|var| &var.name).collect(); // Find the global variables that are not specified in the var list let mut missing_globals = Vec::new(); @@ -106,7 +106,7 @@ impl super::Renderer for DefaultRenderer { if target_vars.contains(&global_var.name) { if match_variables.contains(&global_var.name) { specified_globals.insert(global_var.name.clone(), &global_var); - }else { + } else { missing_globals.push(global_var); } } @@ -120,14 +120,18 @@ impl super::Renderer for DefaultRenderer { variables.extend(&content.vars); // 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(); + 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(); + variable + }) + .collect(); let mut output_map: HashMap = HashMap::new(); @@ -178,11 +182,15 @@ 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, &output_map); + 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(), ExtensionResult::Single("".to_owned())); + output_map.insert( + variable.name.clone(), + ExtensionResult::Single("".to_owned()), + ); warn!( "Could not generate output for variable: {}", variable.name @@ -202,28 +210,23 @@ impl super::Renderer for DefaultRenderer { let var_name = caps.name("name").unwrap().as_str(); 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 + 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, } }); @@ -793,8 +796,6 @@ mod tests { verify_render(rendered, "RESULT"); } - - #[test] fn test_render_variable_order() { let config = get_config_for( diff --git a/src/ui/modulo/form.rs b/src/ui/modulo/form.rs index e69de29..8b13789 100644 --- a/src/ui/modulo/form.rs +++ b/src/ui/modulo/form.rs @@ -0,0 +1 @@ + diff --git a/src/ui/modulo/mod.rs b/src/ui/modulo/mod.rs index 83b1b48..0bf3543 100644 --- a/src/ui/modulo/mod.rs +++ b/src/ui/modulo/mod.rs @@ -1,7 +1,7 @@ use crate::config::Configs; -use std::process::{Command, Child, Output}; use log::{error, info}; use std::io::{Error, Write}; +use std::process::{Child, Command, Output}; pub mod form; @@ -15,9 +15,10 @@ impl ModuloManager { // Check if the `MODULO_PATH` env variable is configured if let Some(_modulo_path) = std::env::var_os("MODULO_PATH") { modulo_path = Some(_modulo_path.to_string_lossy().to_string()) - } else if let Some(ref _modulo_path) = config.modulo_path { // Check the configs + } else if let Some(ref _modulo_path) = config.modulo_path { + // Check the configs modulo_path = Some(_modulo_path.to_owned()); - }else{ + } else { // Check in the same directory of espanso if let Ok(exe_path) = std::env::current_exe() { if let Some(parent) = exe_path.parent() { @@ -46,9 +47,7 @@ impl ModuloManager { info!("Using modulo at {:?}", modulo_path); } - Self { - modulo_path, - } + Self { modulo_path } } pub fn is_valid(&self) -> bool { @@ -97,26 +96,26 @@ impl ModuloManager { } 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{ + } else { error!("unable to open stdin to modulo"); } - }, + } Err(error) => { error!("error reported when invoking modulo: {}", error); - }, + } } } None } -} \ No newline at end of file +} From b9fad308785292a36f6fa742c0dadc83d78e2f41 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 15 Aug 2020 21:07:41 +0200 Subject: [PATCH 24/28] Make tray icon red on Windows when disabled. Fix #293 --- native/libwinbridge/bridge.cpp | 17 ++++++++++++++++- native/libwinbridge/bridge.h | 7 ++++++- src/bridge/windows.rs | 2 ++ src/context/mod.rs | 15 +++++++++++++++ src/context/windows.rs | 23 +++++++++++++++++++++++ src/engine.rs | 3 +++ src/res/win/espansored.ico | Bin 0 -> 30894 bytes 7 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/res/win/espansored.ico diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index 01024c1..4aa279f 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -62,6 +62,7 @@ HWND nw = NULL; HWND hwnd_st_u = NULL; HBITMAP g_espanso_bmp = NULL; HICON g_espanso_ico = NULL; +HICON g_espanso_red_ico = NULL; NOTIFYICONDATA nid = {}; UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); @@ -309,13 +310,14 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR } } -int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path, int32_t _show_icon) { +int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t _show_icon) { manager_instance = self; show_icon = _show_icon; // Load the images g_espanso_bmp = (HBITMAP)LoadImage(NULL, bmp_path, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); g_espanso_ico = (HICON)LoadImage(NULL, ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE); + g_espanso_red_ico = (HICON)LoadImage(NULL, red_ico_path, IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR | LR_SHARED | LR_DEFAULTSIZE | LR_LOADFROMFILE); // Make the notification capable of handling different screen definitions SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); @@ -472,6 +474,19 @@ void eventloop() { // Something went wrong, this should have been an infinite loop. } +void update_tray_icon(int32_t enabled) { + if (enabled) { + nid.hIcon = g_espanso_ico; + }else{ + nid.hIcon = g_espanso_red_ico; + } + + // Update the icon + if (show_icon) { + Shell_NotifyIcon(NIM_MODIFY, &nid); + } +} + /* * Type the given string simulating keyboard presses. */ diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h index f0ef9eb..7db802a 100644 --- a/native/libwinbridge/bridge.h +++ b/native/libwinbridge/bridge.h @@ -33,7 +33,7 @@ extern void * manager_instance; * Initialize the Windows parameters * return: 1 if OK, -1 otherwise. */ -extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * bmp_path, int32_t show_icon); +extern "C" int32_t initialize(void * self, wchar_t * ico_path, wchar_t * red_ico_path, wchar_t * bmp_path, int32_t show_icon); #define LEFT_VARIANT 1 #define RIGHT_VARIANT 2 @@ -152,6 +152,11 @@ extern "C" int32_t show_notification(wchar_t * message); */ extern "C" void close_notification(); +/* + * Update the tray icon status + */ +extern "C" void update_tray_icon(int32_t enabled); + // CLIPBOARD /* diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs index 35cec74..dd7ed32 100644 --- a/src/bridge/windows.rs +++ b/src/bridge/windows.rs @@ -33,6 +33,7 @@ extern "C" { pub fn initialize( s: *const c_void, ico_path: *const u16, + red_ico_path: *const u16, bmp_path: *const u16, show_icon: i32, ) -> i32; @@ -48,6 +49,7 @@ extern "C" { pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void)); pub fn register_context_menu_click_callback(cb: extern "C" fn(_self: *mut c_void, id: i32)); pub fn cleanup_ui(); + pub fn update_tray_icon(enabled: i32); // CLIPBOARD pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32; diff --git a/src/context/mod.rs b/src/context/mod.rs index 50805fb..cd3a5aa 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -48,6 +48,11 @@ pub fn new( macos::MacContext::new(config, send_channel, is_injecting) } +#[cfg(target_os = "macos")] +pub fn update_icon(enabled: bool) { + // TODO: add update icon on macOS +} + // LINUX IMPLEMENTATION #[cfg(target_os = "linux")] pub fn new( @@ -58,6 +63,11 @@ pub fn new( linux::LinuxContext::new(config, send_channel, is_injecting) } +#[cfg(target_os = "linux")] +pub fn update_icon(enabled: bool) { + // No icon on Linux +} + // WINDOWS IMPLEMENTATION #[cfg(target_os = "windows")] pub fn new( @@ -68,6 +78,11 @@ pub fn new( windows::WindowsContext::new(config, send_channel, is_injecting) } +#[cfg(target_os = "windows")] +pub fn update_icon(enabled: bool) { + windows::update_icon(enabled); +} + // espanso directories static WARING_INIT: Once = Once::new(); diff --git a/src/context/windows.rs b/src/context/windows.rs index 07eeffa..e1cf95a 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -32,6 +32,7 @@ use widestring::{U16CStr, U16CString}; const BMP_BINARY: &[u8] = include_bytes!("../res/win/espanso.bmp"); const ICO_BINARY: &[u8] = include_bytes!("../res/win/espanso.ico"); +const RED_ICO_BINARY: &[u8] = include_bytes!("../res/win/espansored.ico"); pub struct WindowsContext { send_channel: Sender, @@ -77,8 +78,22 @@ impl WindowsContext { ); } + let espanso_red_ico_image = espanso_dir.join("espansored.ico"); + if espanso_red_ico_image.exists() { + info!("red ICO already initialized, skipping."); + } else { + fs::write(&espanso_red_ico_image, RED_ICO_BINARY) + .expect("Unable to write windows ico file"); + + info!( + "Extracted 'red ico' icon to: {}", + espanso_red_ico_image.to_str().unwrap_or("error") + ); + } + let bmp_icon = espanso_bmp_image.to_str().unwrap_or_default(); let ico_icon = espanso_ico_image.to_str().unwrap_or_default(); + let red_ico_icon = espanso_red_ico_image.to_str().unwrap_or_default(); let send_channel = send_channel; @@ -96,6 +111,7 @@ impl WindowsContext { register_context_menu_click_callback(context_menu_click_callback); let ico_file_c = U16CString::from_str(ico_icon).unwrap(); + let red_ico_file_c = U16CString::from_str(red_ico_icon).unwrap(); let bmp_file_c = U16CString::from_str(bmp_icon).unwrap(); let show_icon = if config.show_icon { 1 } else { 0 }; @@ -104,6 +120,7 @@ impl WindowsContext { let res = initialize( context_ptr, ico_file_c.as_ptr(), + red_ico_file_c.as_ptr(), bmp_file_c.as_ptr(), show_icon, ); @@ -126,6 +143,12 @@ impl super::Context for WindowsContext { // Native bridge code +pub fn update_icon(enabled: bool) { + unsafe { + crate::bridge::windows::update_tray_icon(if enabled { 1 } else { 0 }); + } +} + extern "C" fn keypress_callback( _self: *mut c_void, raw_buffer: *const u16, diff --git a/src/engine.rs b/src/engine.rs index 2042fcd..3e2a534 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -377,6 +377,9 @@ impl< if config.show_notifications { self.ui_manager.notify(message); } + + // Update the icon on supported OSes. + crate::context::update_icon(status); } fn on_passive(&self) { diff --git a/src/res/win/espansored.ico b/src/res/win/espansored.ico new file mode 100644 index 0000000000000000000000000000000000000000..334bab0c919b7400b41ce065205d2aa37cba5a6d GIT binary patch literal 30894 zcmeI536vGpna7L4gp4LflbqztoSdU%Ml-H}D=r|YC}S3vaR@r@88@PF6vgGJ1B$_Y zCu7u^am5%l92Y!lR6q@m3r0QCJKelqXqIkhXrQ~Hfjj@-eRUu8>eYL%UcJ|7JbCB* z`@40Q@B4rE-nw<`)~#yK8{`f4h7R>4hkDQK;(3R7o;PBIf4qn1H7RcF*!1z9M|<83 zW!O`BRK!!XEhpi5%JcWrh!{lC3H;VtADqwqF_}h8N_EPIO}<%YBZ0{8VzakmHBbJz>IP zbK7n0=K1GWnO9%!GB3Tf+C22o3UkTDDRac(HR6?9HlPpP*s%5-3ICXL_+25=Ho6~v zc!gQDYLkijZQQuUELhNOPWhL`#W-LCTh=_%$N7@|LXQ1|4y-m$JkepcY}p!RIxBAO z+_mPAgKKhh1oUD9Th1=>Tf$pHPC43k(nTq=e*NYwJDWFeHLb0ErlzLHtXsD&t+BD! z95uF9{UTRaKtHyy={Ov!a91Ebi^uHQtFmlU4^KYXVa6X*{*Ukw?~=efQx!sJ8w8yz`crtFCIX z`HT4Oe8Z-*KQo{GapUStb8}yo{SQ7^XHGk9u{rUC1~Ya{tvO)-YO~MYRhCao+i<`p z_P-{~796jPMAxyh-_nw8zrTO0ZS&5~e$&v(PXZE)3)=ntE=B!dTDcO84rJ#&pzkhBaf`ivQJ+h zKfd0!`Jp3gZ2bo_EL^xgPCvw^9g8JzDP;fr^Ou;Oo-G*@)b&j_Ew%k^@4c$bJ@+ij zq!C!Nrr-QrvD5v%s|oTiA5= zcTo6IY!$-ox3@>y3)_F?l`ccSEuafq*bH8Sj?!`7F_?B>oMDWsuHIl6|HD7dORj+@ z{H&qSH4q!va+hCM2h7nP77FpUZE^HbwPwnc7W4RH9p>eiyUd(9UG{n~Mg4*CqCmZ) z8yh+ELB+v?Hu+2_$bq_G4n)5o3ftzr+Y0C?q`y!aI?-+I zgzarhK3S(2A)F(yzF~j6m$y!lR`S*QOMmjQqvQ_-`r1U{ zBH@=l2opyddB~gS+^a zh1Z0uiLIWX5|uJ2G}?p;hj9N-em2S4BUCI2WC)166&E9*wA zosJ$=W7kZ6e0;5yqes`+do0q26~1@Q=nL6~3!H+dXHJ-O;co=yZL!W(caL+z&+5$` zw=XqsywRogmL9wAwqnIbE9)D2%)EJP%zgK@n+gBgpnIXB_d{Vn-~uPOwU7$0{7TPVbk`^5e-Tv)8X2 zw>Yw|Z;RHSm)LvjDBXGD;0DL6Ht>(>TMr6Z9HWl8pTF>eB{^%~VcuGHF4#7=-PY!x z^Tlljc)&61JkT-z3>E5yqWoE}VQn_wkM){&-dU@)yjFAhWhrylo$XrB?a9hco-40t zF17yQ21mGN&SS#D1Ywg#u#Y27c0s^;(}zuh;WD z;PuzL>@$J{*9JJk)$&LBxLdLychdLM7=E#yN%i+{TQ?2*v1CcF88@yrXPl?coqblL znLWGH%$U(;uMyNm9Gek&;OclI|5o6hqNqNuK{wvm8l;>Rh9C9KbqWOQ7rrsq!;0~M zE1aDVf3I*&T>kgoo4qa_bl{a&R-1FrS!_-}o#!a^ndd0!Yp<@)h1cD@t-`oIq$g6xY*mf**`eyqkvp7TWgc;>@14#psSJ?7|I)pciNd-dyC#Z5Q0CAo&d z70!5)nfAiT4#lscOjz8YQhZt{mt4C6}a1?L%;db8rsvA94AUk2-z)@%-~= z&I87&v(Iicvu1T@z7c&66VE=060Zqx4bR^WR$OOXA6y%xQoXJ!bL6(MG$$z&!8L(B z4<+0e!x64w{yQm-waC2vCY(l{U9IPy;XN=Jaxnr`RU7R29h@hH?Sziu2-h%w&V>tv zsI}v~K5;L8%PmV2`M|SY%9hY~;08x~-ew$7z>i~2v{1;)Hz!T=F?_f}eVw^Z+#h}G z*=IXSy?(*X&f9|A2`k=#XAY73wQ{%z{G<-qxAedR%e1d$gZ3&ghsgD&w|9%7&rF}* zRw{Qm!7ZomoiBdccO*+X50u}w7w0s79I5px#+@6dwVM0yUv3_JaJiW|bGiBTuiMPO zYkv&uS>!9B&!ZnsaC3AcCFug|EK7y>{V0$ZUhv~O=dJ-xCg2SRxY)IyNYa5w<%2)Z zNiP;Q2#I{CU^zIzCHAvh<;4fqiEb52J{MG`Qa)oF4moR9fhvy-sti7SOWdR zREo3HgH3G1A(CUUNXiM{6mAr{1*dzUCAP4M?P4?wrc++{x^RxLSQu#T*uWMx3v!E_ zh#nuqB%c=63zhZ*o#@7fwHKFPoP18FLvJL9 zR6I7Z4TtSEKKQnFkUT+HC~Of1%Ew}Qu?+{fz$v=HDin);VJG2Qp;IWvZ+oS~1x|3Q zkhj@t&?|7w=H9AL*j~QHwF4)(!7-bBnMdgJ_5QEq7uny7)dk$(Sg|^gE`0xvulLLB zyQ>>l2XHQ{<$d4T&vK_w$?Lh}I#3d>aJGAKl2n4q((Z!<=3YHQ*<5)J!P-6Vpzgib zq72-g0MaXA$JYU8xZ^`fRX~0pM@Y5`iFxt-6Mxx1Jnk4he>rQhnXG*>*I(akrcOq@Qh6s4n1+57wW!mEh;+{CQPKVkfe)PA5GKExgil-ITW z6$uo3q#D)?3>Q&(Yb}Sjvz3;928USEX!!DDin*m?u6=c77G>1Ae|AN!AHr zT}6-a{iw0LO3CTn|*T~9o*qEr`)=AS#aOTL%wKR)4G zWFPQ-r+zLKN?EJ0?LzyUcV3gev(aDFpHef_H>}sQ`Sg1;;+C`@;1j+@_5nYhb^SMm z(rxTtW!`)gx zC6wepdk-e+`n`5-@wR7w%Zo2|no~}x*ENFg(JV3t9av>1OlUA~zugtNx8$j(R+h4t zrx161!^bTDIrcF{vQYWRG_KRP-`*ChTR;DY4(Z7L_Klq@NC*2V7rd9f|MLC!*V=ve z6`BvhA0M;M0sok_q}f8GkA>oC|2cEAzj+i@8B3S;nI9itr?!l?!5DJVN%aPwKl!BF zJTSA}UQ5|iSjc{fY5176Zl+`E;|D^OP?9a$j{f_>huPoUiSnO*PhAzVK|3>d0@OzZ z+uhm4M|{nk1B8Wxgp$vF{2w){+SJx%f72$)|C?`aHGAy7$ohqE)J2qk=mg?35Wq)# z4f}tx;!5661OH==&i&?8*nh_SXP)WQx0agBq)CnDtTP+UiF)^E?3fxeLgN8r2#BhW z3fBWZ;%nG{?iEYE?#K0?`k)T_H*d*&hloApjg38K{@ZKJ!;dUC7hTk3>w~@lD(tt@ zhmZIg_WwVV*rT5^H>r!!|HFMEe9ZTmnwomd)4yA3&OC$f@+E$+*7=%G;%nIdsY(6k zowh&!d0p6@vW}TcJo{XSIsTvP2BI(EYeoF0&SQDT+01$eZvNGj^6vK z@cB?t|KawW+N#*^jW<^5{nW);_h~hc{P%Lb)3Y|#{X%PNuig=>XdP74|9!N+HDgBc zcgJ|QbFV#F_e-z;F>g5Y%*Fa9UsvRNK*9R_?6dWDO{hZGhbaFqOLG2GhW0=A+(!F; zS&(x$Tvwm{P61;l*F5e6UVN$e{E>GB$)A*Gpd7x2&;N;vOSIlao3OW>YgwTm?}VKG zZ@K-L`eWXk?^~@-XLYsSsnR_{nSB5s@ipxK5sF(YB&uKLa?d^2>8R$Fzy0lUe?54c z9s0qcBdbk~{~mO{!mKTvc3MNxIdl;p@DX3b{{K*M4Moc4tsB~K(xfGN$2I!>Y3CDT zS^6GmTR)~fS??|M&JpEV7o9Stx!iLAAMrKp|Bj0LV^aTV!;xykg<8`n_Ivf!)%r$e z`ndr0_D?@rWNx^jCGWfE+w^TU)Aa5}nPUJx;%nG{?tyMj>ObY+#q$j2xP^YK|1Vgu z)?9NM!f^Q)T~??{mx%d5A%lWuP@twe9XH4ry~5g@sjKDB?)XF&|DDT;{52( zoG)?bL%ctJ!3B-w%!Tj`A2V(50>la57d{da`A<3eGh=+A??A+7`4G&WnoG5AU(^ zZ4dfY8NBeR;2tTZyK`Wq!e#dXxN=PxKfYG)L@&2vd?`;t85da(m@*}0->XJnLf{KN z#n8x&RF`p^<{Fwmx68Y{6{J;*j@Nk$nvF}W9ota!2d@$Uf)pTdlq~%%)?|9<_z64z?7$&ceQzg3vr>i28c>;cMKD&G2^nRH18YpSx zseoO$<3rJYAFdF;%n-@Dg};P-U*Y|U&6$_dODUJ%0izwF0wj-2bna$cEK z-hsUnfBFlm1Gw7#9{5nskMchDm3&Y5vbQ@N;c9u8 zgC&m^*e~-%^aX5U8xC*@QXKFw_I!L#lJ5>N&)O^uR69Gp*uWMxvAtc|JYR+S_Ss|1 zxyL@s4+Y*uaC|Ez(TQ$sV5{VP&G~$&M7;R8{=l5$6ybk_T495bv<@hX4s@Z@&SxYm zL3zM*z)#>^McU~n!VLoB^ZyC;!g7Iq7(oz68hOY|8OoyLOZnbooC*|o*DrQZco$)) zFic=A<^Ugri6f0X2- z=?@p!YnL?_5 ltzy-;RkWKP3KBnNTl=Ix7@=cjR5qJo4AT7v9~;j6`G1Fkt&#u$ literal 0 HcmV?d00001 From 4ad96eb2f351bd17e6bf479d4f82297339fd24c5 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 16 Aug 2020 17:25:48 +0200 Subject: [PATCH 25/28] Update packager to include modulo on Windows --- Cargo.toml | 3 +++ packager.py | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aea476f..10ea57f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,9 @@ homepage = "https://github.com/federico-terzi/espanso" edition = "2018" build="build.rs" +[modulo] +version = "0.1.0" + [dependencies] widestring = "0.4.0" serde = { version = "1.0", features = ["derive"] } diff --git a/packager.py b/packager.py index 1f2c371..af3fc54 100644 --- a/packager.py +++ b/packager.py @@ -20,6 +20,7 @@ class PackageInfo: description: str publisher: str url: str + modulo_version: str @click.group() def cli(): @@ -44,7 +45,8 @@ def build(skipcargo): cargo_info["package"]["version"], cargo_info["package"]["description"], cargo_info["package"]["authors"][0], - cargo_info["package"]["homepage"]) + cargo_info["package"]["homepage"], + cargo_info["modulo"]["version"]) print(package_info) if not skipcargo: @@ -58,6 +60,11 @@ def build(skipcargo): elif TARGET_OS == "macos": build_mac(package_info) +def calculate_sha256(file): + with open(file, "rb") as f: + b = f.read() # read entire file as bytes + readable_hash = hashlib.sha256(b).hexdigest() + return readable_hash def build_windows(package_info): print("Starting packaging process for Windows...") @@ -78,6 +85,22 @@ def build_windows(package_info): TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "win") os.makedirs(TARGET_DIR, exist_ok=True) + modulo_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe".format(package_info.modulo_version) + modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe.sha256.txt".format(package_info.modulo_version) + print("Pulling modulo depencency from:", modulo_url) + modulo_target_file = os.path.join(TARGET_DIR, "modulo.exe") + urllib.request.urlretrieve(modulo_url, modulo_target_file) + print("Pulling SHA signature from:", modulo_sha_url) + modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256") + urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file) + print("Checking signatures...") + expected_sha = None + with open(modulo_sha_file, "r") as sha_f: + expected_sha = sha_f.read() + actual_sha = calculate_sha256(modulo_target_file) + if actual_sha != expected_sha: + raise Exception("Modulo SHA256 is not matching") + print("Gathering CRT DLLs...") msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*") print("Found Redists: ", msvc_dirs) @@ -104,12 +127,15 @@ def build_windows(package_info): dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll") print("Found DLLs:") - dll_include_list = [] + include_list = [] for dll in dll_files: print("Including: "+dll) - dll_include_list.append("Source: \""+dll+"\"; DestDir: \"{app}\"; Flags: ignoreversion") + include_list.append("Source: \""+dll+"\"; DestDir: \"{app}\"; Flags: ignoreversion") - dll_include = "\r\n".join(dll_include_list) + print("Including modulo") + include_list.append("Source: \""+modulo_target_file+"\"; DestDir: \"{app}\"; Flags: ignoreversion") + + include = "\r\n".join(include_list) INSTALLER_NAME = f"espanso-win-installer" @@ -130,7 +156,7 @@ def build_windows(package_info): content = content.replace("{{{executable_path}}}", os.path.abspath("target/release/espanso.exe")) content = content.replace("{{{output_dir}}}", os.path.abspath(TARGET_DIR)) content = content.replace("{{{output_name}}}", INSTALLER_NAME) - content = content.replace("{{{dll_include}}}", dll_include) + content = content.replace("{{{dll_include}}}", include) with open(os.path.join(TARGET_DIR, "setupscript.iss"), "w") as output_script: output_script.write(content) From 510a886b080fdb1172c6922a6ad85c9d0b0a84f5 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 16 Aug 2020 18:56:07 +0200 Subject: [PATCH 26/28] Fix console showing on Windows when using Form extension --- native/libwinbridge/bridge.cpp | 2 +- packager.py | 2 +- src/extension/script.rs | 2 +- src/extension/shell.rs | 2 +- src/extension/utils.rs | 14 -------------- src/process.rs | 2 +- src/ui/modulo/form.rs | 1 - src/ui/modulo/mod.rs | 13 +++++++------ src/utils.rs | 14 ++++++++++++++ 9 files changed, 26 insertions(+), 26 deletions(-) delete mode 100644 src/ui/modulo/form.rs diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index 4aa279f..55d9567 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -745,7 +745,7 @@ int32_t start_daemon_process() { NULL, NULL, FALSE, - DETACHED_PROCESS, + DETACHED_PROCESS | CREATE_NO_WINDOW, NULL, NULL, &si, diff --git a/packager.py b/packager.py index af3fc54..fa34dd0 100644 --- a/packager.py +++ b/packager.py @@ -133,7 +133,7 @@ def build_windows(package_info): include_list.append("Source: \""+dll+"\"; DestDir: \"{app}\"; Flags: ignoreversion") print("Including modulo") - include_list.append("Source: \""+modulo_target_file+"\"; DestDir: \"{app}\"; Flags: ignoreversion") + include_list.append("Source: \""+os.path.abspath(modulo_target_file)+"\"; DestDir: \"{app}\"; Flags: ignoreversion") include = "\r\n".join(include_list) diff --git a/src/extension/script.rs b/src/extension/script.rs index 7261ed2..3a68a10 100644 --- a/src/extension/script.rs +++ b/src/extension/script.rs @@ -102,7 +102,7 @@ impl super::Extension for ScriptExtension { let mut command = Command::new(&str_args[0]); // Set the OS-specific flags - super::utils::set_command_flags(&mut command); + crate::utils::set_command_flags(&mut command); // Inject the $CONFIG variable command.env("CONFIG", crate::context::get_config_dir()); diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 3beac0b..fd0f43a 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -81,7 +81,7 @@ impl Shell { }; // Set the OS-specific flags - super::utils::set_command_flags(&mut command); + crate::utils::set_command_flags(&mut command); // Inject the $CONFIG variable command.env("CONFIG", crate::context::get_config_dir()); diff --git a/src/extension/utils.rs b/src/extension/utils.rs index 0844cc4..fcf5bac 100644 --- a/src/extension/utils.rs +++ b/src/extension/utils.rs @@ -1,6 +1,5 @@ use crate::extension::ExtensionResult; use std::collections::HashMap; -use std::process::Command; pub fn convert_to_env_variables( original_vars: &HashMap, @@ -25,19 +24,6 @@ pub fn convert_to_env_variables( output } -#[cfg(target_os = "windows")] -pub fn set_command_flags(command: &mut Command) { - use std::os::windows::process::CommandExt; - // Avoid showing the shell window - // See: https://github.com/federico-terzi/espanso/issues/249 - command.creation_flags(0x08000000); -} - -#[cfg(not(target_os = "windows"))] -pub fn set_command_flags(command: &mut Command) { - // NOOP on Linux and macOS -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/process.rs b/src/process.rs index 7a03222..660abb0 100644 --- a/src/process.rs +++ b/src/process.rs @@ -25,7 +25,7 @@ use std::process::{Child, Command, Stdio}; pub fn spawn_process(cmd: &str, args: &Vec) -> io::Result { use std::os::windows::process::CommandExt; Command::new(cmd) - .creation_flags(0x00000008) // Detached Process + .creation_flags(0x08000008) // Detached Process without window .args(args) .spawn() } diff --git a/src/ui/modulo/form.rs b/src/ui/modulo/form.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/ui/modulo/form.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/ui/modulo/mod.rs b/src/ui/modulo/mod.rs index 0bf3543..5f306ac 100644 --- a/src/ui/modulo/mod.rs +++ b/src/ui/modulo/mod.rs @@ -3,8 +3,6 @@ use log::{error, info}; use std::io::{Error, Write}; use std::process::{Child, Command, Output}; -pub mod form; - pub struct ModuloManager { modulo_path: Option, } @@ -72,12 +70,15 @@ impl ModuloManager { } if let Some(ref modulo_path) = self.modulo_path { - let child = Command::new(modulo_path) - .args(args) + let mut command = Command::new(modulo_path); + command.args(args) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn(); + .stderr(std::process::Stdio::piped()); + + crate::utils::set_command_flags(&mut command); + + let child = command.spawn(); match child { Ok(mut child) => { diff --git a/src/utils.rs b/src/utils.rs index e619c8c..bea9fe2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,6 +20,7 @@ use std::error::Error; use std::fs::create_dir; use std::path::Path; +use std::process::Command; pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box> { for entry in std::fs::read_dir(source_dir)? { @@ -40,6 +41,19 @@ pub fn copy_dir(source_dir: &Path, dest_dir: &Path) -> Result<(), Box Ok(()) } +#[cfg(target_os = "windows")] +pub fn set_command_flags(command: &mut Command) { + use std::os::windows::process::CommandExt; + // Avoid showing the shell window + // See: https://github.com/federico-terzi/espanso/issues/249 + command.creation_flags(0x08000000); +} + +#[cfg(not(target_os = "windows"))] +pub fn set_command_flags(command: &mut Command) { + // NOOP on Linux and macOS +} + #[cfg(test)] mod tests { use super::*; From b2bfb9737f87b2430582e68b673e2ad7e4afe3ad Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 16 Aug 2020 19:06:19 +0200 Subject: [PATCH 27/28] Include modulo in macOS packaging --- packager.py | 12 ++++++++++++ packager/mac/espanso.rb | 11 +++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packager.py b/packager.py index af3fc54..5a66bbd 100644 --- a/packager.py +++ b/packager.py @@ -211,6 +211,16 @@ def build_mac(package_info): with open(hash_file, "w") as hf: hf.write(sha256_hash.hexdigest()) + modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-mac.sha256.txt".format(package_info.modulo_version) + print("Pulling SHA signature from:", modulo_sha_url) + modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256") + urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file) + modulo_sha = None + with open(modulo_sha_file, "r") as sha_f: + modulo_sha = sha_f.read() + if modulo_sha is None: + raise Exception("Cannot determine modulo SHA") + print("Processing Homebrew formula template") with open("packager/mac/espanso.rb", "r") as formula_template: content = formula_template.read() @@ -219,6 +229,8 @@ def build_mac(package_info): content = content.replace("{{{app_desc}}}", package_info.description) content = content.replace("{{{app_url}}}", package_info.url) content = content.replace("{{{app_version}}}", package_info.version) + content = content.replace("{{{modulo_version}}}", package_info.modulo_version) + content = content.replace("{{{modulo_sha}}}", modulo_sha) # Calculate hash with open(archive_target, "rb") as f: diff --git a/packager/mac/espanso.rb b/packager/mac/espanso.rb index 6e7f477..399f151 100644 --- a/packager/mac/espanso.rb +++ b/packager/mac/espanso.rb @@ -4,11 +4,18 @@ class Espanso < Formula desc "{{{app_desc}}}" homepage "{{{app_url}}}" - url "https://github.com/federico-terzi/espanso/releases/latest/download/espanso-mac.tar.gz" + url "https://github.com/federico-terzi/espanso/releases/v{{{app_version}}}/download/espanso-mac.tar.gz" sha256 "{{{release_hash}}}" version "{{{app_version}}}" + resource "modulo" do + url "https://github.com/federico-terzi/modulo/releases/download/v{{{modulo_version}}}/modulo-mac" + sha256 "{{{modulo_sha}}}" + end + def install bin.install "espanso" - end + + resource("modulo").stage { bin.install "modulo-mac" => "modulo" } + end end \ No newline at end of file From 695c44c6913f4abebfce4e554454858871259350 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 16 Aug 2020 20:22:58 +0200 Subject: [PATCH 28/28] Add icon and title to modulo forms --- src/context/mod.rs | 15 +++++++++++++++ src/context/windows.rs | 7 ++++++- src/extension/form.rs | 5 +++++ src/ui/modulo/mod.rs | 2 +- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/context/mod.rs b/src/context/mod.rs index cd3a5aa..ea656fe 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -53,6 +53,11 @@ pub fn update_icon(enabled: bool) { // TODO: add update icon on macOS } +#[cfg(target_os = "macos")] +pub fn get_icon_path() -> Option { + None +} + // LINUX IMPLEMENTATION #[cfg(target_os = "linux")] pub fn new( @@ -68,6 +73,11 @@ pub fn update_icon(enabled: bool) { // No icon on Linux } +#[cfg(target_os = "linux")] +pub fn get_icon_path() -> Option { + None +} + // WINDOWS IMPLEMENTATION #[cfg(target_os = "windows")] pub fn new( @@ -83,6 +93,11 @@ pub fn update_icon(enabled: bool) { windows::update_icon(enabled); } +#[cfg(target_os = "windows")] +pub fn get_icon_path() -> Option { + Some(windows::get_icon_path(&get_data_dir())) +} + // espanso directories static WARING_INIT: Once = Once::new(); diff --git a/src/context/windows.rs b/src/context/windows.rs index e1cf95a..6928850 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -28,6 +28,7 @@ use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering::Acquire; use std::sync::mpsc::Sender; use std::sync::Arc; +use std::path::{Path, PathBuf}; use widestring::{U16CStr, U16CString}; const BMP_BINARY: &[u8] = include_bytes!("../res/win/espanso.bmp"); @@ -66,7 +67,7 @@ impl WindowsContext { ); } - let espanso_ico_image = espanso_dir.join("espanso.ico"); + let espanso_ico_image = get_icon_path(&espanso_dir); if espanso_ico_image.exists() { info!("ICO already initialized, skipping."); } else { @@ -141,6 +142,10 @@ impl super::Context for WindowsContext { } } +pub fn get_icon_path(espanso_dir: &Path) -> PathBuf { + espanso_dir.join("espanso.ico") +} + // Native bridge code pub fn update_icon(enabled: bool) { diff --git a/src/extension/form.rs b/src/extension/form.rs index 5b9abcd..79133f6 100644 --- a/src/extension/form.rs +++ b/src/extension/form.rs @@ -53,12 +53,17 @@ impl super::Extension for FormExtension { }; let mut form_config = Mapping::new(); + form_config.insert(Value::from("title"), Value::from("espanso")); form_config.insert(Value::from("layout"), Value::from(layout)); if let Some(fields) = params.get(&Value::from("fields")) { form_config.insert(Value::from("fields"), fields.clone()); } + if let Some(icon_path) = crate::context::get_icon_path() { + form_config.insert(Value::from("icon"), Value::from(icon_path.to_string_lossy().to_string())); + } + let serialized_config: String = serde_yaml::to_string(&form_config).expect("unable to serialize form config"); diff --git a/src/ui/modulo/mod.rs b/src/ui/modulo/mod.rs index 5f306ac..008956d 100644 --- a/src/ui/modulo/mod.rs +++ b/src/ui/modulo/mod.rs @@ -104,7 +104,7 @@ impl ModuloManager { } } Err(error) => { - error!("error while sending body to modulo"); + error!("error while sending body to modulo: {}", error); } } } else {