diff --git a/Cargo.lock b/Cargo.lock index 8302dbe..84de321 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.4.0" +version = "0.4.1" dependencies = [ "backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -384,6 +384,7 @@ dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log-panics 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.20 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1003,7 +1004,7 @@ dependencies = [ [[package]] name = "rand" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1410,7 +1411,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)", - "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "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)", @@ -1892,7 +1893,7 @@ dependencies = [ "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" "checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -"checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c" +"checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" "checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" diff --git a/Cargo.toml b/Cargo.toml index 985ca3d..fccfc68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.4.0" +version = "0.4.1" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" @@ -29,6 +29,7 @@ reqwest = "0.9.20" git2 = {version = "0.10.1", features = ["https"]} tempfile = "3.1.0" dialoguer = "0.4.0" +rand = "0.7.2" [target.'cfg(unix)'.dependencies] libc = "0.2.62" diff --git a/README.md b/README.md index df78593..976fbde 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ ___ * Works on **Windows**, **macOS** and **Linux** * Works with almost **any program** * Works with **Emojis** 😄 +* Works with **Images** * **Date** expansion support * **Custom scripts** support * **Shell commands** support @@ -54,7 +55,8 @@ espanso is a free, open source software developed in my (little) spare time. If you liked the project and would like to support further development, please consider making a small donation, it really helps :) -[![Donate with PayPal](images/donate.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FHNLR5DRS267E&source=url) +[![Donate with PayPal](images/githubsponsor.png)](https://github.com/sponsors/federico-terzi) +[![Donate with PayPal](images/paypal.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FHNLR5DRS267E&source=url) ## Contributors @@ -65,6 +67,7 @@ Many people helped the project along the way, thanks to all of you. In particula * [Matteo Pellegrino](https://www.matteopellegrino.me/) - MacOS Tester * [Timo Runge](http://timorunge.com/) - MacOS contributor * [NickSeagull](http://nickseagull.github.io/) - Contributor +* [matt-h](https://github.com/matt-h) - Contributor ## Remarks diff --git a/images/githubsponsor.png b/images/githubsponsor.png new file mode 100644 index 0000000..2a4d314 Binary files /dev/null and b/images/githubsponsor.png differ diff --git a/images/paypal.png b/images/paypal.png new file mode 100644 index 0000000..23e9e9f Binary files /dev/null and b/images/paypal.png differ diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 5695fa3..a34661a 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -303,6 +303,10 @@ void trigger_shift_ins_paste() { xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Insert", 8000); } +void trigger_alt_shift_ins_paste() { + xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Alt+Insert", 8000); +} + // SYSTEM MODULE // Function taken from the wmlib tool source code @@ -449,7 +453,7 @@ int32_t get_active_window_executable(char *buffer, int32_t size) { return result; } -int32_t is_current_window_terminal() { +int32_t is_current_window_special() { char class_buffer[250]; int res = get_active_window_class(class_buffer, 250); if (res > 0) { @@ -465,10 +469,16 @@ int32_t is_current_window_terminal() { return 1; }else if (strstr(class_buffer, "Terminator") != NULL) { // Terminator return 1; + }else if (strstr(class_buffer, "stterm") != NULL) { // Simple terminal 3 + return 2; }else if (strstr(class_buffer, "St") != NULL) { // Simple terminal return 1; + }else if (strstr(class_buffer, "st") != NULL) { // Simple terminal 2 + return 1; }else if (strstr(class_buffer, "Alacritty") != NULL) { // Alacritty terminal return 1; + }else if (strstr(class_buffer, "Emacs") != NULL) { // Emacs + return 3; } } diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h index 9d07122..6e921bf 100644 --- a/native/liblinuxbridge/bridge.h +++ b/native/liblinuxbridge/bridge.h @@ -87,6 +87,11 @@ extern "C" void trigger_terminal_paste(); */ extern "C" void trigger_shift_ins_paste(); +/* + * Trigger alt shift ins pasting( Pressing ALT+SHIFT+INS ) + */ +extern "C" void trigger_alt_shift_ins_paste(); + // SYSTEM MODULE @@ -106,8 +111,8 @@ extern "C" int32_t get_active_window_class(char * buffer, int32_t size); extern "C" int32_t get_active_window_executable(char * buffer, int32_t size); /* - * Return 1 if the current window is a terminal window, 0 otherwise. + * Return a value greater than 0 if the current window needs a special paste combination, 0 otherwise. */ -extern "C" int32_t is_current_window_terminal(); +extern "C" int32_t is_current_window_special(); #endif //ESPANSO_BRIDGE_H diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs index 4947629..90edfc6 100644 --- a/src/bridge/linux.rs +++ b/src/bridge/linux.rs @@ -31,7 +31,7 @@ extern { pub fn get_active_window_name(buffer: *mut c_char, size: i32) -> i32; pub fn get_active_window_class(buffer: *mut c_char, size: i32) -> i32; pub fn get_active_window_executable(buffer: *mut c_char, size: i32) -> i32; - pub fn is_current_window_terminal() -> i32; + pub fn is_current_window_special() -> i32; // Keyboard pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const u8, @@ -43,4 +43,5 @@ extern { pub fn trigger_paste(); pub fn trigger_terminal_paste(); pub fn trigger_shift_ins_paste(); + pub fn trigger_alt_shift_ins_paste(); } \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs index 64463d4..c849527 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -53,6 +53,7 @@ fn default_use_system_agent() -> bool { true } fn default_config_caching_interval() -> i32 { 800 } fn default_word_separators() -> Vec { vec![' ', ',', '.', '\r', '\n', 22u8 as char] } fn default_toggle_interval() -> u32 { 230 } +fn default_preserve_clipboard() -> bool {false} fn default_backspace_limit() -> i32 { 3 } fn default_exclude_default_matches() -> bool {false} fn default_matches() -> Vec { Vec::new() } @@ -98,6 +99,9 @@ pub struct Configs { #[serde(default = "default_toggle_interval")] pub toggle_interval: u32, + #[serde(default = "default_preserve_clipboard")] + pub preserve_clipboard: bool, + #[serde(default)] pub paste_shortcut: PasteShortcut, @@ -145,6 +149,7 @@ impl Configs { validate_field!(result, self.backspace_limit, default_backspace_limit()); validate_field!(result, self.ipc_server_port, default_ipc_server_port()); validate_field!(result, self.use_system_agent, default_use_system_agent()); + validate_field!(result, self.preserve_clipboard, default_preserve_clipboard()); result } diff --git a/src/engine.rs b/src/engine.rs index 79f5e3d..8f49a5c 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -95,6 +95,19 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa menu } + + fn return_content_if_preserve_clipboard_is_enabled(&self) -> Option { + // If the preserve_clipboard option is enabled, first save the current + // clipboard content in order to restore it later. + if self.config_manager.default_config().preserve_clipboard { + match self.clipboard_manager.get_clipboard() { + Some(clipboard) => {Some(clipboard)}, + None => {None}, + } + }else { + None + } + } } lazy_static! { @@ -119,6 +132,8 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.keyboard_manager.delete_string(char_count); + let mut previous_clipboard_content : Option = None; + // Manage the different types of matches match &m.content { // Text Match @@ -205,6 +220,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa } }, 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.paste_shortcut); }, @@ -220,6 +239,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa MatchContentType::Image(content) => { // Make sure the image exist beforehand if content.path.exists() { + // 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_image(&content.path); self.keyboard_manager.trigger_paste(&config.paste_shortcut); }else{ @@ -227,6 +250,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa } }, } + + // Restore previous clipboard content + if let Some(previous_clipboard_content) = previous_clipboard_content { + self.clipboard_manager.set_clipboard(&previous_clipboard_content); + } } fn on_enable_update(&self, status: bool) { diff --git a/src/extension/mod.rs b/src/extension/mod.rs index 26da4cd..4c0f11e 100644 --- a/src/extension/mod.rs +++ b/src/extension/mod.rs @@ -22,6 +22,7 @@ use serde_yaml::Mapping; mod date; mod shell; mod script; +mod random; pub trait Extension { fn name(&self) -> String; @@ -33,5 +34,6 @@ pub fn get_extensions() -> Vec> { Box::new(date::DateExtension::new()), Box::new(shell::ShellExtension::new()), Box::new(script::ScriptExtension::new()), + Box::new(random::RandomExtension::new()), ] } \ No newline at end of file diff --git a/src/extension/random.rs b/src/extension/random.rs new file mode 100644 index 0000000..e44a2e6 --- /dev/null +++ b/src/extension/random.rs @@ -0,0 +1,93 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019 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 rand::seq::SliceRandom; +use log::{warn, error}; + +pub struct RandomExtension {} + +impl RandomExtension { + pub fn new() -> RandomExtension { + RandomExtension{} + } +} + +impl super::Extension for RandomExtension { + fn name(&self) -> String { + String::from("random") + } + + fn calculate(&self, params: &Mapping) -> Option { + let choices = params.get(&Value::from("choices")); + if choices.is_none() { + warn!("No 'choices' parameter specified for random variable"); + return None + } + let choices = choices.unwrap().as_sequence(); + if let Some(choices) = choices { + let str_choices = choices.iter().map(|arg| { + arg.as_str().unwrap_or_default().to_string() + }).collect::>(); + + // Select a random choice between the possibilities + let choice = str_choices.choose(&mut rand::thread_rng()); + + match choice { + Some(output) => { + return Some(output.clone()) + }, + None => { + error!("Could not select a random choice."); + return None + }, + } + + } + + error!("choices array have an invalid format '{:?}'", choices); + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::extension::Extension; + + #[test] + fn test_random_basic() { + let mut params = Mapping::new(); + let choices = vec!( + "first", + "second", + "third", + ); + params.insert(Value::from("choices"), Value::from(choices.clone())); + + let extension = RandomExtension::new(); + let output = extension.calculate(¶ms); + + assert!(output.is_some()); + + let output = output.unwrap(); + + assert!(choices.iter().any(|x| x == &output)); + } +} \ No newline at end of file diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 4ca4999..aa1012a 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -56,8 +56,20 @@ impl super::Extension for ShellExtension { match output { Ok(output) => { let output_str = String::from_utf8_lossy(output.stdout.as_slice()); + let mut output_str = output_str.into_owned(); - Some(output_str.into_owned()) + // If specified, trim the output + let trim_opt = params.get(&Value::from("trim")); + if let Some(value) = trim_opt { + let val = value.as_bool(); + if let Some(val) = val { + if val { + output_str = output_str.trim().to_owned() + } + } + } + + Some(output_str) }, Err(e) => { error!("Could not execute cmd '{}', error: {}", cmd, e); @@ -65,4 +77,89 @@ impl super::Extension for ShellExtension { }, } } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::extension::Extension; + + #[test] + fn test_shell_basic() { + let mut params = Mapping::new(); + params.insert(Value::from("cmd"), Value::from("echo hello world")); + + let extension = ShellExtension::new(); + let output = extension.calculate(¶ms); + + assert!(output.is_some()); + + if cfg!(target_os = "windows") { + assert_eq!(output.unwrap(), "hello world\r\n"); + }else{ + assert_eq!(output.unwrap(), "hello world\n"); + } + } + + #[test] + fn test_shell_trimmed() { + let mut params = Mapping::new(); + params.insert(Value::from("cmd"), Value::from("echo hello world")); + params.insert(Value::from("trim"), Value::from(true)); + + let extension = ShellExtension::new(); + let output = extension.calculate(¶ms); + + assert!(output.is_some()); + assert_eq!(output.unwrap(), "hello world"); + } + + #[test] + fn test_shell_trimmed_2() { + let mut params = Mapping::new(); + if cfg!(target_os = "windows") { + params.insert(Value::from("cmd"), Value::from("echo hello world ")); + }else{ + params.insert(Value::from("cmd"), Value::from("echo \" hello world \"")); + } + + params.insert(Value::from("trim"), Value::from(true)); + + let extension = ShellExtension::new(); + let output = extension.calculate(¶ms); + + assert!(output.is_some()); + assert_eq!(output.unwrap(), "hello world"); + } + + #[test] + fn test_shell_trimmed_malformed() { + let mut params = Mapping::new(); + params.insert(Value::from("cmd"), Value::from("echo hello world")); + params.insert(Value::from("trim"), Value::from("error")); + + let extension = ShellExtension::new(); + let output = extension.calculate(¶ms); + + assert!(output.is_some()); + if cfg!(target_os = "windows") { + assert_eq!(output.unwrap(), "hello world\r\n"); + }else{ + assert_eq!(output.unwrap(), "hello world\n"); + } + } + + #[test] + #[cfg(not(target_os = "windows"))] + fn test_shell_pipes() { + let mut params = Mapping::new(); + params.insert(Value::from("cmd"), Value::from("echo hello world | cat")); + params.insert(Value::from("trim"), Value::from(true)); + + let extension = ShellExtension::new(); + let output = extension.calculate(¶ms); + + assert!(output.is_some()); + assert_eq!(output.unwrap(), "hello world"); + } } \ No newline at end of file diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs index 956d89d..538c375 100644 --- a/src/keyboard/linux.rs +++ b/src/keyboard/linux.rs @@ -42,12 +42,16 @@ impl super::KeyboardManager for LinuxKeyboardManager { unsafe { match shortcut { PasteShortcut::Default => { - let is_terminal = is_current_window_terminal(); + let is_special = is_current_window_special(); // Terminals use a different keyboard combination to paste from clipboard, // so we need to check the correct situation. - if is_terminal == 0 { + if is_special == 0 { trigger_paste(); + }else if is_special == 2 { // Special case for stterm + trigger_alt_shift_ins_paste(); + }else if is_special == 3 { // Special case for Emacs + trigger_shift_ins_paste(); }else{ trigger_terminal_paste(); } diff --git a/src/main.rs b/src/main.rs index 2b6ab0d..5d59a2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -402,9 +402,24 @@ fn start_daemon(config_set: ConfigSet) { #[cfg(target_os = "linux")] fn start_daemon(config_set: ConfigSet) { - if config_set.default.use_system_agent { - use std::process::Command; + use std::process::{Command, Stdio}; + // Check if Systemd is available in the system + let status = Command::new("systemctl") + .args(&["--version"]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .status(); + + // If Systemd is not available in the system, espanso should default to unmanaged mode + // See issue https://github.com/federico-terzi/espanso/issues/139 + let force_unmanaged = if let Err(status) = status { + true + } else { + false + }; + + if config_set.default.use_system_agent && !force_unmanaged { // Make sure espanso is currently registered in systemd let res = Command::new("systemctl") .args(&["--user", "is-enabled", "espanso.service"]) @@ -442,6 +457,10 @@ fn start_daemon(config_set: ConfigSet) { eprintln!("Error starting systemd daemon: {}", res.unwrap_err()); } }else{ + if force_unmanaged { + eprintln!("Systemd is not available in this system, switching to unmanaged mode."); + } + fork_daemon(config_set); } }