From 746957c65c52dee061fdfda9e253bffc53913848 Mon Sep 17 00:00:00 2001 From: Nikita Tchayka Date: Sun, 24 Nov 2019 21:55:55 +0000 Subject: [PATCH 01/11] Allow turning the toggle key off --- src/event/mod.rs | 1 + src/matcher/scrolling.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/event/mod.rs b/src/event/mod.rs index aeb50da..bc9828a 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -63,6 +63,7 @@ pub enum KeyModifier { ALT, META, BACKSPACE, + OFF, } impl Default for KeyModifier { diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 83db187..2d59b19 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -194,6 +194,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let config = self.config_manager.default_config(); if m == config.toggle_key { + if m == KeyModifier::OFF { return } let mut toggle_press_time = self.toggle_press_time.borrow_mut(); if let Ok(elapsed) = toggle_press_time.elapsed() { if elapsed.as_millis() < u128::from(config.toggle_interval) { From a31a2e3c5738fb3ade80f694e381fae0f9d6371b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Thu, 28 Nov 2019 20:06:40 +0100 Subject: [PATCH 02/11] Version Bump 0.3.6 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f2dae4..6c3e91e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.3.5" +version = "0.3.6" 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 1812ac1..b27483a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.3.5" +version = "0.3.6" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" From 5c875eeaed22e546baf1ca4bbd935ad924a23b73 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Thu, 28 Nov 2019 22:56:00 +0100 Subject: [PATCH 03/11] Refactor to generalize Match and implemented macOS backend for image clipboard --- native/libmacbridge/bridge.h | 6 ++ native/libmacbridge/bridge.mm | 20 +++- src/bridge/macos.rs | 1 + src/clipboard/macos.rs | 22 +++- src/clipboard/mod.rs | 3 + src/config/mod.rs | 25 ++++- src/engine.rs | 182 ++++++++++++++++++---------------- src/matcher/mod.rs | 116 ++++++++++++++++++---- 8 files changed, 268 insertions(+), 107 deletions(-) diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h index dd86bf4..33848c9 100644 --- a/native/libmacbridge/bridge.h +++ b/native/libmacbridge/bridge.h @@ -148,5 +148,11 @@ int32_t get_clipboard(char * buffer, int32_t size); */ int32_t set_clipboard(char * text); +/* + * Set the clipboard image to the given file + */ +int32_t set_clipboard_image(char * path); + + }; #endif //ESPANSO_BRIDGE_H diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm index 02ed1a5..8b31e07 100644 --- a/native/libmacbridge/bridge.mm +++ b/native/libmacbridge/bridge.mm @@ -230,6 +230,24 @@ int32_t set_clipboard(char * text) { [pasteboard setString:nsText forType:NSPasteboardTypeString]; } +int32_t set_clipboard_image(char *path) { + NSString *pathString = [NSString stringWithUTF8String:path]; + NSImage *image = [[NSImage alloc] initWithContentsOfFile:pathString]; + int result = 0; + + if (image != nil) { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + NSArray *copiedObjects = [NSArray arrayWithObject:image]; + [pasteboard writeObjects:copiedObjects]; + result = 1; + } + [image release]; + + return result; +} + + // CONTEXT MENU int32_t show_context_menu(MenuItem * items, int32_t count) { @@ -273,4 +291,4 @@ int32_t prompt_accessibility() { void open_settings_panel() { NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"; [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; -} +} \ No newline at end of file diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs index 0b09101..f99a6f8 100644 --- a/src/bridge/macos.rs +++ b/src/bridge/macos.rs @@ -43,6 +43,7 @@ extern { // Clipboard pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32; pub fn set_clipboard(text: *const c_char) -> i32; + pub fn set_clipboard_image(path: *const c_char) -> i32; // UI pub fn register_icon_click_callback(cb: extern fn(_self: *mut c_void)); diff --git a/src/clipboard/macos.rs b/src/clipboard/macos.rs index 65c10bf..6c653db 100644 --- a/src/clipboard/macos.rs +++ b/src/clipboard/macos.rs @@ -18,8 +18,10 @@ */ use std::os::raw::c_char; -use crate::bridge::macos::{get_clipboard, set_clipboard}; +use crate::bridge::macos::*; use std::ffi::{CStr, CString}; +use std::path::Path; +use log::{error, warn}; pub struct MacClipboardManager { @@ -52,6 +54,24 @@ impl super::ClipboardManager for MacClipboardManager { } } } + + fn set_clipboard_image(&self, image_path: &Path) { + // Make sure the image exist beforehand + if !image_path.exists() { + error!("Image not found in path: {:?}", image_path); + }else{ + let path_string = image_path.to_string_lossy().into_owned(); + let res = CString::new(path_string); + if let Ok(path) = res { + unsafe { + let result = set_clipboard_image(path.as_ptr()); + if result != 1 { + warn!("Couldn't set clipboard for image: {:?}", image_path) + } + } + } + } + } } impl MacClipboardManager { diff --git a/src/clipboard/mod.rs b/src/clipboard/mod.rs index c99a4cc..b04d5c2 100644 --- a/src/clipboard/mod.rs +++ b/src/clipboard/mod.rs @@ -17,6 +17,8 @@ * along with espanso. If not, see . */ +use std::path::Path; + #[cfg(target_os = "windows")] mod windows; @@ -29,6 +31,7 @@ mod macos; pub trait ClipboardManager { fn get_clipboard(&self) -> Option; fn set_clipboard(&self, payload: &str); + fn set_clipboard_image(&self, image_path: &Path); } // LINUX IMPLEMENTATION diff --git a/src/config/mod.rs b/src/config/mod.rs index 50361ae..78326e3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -426,6 +426,7 @@ mod tests { use std::io::Write; use tempfile::{NamedTempFile, TempDir}; use std::any::Any; + use crate::matcher::{TextContent, MatchContentType}; const TEST_WORKING_CONFIG_FILE : &str = include_str!("../res/test/working_config.yml"); const TEST_CONFIG_FILE_WITH_BAD_YAML : &str = include_str!("../res/test/config_with_bad_yaml.yml"); @@ -727,7 +728,13 @@ mod tests { assert_eq!(config_set.default.matches.len(), 2); assert_eq!(config_set.specific[0].matches.len(), 2); - assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":lol" && x.replace == "newstring").is_some()); + assert!(config_set.specific[0].matches.iter().find(|x| { + if let MatchContentType::Text(content) = &x.content { + x.trigger == ":lol" && content.replace == "newstring" + }else{ + false + } + }).is_some()); assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":yess").is_some()); } @@ -755,7 +762,13 @@ mod tests { assert_eq!(config_set.default.matches.len(), 2); assert_eq!(config_set.specific[0].matches.len(), 1); - assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == "hello" && x.replace == "newstring").is_some()); + assert!(config_set.specific[0].matches.iter().find(|x| { + if let MatchContentType::Text(content) = &x.content { + x.trigger == "hello" && content.replace == "newstring" + }else{ + false + } + }).is_some()); } #[test] @@ -897,7 +910,13 @@ mod tests { let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); assert_eq!(config_set.specific.len(), 0); assert_eq!(config_set.default.matches.len(), 1); - assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta" && m.replace == "world")); + assert!(config_set.default.matches.iter().any(|m| { + if let MatchContentType::Text(content) = &m.content { + m.trigger == "hasta" && content.replace == "world" + }else{ + false + } + })); } #[test] diff --git a/src/engine.rs b/src/engine.rs index bc604c1..5abdde0 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use crate::matcher::{Match, MatchReceiver}; +use crate::matcher::{Match, MatchReceiver, MatchContentType}; use crate::keyboard::KeyboardManager; use crate::config::ConfigManager; use crate::config::BackendType; @@ -29,6 +29,7 @@ use crate::extension::Extension; use std::cell::RefCell; use std::process::exit; use std::collections::HashMap; +use std::path::PathBuf; use regex::{Regex, Captures}; pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, @@ -118,97 +119,110 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.keyboard_manager.delete_string(char_count); - let mut target_string = if m._has_vars { - let mut output_map = HashMap::new(); + // Manage the different types of matches + match &m.content { + // Text Match + MatchContentType::Text(content) => { + let mut target_string = if content._has_vars { + let mut output_map = HashMap::new(); - for variable in m.vars.iter() { - let extension = self.extension_map.get(&variable.var_type); - if let Some(extension) = extension { - let ext_out = extension.calculate(&variable.params); - if let Some(output) = ext_out { - output_map.insert(variable.name.clone(), output); - }else{ - output_map.insert(variable.name.clone(), "".to_owned()); - warn!("Could not generate output for variable: {}", variable.name); - } - }else{ - error!("No extension found for variable type: {}", variable.var_type); - } - } - - // Replace the variables - let result = VAR_REGEX.replace_all(&m.replace, |caps: &Captures| { - let var_name = caps.name("name").unwrap().as_str(); - let output = output_map.get(var_name); - output.unwrap() - }); - - result.to_string() - }else{ // No variables, simple text substitution - m.replace.clone() - }; - - // If a trailing separator was counted in the match, add it back to the target string - if let Some(trailing_separator) = trailing_separator { - if trailing_separator == '\r' { // If the trailing separator is a carriage return, - target_string.push('\n'); // convert it to new line - }else{ - target_string.push(trailing_separator); - } - } - - // Convert Windows style newlines into unix styles - target_string = target_string.replace("\r\n", "\n"); - - // Calculate cursor rewind moves if a Cursor Hint is present - let index = target_string.find("$|$"); - let cursor_rewind = if let Some(index) = index { - // Convert the byte index to a char index - let char_str = &target_string[0..index]; - let char_index = char_str.chars().count(); - let total_size = target_string.chars().count(); - - // Remove the $|$ placeholder - target_string = target_string.replace("$|$", ""); - - // Calculate the amount of rewind moves needed (LEFT ARROW). - // Subtract also 3, equal to the number of chars of the placeholder "$|$" - let moves = (total_size - char_index - 3) as i32; - Some(moves) - }else{ - None - }; - - match config.backend { - BackendType::Inject => { - // Send the expected string. On linux, newlines are managed automatically - // while on windows and macos, we need to emulate a Enter key press. - - if cfg!(target_os = "linux") { - self.keyboard_manager.send_string(&target_string); - }else{ - // 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(); + for variable in content.vars.iter() { + let extension = self.extension_map.get(&variable.var_type); + if let Some(extension) = extension { + let ext_out = extension.calculate(&variable.params); + if let Some(output) = ext_out { + output_map.insert(variable.name.clone(), output); + }else{ + output_map.insert(variable.name.clone(), "".to_owned()); + warn!("Could not generate output for variable: {}", variable.name); + } + }else{ + error!("No extension found for variable type: {}", variable.var_type); } - - self.keyboard_manager.send_string(split); } + + // 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() + }); + + result.to_string() + }else{ // No variables, simple text substitution + content.replace.clone() + }; + + // If a trailing separator was counted in the match, add it back to the target string + if let Some(trailing_separator) = trailing_separator { + if trailing_separator == '\r' { // If the trailing separator is a carriage return, + target_string.push('\n'); // convert it to new line + }else{ + target_string.push(trailing_separator); + } + } + + // Convert Windows style newlines into unix styles + target_string = target_string.replace("\r\n", "\n"); + + // Calculate cursor rewind moves if a Cursor Hint is present + let index = target_string.find("$|$"); + let cursor_rewind = if let Some(index) = index { + // Convert the byte index to a char index + let char_str = &target_string[0..index]; + let char_index = char_str.chars().count(); + let total_size = target_string.chars().count(); + + // Remove the $|$ placeholder + target_string = target_string.replace("$|$", ""); + + // Calculate the amount of rewind moves needed (LEFT ARROW). + // Subtract also 3, equal to the number of chars of the placeholder "$|$" + let moves = (total_size - char_index - 3) as i32; + Some(moves) + }else{ + None + }; + + match config.backend { + BackendType::Inject => { + // Send the expected string. On linux, newlines are managed automatically + // while on windows and macos, we need to emulate a Enter key press. + + if cfg!(target_os = "linux") { + self.keyboard_manager.send_string(&target_string); + }else{ + // 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(); + } + + self.keyboard_manager.send_string(split); + } + } + }, + BackendType::Clipboard => { + self.clipboard_manager.set_clipboard(&target_string); + self.keyboard_manager.trigger_paste(); + }, + } + + if let Some(moves) = cursor_rewind { + // Simulate left arrow key presses to bring the cursor into the desired position + self.keyboard_manager.move_cursor_left(moves); } }, - BackendType::Clipboard => { - self.clipboard_manager.set_clipboard(&target_string); + + // Image Match + MatchContentType::Image(content) => { + let image_path = PathBuf::from(&content.path); + self.clipboard_manager.set_clipboard_image(&image_path); self.keyboard_manager.trigger_paste(); }, } - - if let Some(moves) = cursor_rewind { - // Simulate left arrow key presses to bring the cursor into the desired position - self.keyboard_manager.move_cursor_left(moves); - } } fn on_enable_update(&self, status: bool) { diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 4667318..2811a07 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -28,18 +28,34 @@ pub(crate) mod scrolling; #[derive(Debug, Serialize, Clone)] pub struct Match { pub trigger: String, - pub replace: String, - pub vars: Vec, + pub content: MatchContentType, pub word: bool, - #[serde(skip_serializing)] - pub _has_vars: bool, - // Automatically calculated from the trigger, used by the matcher to check for correspondences. #[serde(skip_serializing)] pub _trigger_sequence: Vec, } +#[derive(Debug, Serialize, Clone)] +pub enum MatchContentType { + Text(TextContent), + Image(ImageContent), +} + +#[derive(Debug, Serialize, Clone)] +pub struct TextContent { + pub replace: String, + pub vars: Vec, + + #[serde(skip_serializing)] + pub _has_vars: bool, +} + +#[derive(Debug, Serialize, Clone)] +pub struct ImageContent { + pub path: String, +} + impl <'de> serde::Deserialize<'de> for Match { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { @@ -53,15 +69,10 @@ 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(); - } + }; // TODO: may need to replace windows newline (\r\n) with newline only (\n) - let new_replace = other.replace.clone(); - - // Check if the match contains variables - let has_vars = VAR_REGEX.is_match(&other.replace); - // Calculate the trigger sequence let mut trigger_sequence = Vec::new(); let trigger_chars : Vec = other.trigger.chars().collect(); @@ -72,12 +83,34 @@ impl<'a> From<&'a AutoMatch> for Match{ trigger_sequence.push(TriggerEntry::WordSeparator); } + let content = if let Some(replace) = &other.replace { // Text match + let new_replace = replace.clone(); + + // Check if the match contains variables + let has_vars = VAR_REGEX.is_match(replace); + + let content = TextContent { + replace: new_replace, + vars: other.vars.clone(), + _has_vars: has_vars, + }; + + MatchContentType::Text(content) + }else if let Some(image_path) = &other.image_path { // Image match + let content = ImageContent { + path: image_path.clone() + }; + + MatchContentType::Image(content) + }else { + eprintln!("ERROR: no action specified for match {}, please specify either 'replace' or 'image_path'", other.trigger); + std::process::exit(2); + }; + Self { trigger: other.trigger.clone(), - replace: new_replace, - vars: other.vars.clone(), + content, word: other.word.clone(), - _has_vars: has_vars, _trigger_sequence: trigger_sequence, } } @@ -87,7 +120,12 @@ impl<'a> From<&'a AutoMatch> for Match{ #[derive(Debug, Serialize, Deserialize, Clone)] struct AutoMatch { pub trigger: String, - pub replace: String, + + #[serde(default = "default_replace")] + pub replace: Option, + + #[serde(default = "default_image_path")] + pub image_path: Option, #[serde(default = "default_vars")] pub vars: Vec, @@ -98,6 +136,8 @@ struct AutoMatch { fn default_vars() -> Vec {Vec::new()} fn default_word() -> bool {false} +fn default_replace() -> Option {None} +fn default_image_path() -> Option {None} #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MatchVariable { @@ -154,7 +194,14 @@ mod tests { let _match : Match = serde_yaml::from_str(match_str).unwrap(); - assert_eq!(_match._has_vars, false); + match _match.content { + MatchContentType::Text(content) => { + assert_eq!(content._has_vars, false); + }, + _ => { + assert!(false); + }, + } } #[test] @@ -166,7 +213,14 @@ mod tests { let _match : Match = serde_yaml::from_str(match_str).unwrap(); - assert_eq!(_match._has_vars, true); + match _match.content { + MatchContentType::Text(content) => { + assert_eq!(content._has_vars, true); + }, + _ => { + assert!(false); + }, + } } #[test] @@ -178,7 +232,14 @@ mod tests { let _match : Match = serde_yaml::from_str(match_str).unwrap(); - assert_eq!(_match._has_vars, true); + match _match.content { + MatchContentType::Text(content) => { + assert_eq!(content._has_vars, true); + }, + _ => { + assert!(false); + }, + } } #[test] @@ -212,4 +273,23 @@ mod tests { assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t')); assert_eq!(_match._trigger_sequence[4], TriggerEntry::WordSeparator); } + + #[test] + fn test_match_with_image_content() { + let match_str = r###" + trigger: "test" + image_path: "/path/to/file" + "###; + + let _match : Match = serde_yaml::from_str(match_str).unwrap(); + + match _match.content { + MatchContentType::Image(content) => { + assert_eq!(content.path, "/path/to/file"); + }, + _ => { + assert!(false); + }, + } + } } \ No newline at end of file From 283db379c8cff86edface8e9e8aa4e0c988c5a7f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Thu, 28 Nov 2019 23:06:12 +0100 Subject: [PATCH 04/11] Move image check in the engine --- src/clipboard/macos.rs | 19 +++++++------------ src/engine.rs | 10 ++++++++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/clipboard/macos.rs b/src/clipboard/macos.rs index 6c653db..3777d21 100644 --- a/src/clipboard/macos.rs +++ b/src/clipboard/macos.rs @@ -56,18 +56,13 @@ impl super::ClipboardManager for MacClipboardManager { } fn set_clipboard_image(&self, image_path: &Path) { - // Make sure the image exist beforehand - if !image_path.exists() { - error!("Image not found in path: {:?}", image_path); - }else{ - let path_string = image_path.to_string_lossy().into_owned(); - let res = CString::new(path_string); - if let Ok(path) = res { - unsafe { - let result = set_clipboard_image(path.as_ptr()); - if result != 1 { - warn!("Couldn't set clipboard for image: {:?}", image_path) - } + let path_string = image_path.to_string_lossy().into_owned(); + let res = CString::new(path_string); + if let Ok(path) = res { + unsafe { + let result = set_clipboard_image(path.as_ptr()); + if result != 1 { + warn!("Couldn't set clipboard for image: {:?}", image_path) } } } diff --git a/src/engine.rs b/src/engine.rs index 5abdde0..d0163e6 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -219,8 +219,14 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa // Image Match MatchContentType::Image(content) => { let image_path = PathBuf::from(&content.path); - self.clipboard_manager.set_clipboard_image(&image_path); - self.keyboard_manager.trigger_paste(); + + // Make sure the image exist beforehand + if image_path.exists() { + self.clipboard_manager.set_clipboard_image(&image_path); + self.keyboard_manager.trigger_paste(); + }else{ + error!("Image not found in path: {:?}", image_path); + } }, } } From 89d0bd8596d5fa28f4a72beb32f9ec40ecd18e6d Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 29 Nov 2019 00:01:26 +0100 Subject: [PATCH 05/11] Add Windows backend on Image clipboard --- native/libwinbridge/bridge.cpp | 43 ++++++++++++++++++++++++++++++++++ native/libwinbridge/bridge.h | 5 ++++ src/bridge/windows.rs | 1 + src/clipboard/windows.rs | 11 ++++++++- src/engine.rs | 8 +++---- src/matcher/mod.rs | 27 +++++++++++++++++++-- 6 files changed, 87 insertions(+), 8 deletions(-) diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index 309395c..f83e26b 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -31,6 +31,9 @@ #include #include +#pragma comment( lib, "gdiplus.lib" ) +#include + // How many milliseconds must pass between keystrokes to refresh the keyboard layout const long refreshKeyboardLayoutInterval = 2000; @@ -656,3 +659,43 @@ int32_t get_clipboard(wchar_t *buffer, int32_t size) { CloseClipboard(); } + +int32_t set_clipboard_image(wchar_t *path) { + bool result = false; + + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); + + Gdiplus::Bitmap *gdibmp = Gdiplus::Bitmap::FromFile(path); + if (gdibmp) + { + HBITMAP hbitmap; + gdibmp->GetHBITMAP(0, &hbitmap); + if (OpenClipboard(NULL)) + { + EmptyClipboard(); + DIBSECTION ds; + if (GetObject(hbitmap, sizeof(DIBSECTION), &ds)) + { + HDC hdc = GetDC(HWND_DESKTOP); + //create compatible bitmap (get DDB from DIB) + HBITMAP hbitmap_ddb = CreateDIBitmap(hdc, &ds.dsBmih, CBM_INIT, + ds.dsBm.bmBits, (BITMAPINFO*)&ds.dsBmih, DIB_RGB_COLORS); + ReleaseDC(HWND_DESKTOP, hdc); + SetClipboardData(CF_BITMAP, hbitmap_ddb); + DeleteObject(hbitmap_ddb); + result = true; + } + CloseClipboard(); + } + + //cleanup: + DeleteObject(hbitmap); + delete gdibmp; + } + + Gdiplus::GdiplusShutdown(gdiplusToken); + + return result; +} diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h index 85d86ad..77fa4d4 100644 --- a/native/libwinbridge/bridge.h +++ b/native/libwinbridge/bridge.h @@ -146,4 +146,9 @@ extern "C" int32_t get_clipboard(wchar_t * buffer, int32_t size); */ extern "C" int32_t set_clipboard(wchar_t * text); +/* + * Set the clipboard image to the given path + */ +extern "C" int32_t set_clipboard_image(wchar_t * path); + #endif //ESPANSO_BRIDGE_H \ No newline at end of file diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs index db9833e..ff916fa 100644 --- a/src/bridge/windows.rs +++ b/src/bridge/windows.rs @@ -47,6 +47,7 @@ extern { // CLIPBOARD pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32; pub fn set_clipboard(payload: *const u16) -> i32; + pub fn set_clipboard_image(path: *const u16) -> i32; // KEYBOARD pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const u16, diff --git a/src/clipboard/windows.rs b/src/clipboard/windows.rs index caa8e0e..63fa070 100644 --- a/src/clipboard/windows.rs +++ b/src/clipboard/windows.rs @@ -18,7 +18,8 @@ */ use widestring::U16CString; -use crate::bridge::windows::{set_clipboard, get_clipboard}; +use crate::bridge::windows::{set_clipboard, get_clipboard, set_clipboard_image}; +use std::path::Path; pub struct WindowsClipboardManager { @@ -53,4 +54,12 @@ impl super::ClipboardManager for WindowsClipboardManager { set_clipboard(payload_c.as_ptr()); } } + + fn set_clipboard_image(&self, image_path: &Path) { + let path_string = image_path.to_string_lossy().into_owned(); + unsafe { + let payload_c = U16CString::from_str(path_string).unwrap(); + set_clipboard_image(payload_c.as_ptr()); + } + } } \ No newline at end of file diff --git a/src/engine.rs b/src/engine.rs index d0163e6..19f96f8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -218,14 +218,12 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa // Image Match MatchContentType::Image(content) => { - let image_path = PathBuf::from(&content.path); - // Make sure the image exist beforehand - if image_path.exists() { - self.clipboard_manager.set_clipboard_image(&image_path); + if content.path.exists() { + self.clipboard_manager.set_clipboard_image(&content.path); self.keyboard_manager.trigger_paste(); }else{ - error!("Image not found in path: {:?}", image_path); + error!("Image not found in path: {:?}", content.path); } }, } diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 2811a07..947dce0 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -22,6 +22,8 @@ use crate::event::{KeyEvent, KeyModifier}; use crate::event::KeyEventReceiver; use serde_yaml::Mapping; use regex::Regex; +use std::path::PathBuf; +use std::fs; pub(crate) mod scrolling; @@ -53,7 +55,7 @@ pub struct TextContent { #[derive(Debug, Serialize, Clone)] pub struct ImageContent { - pub path: String, + pub path: PathBuf, } impl <'de> serde::Deserialize<'de> for Match { @@ -97,8 +99,29 @@ impl<'a> From<&'a AutoMatch> for Match{ MatchContentType::Text(content) }else if let Some(image_path) = &other.image_path { // Image match + // On Windows, we have to replace the forward / with the backslash \ in the path + let new_path = if cfg!(target_os = "windows") { + image_path.replace("/", "\\") + }else{ + image_path.to_owned() + }; + + // Calculate variables in path + let new_path = if new_path.contains("$CONFIG") { + let config_dir = crate::context::get_config_dir(); + let config_path = fs::canonicalize(&config_dir); + let config_path = if let Ok(config_path) = config_path { + config_path.to_string_lossy().into_owned() + }else{ + "".to_owned() + }; + new_path.replace("$CONFIG", &config_path) + }else{ + new_path.to_owned() + }; + let content = ImageContent { - path: image_path.clone() + path: PathBuf::from(new_path) }; MatchContentType::Image(content) From f884ade3ffed30a274bad19e9c3e8ad415b06e2f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 29 Nov 2019 21:38:29 +0100 Subject: [PATCH 06/11] Add image clipboard backend on Linux --- src/clipboard/linux.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/clipboard/linux.rs b/src/clipboard/linux.rs index 7e14a07..8fb76e1 100644 --- a/src/clipboard/linux.rs +++ b/src/clipboard/linux.rs @@ -19,7 +19,8 @@ use std::process::{Command, Stdio}; use std::io::{Write}; -use log::error; +use log::{error, warn}; +use std::path::Path; pub struct LinuxClipboardManager {} @@ -63,6 +64,29 @@ impl super::ClipboardManager for LinuxClipboardManager { } } } + + fn set_clipboard_image(&self, image_path: &Path) { + let extension = image_path.extension(); + let mime = match extension { + Some(ext) => { + let ext = ext.to_string_lossy().to_lowercase(); + match ext.as_ref() { + "png" => {"image/png"}, + "jpg" | "jpeg" => {"image/jpeg"}, + "gif" => {"image/gif"}, + "svg" => {"image/svg"}, + _ => {"image/png"}, + } + }, + None => {"image/png"}, + }; + + let image_path = image_path.to_string_lossy().into_owned(); + + let res = Command::new("xclip") + .args(&["-selection", "clipboard", "-t", mime, "-i", &image_path]) + .spawn(); + } } impl LinuxClipboardManager { From c1ab626b75ca771f7f025d3b0192937ced282320 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 29 Nov 2019 21:45:42 +0100 Subject: [PATCH 07/11] Version bump 0.4.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c3e91e..8302dbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.3.6" +version = "0.4.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 b27483a..985ca3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.3.6" +version = "0.4.0" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" From 182d44580ae9608ca587c94edb47d7a10e89f50a Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 29 Nov 2019 22:09:02 +0100 Subject: [PATCH 08/11] Add configurable Paste shortcuts. Helps to fix #130 --- src/config/mod.rs | 4 ++++ src/engine.rs | 4 ++-- src/keyboard/linux.rs | 31 +++++++++++++++++++++++-------- src/keyboard/macos.rs | 15 +++++++++++++-- src/keyboard/mod.rs | 19 ++++++++++++++++++- src/keyboard/windows.rs | 15 +++++++++++++-- 6 files changed, 73 insertions(+), 15 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 78326e3..64463d4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -26,6 +26,7 @@ use std::fs::{File, create_dir_all}; use std::io::Read; use serde::{Serialize, Deserialize}; use crate::event::KeyModifier; +use crate::keyboard::PasteShortcut; use std::collections::{HashSet, HashMap}; use log::{error}; use std::fmt; @@ -97,6 +98,9 @@ pub struct Configs { #[serde(default = "default_toggle_interval")] pub toggle_interval: u32, + #[serde(default)] + pub paste_shortcut: PasteShortcut, + #[serde(default = "default_backspace_limit")] pub backspace_limit: i32, diff --git a/src/engine.rs b/src/engine.rs index 19f96f8..79f5e3d 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -206,7 +206,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa }, BackendType::Clipboard => { self.clipboard_manager.set_clipboard(&target_string); - self.keyboard_manager.trigger_paste(); + self.keyboard_manager.trigger_paste(&config.paste_shortcut); }, } @@ -221,7 +221,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa // Make sure the image exist beforehand if content.path.exists() { self.clipboard_manager.set_clipboard_image(&content.path); - self.keyboard_manager.trigger_paste(); + self.keyboard_manager.trigger_paste(&config.paste_shortcut); }else{ error!("Image not found in path: {:?}", content.path); } diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs index 7b30028..1476316 100644 --- a/src/keyboard/linux.rs +++ b/src/keyboard/linux.rs @@ -19,6 +19,8 @@ use std::ffi::CString; use crate::bridge::linux::*; +use super::PasteShortcut; +use log::error; pub struct LinuxKeyboardManager { } @@ -36,16 +38,29 @@ impl super::KeyboardManager for LinuxKeyboardManager { // On linux this is not needed, so NOOP } - fn trigger_paste(&self) { + fn trigger_paste(&self, shortcut: &PasteShortcut) { unsafe { - let is_terminal = is_current_window_terminal(); + match shortcut { + PasteShortcut::Default => { + let is_terminal = is_current_window_terminal(); - // Terminals use a different keyboard combination to paste from clipboard, - // so we need to check the correct situation. - if is_terminal == 0 { - trigger_paste(); - }else{ - trigger_terminal_paste(); + // Terminals use a different keyboard combination to paste from clipboard, + // so we need to check the correct situation. + if is_terminal == 0 { + trigger_paste(); + }else{ + trigger_terminal_paste(); + } + }, + PasteShortcut::CtrlV => { + trigger_paste(); + }, + PasteShortcut::CtrlShiftV => { + trigger_terminal_paste(); + } + _ => { + error!("Linux backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.") + } } } } diff --git a/src/keyboard/macos.rs b/src/keyboard/macos.rs index 724845a..6cba0da 100644 --- a/src/keyboard/macos.rs +++ b/src/keyboard/macos.rs @@ -19,6 +19,8 @@ use std::ffi::CString; use crate::bridge::macos::*; +use super::PasteShortcut; +use log::error; pub struct MacKeyboardManager { } @@ -39,9 +41,18 @@ impl super::KeyboardManager for MacKeyboardManager { } } - fn trigger_paste(&self) { + fn trigger_paste(&self, shortcut: &PasteShortcut) { unsafe { - trigger_paste(); + match shortcut { + PasteShortcut::Default => { + unsafe { + trigger_paste(); + } + }, + _ => { + error!("MacOS backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.") + } + } } } diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs index 5a9cc5b..d8f2611 100644 --- a/src/keyboard/mod.rs +++ b/src/keyboard/mod.rs @@ -17,6 +17,8 @@ * along with espanso. If not, see . */ +use serde::{Serialize, Deserialize, Deserializer}; + #[cfg(target_os = "windows")] mod windows; @@ -29,11 +31,26 @@ mod macos; pub trait KeyboardManager { fn send_string(&self, s: &str); fn send_enter(&self); - fn trigger_paste(&self); + fn trigger_paste(&self, shortcut: &PasteShortcut); fn delete_string(&self, count: i32); fn move_cursor_left(&self, count: i32); } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum PasteShortcut { + Default, // Default one for the current system + CtrlV, // Classic Ctrl+V shortcut + CtrlShiftV, // Could be used to paste without formatting in many applications + ShiftInsert, // Often used in Linux systems + MetaV, // Corresponding to Win+V on Windows and Linux, CMD+V on macOS +} + +impl Default for PasteShortcut{ + fn default() -> Self { + PasteShortcut::Default + } +} + // WINDOWS IMPLEMENTATION #[cfg(target_os = "windows")] pub fn get_manager() -> impl KeyboardManager { diff --git a/src/keyboard/windows.rs b/src/keyboard/windows.rs index 8193006..c4e110b 100644 --- a/src/keyboard/windows.rs +++ b/src/keyboard/windows.rs @@ -19,6 +19,8 @@ use widestring::{U16CString}; use crate::bridge::windows::*; +use super::PasteShortcut; +use log::error; pub struct WindowsKeyboardManager { } @@ -44,9 +46,18 @@ impl super::KeyboardManager for WindowsKeyboardManager { } } - fn trigger_paste(&self) { + fn trigger_paste(&self, shortcut: &PasteShortcut) { unsafe { - trigger_paste(); + match shortcut { + PasteShortcut::Default => { + unsafe { + trigger_paste(); + } + }, + _ => { + error!("Windows backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.") + } + } } } From 87ca08c0590c0a9a42771b34470115accb617ecc Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 29 Nov 2019 22:20:37 +0100 Subject: [PATCH 09/11] Add Shift Insert shortcut on Linux backend --- native/liblinuxbridge/bridge.cpp | 6 ++++++ native/liblinuxbridge/bridge.h | 5 +++++ src/bridge/linux.rs | 1 + src/keyboard/linux.rs | 5 ++++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index b757ff4..5695fa3 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -299,6 +299,10 @@ void trigger_terminal_paste() { xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Shift+v", 8000); } +void trigger_shift_ins_paste() { + xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Insert", 8000); +} + // SYSTEM MODULE // Function taken from the wmlib tool source code @@ -470,3 +474,5 @@ int32_t is_current_window_terminal() { return 0; } + + diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h index 16972eb..9d07122 100644 --- a/native/liblinuxbridge/bridge.h +++ b/native/liblinuxbridge/bridge.h @@ -82,6 +82,11 @@ extern "C" void trigger_paste(); */ extern "C" void trigger_terminal_paste(); +/* + * Trigger shift ins pasting( Pressing SHIFT+INS ) + */ +extern "C" void trigger_shift_ins_paste(); + // SYSTEM MODULE diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs index f93efbb..4947629 100644 --- a/src/bridge/linux.rs +++ b/src/bridge/linux.rs @@ -42,4 +42,5 @@ extern { pub fn left_arrow(count: i32); pub fn trigger_paste(); pub fn trigger_terminal_paste(); + pub fn trigger_shift_ins_paste(); } \ No newline at end of file diff --git a/src/keyboard/linux.rs b/src/keyboard/linux.rs index 1476316..956d89d 100644 --- a/src/keyboard/linux.rs +++ b/src/keyboard/linux.rs @@ -57,7 +57,10 @@ impl super::KeyboardManager for LinuxKeyboardManager { }, PasteShortcut::CtrlShiftV => { trigger_terminal_paste(); - } + }, + PasteShortcut::ShiftInsert=> { + trigger_shift_ins_paste(); + }, _ => { error!("Linux backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.") } From c9dd62463702f8331970792efd5af83712b39b85 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 30 Nov 2019 18:47:22 +0100 Subject: [PATCH 10/11] Update readme with Support link and new contributor --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 1df359f..df78593 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,13 @@ ___ Visit the [official documentation](https://espanso.org/docs/). +## Support + +If you need some help to setup espanso, want to ask a question or simply get involved +in the community, [Join the official Subreddit](https://www.reddit.com/r/espanso/)! :) + + + ## Donations espanso is a free, open source software developed in my (little) spare time. @@ -57,6 +64,7 @@ Many people helped the project along the way, thanks to all of you. In particula * [Luca Antognetti](https://github.com/luca-ant) - Linux and Windows Tester * [Matteo Pellegrino](https://www.matteopellegrino.me/) - MacOS Tester * [Timo Runge](http://timorunge.com/) - MacOS contributor +* [NickSeagull](http://nickseagull.github.io/) - Contributor ## Remarks From 3873bccdd891f28745c3d1afb5beca1e67915b0c Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 30 Nov 2019 19:06:19 +0100 Subject: [PATCH 11/11] Fix bug that prevented a test from working correctly --- src/matcher/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 947dce0..c1ac263 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -308,7 +308,7 @@ mod tests { match _match.content { MatchContentType::Image(content) => { - assert_eq!(content.path, "/path/to/file"); + assert_eq!(content.path, PathBuf::from("/path/to/file")); }, _ => { assert!(false);