From 6c1977f48ad422f951d68927c67b05d10e157a30 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 19 Oct 2019 23:31:05 +0200 Subject: [PATCH 01/16] First draft of the word separation feature, proposed in issue #82 --- src/config/mod.rs | 4 ++ src/engine.rs | 17 +++++- src/matcher/mod.rs | 8 ++- src/matcher/scrolling.rs | 115 ++++++++++++++++++++++++++++++++------- 4 files changed, 121 insertions(+), 23 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index a8bf6cc..009526b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -50,6 +50,7 @@ fn default_log_level() -> i32 { 0 } fn default_ipc_server_port() -> i32 { 34982 } fn default_use_system_agent() -> bool { true } fn default_config_caching_interval() -> i32 { 800 } +fn default_word_separators() -> Vec { vec![' ', ',', '.', '\r', '\n'] } fn default_toggle_interval() -> u32 { 230 } fn default_backspace_limit() -> i32 { 3 } fn default_exclude_default_matches() -> bool {false} @@ -87,6 +88,9 @@ pub struct Configs { #[serde(default = "default_config_caching_interval")] pub config_caching_interval: i32, + #[serde(default = "default_word_separators")] + pub word_separators: Vec, // TODO: add parsing test + #[serde(default)] pub toggle_key: KeyModifier, diff --git a/src/engine.rs b/src/engine.rs index d848b27..72ad643 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -103,16 +103,22 @@ lazy_static! { impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> MatchReceiver for Engine<'a, S, C, M, U>{ - fn on_match(&self, m: &Match) { + fn on_match(&self, m: &Match, trailing_separator: Option) { let config = self.config_manager.active_config(); if config.disabled { return; } - self.keyboard_manager.delete_string(m.trigger.chars().count() as i32); + let char_count = if trailing_separator.is_none() { + m.trigger.chars().count() as i32 + }else{ + m.trigger.chars().count() as i32 + 1 // Count also the separator + }; - let target_string = if m._has_vars { + self.keyboard_manager.delete_string(char_count); + + let mut target_string = if m._has_vars { let mut output_map = HashMap::new(); for variable in m.vars.iter() { @@ -142,6 +148,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa 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 { + target_string.push(trailing_separator); + } + match config.backend { BackendType::Inject => { // Send the expected string. On linux, newlines are managed automatically diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 59b9ca7..a806edc 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -30,6 +30,7 @@ pub struct Match { pub trigger: String, pub replace: String, pub vars: Vec, + pub word: bool, #[serde(skip_serializing)] pub _has_vars: bool, @@ -57,6 +58,7 @@ impl<'a> From<&'a AutoMatch> for Match{ trigger: other.trigger.clone(), replace: other.replace.clone(), vars: other.vars.clone(), + word: other.word.clone(), _has_vars: has_vars, } } @@ -70,9 +72,13 @@ struct AutoMatch { #[serde(default = "default_vars")] pub vars: Vec, + + #[serde(default = "default_word")] + pub word: bool, } fn default_vars() -> Vec {Vec::new()} +fn default_word() -> bool {false} #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MatchVariable { @@ -85,7 +91,7 @@ pub struct MatchVariable { } pub trait MatchReceiver { - fn on_match(&self, m: &Match); + fn on_match(&self, m: &Match, trailing_separator: Option); fn on_enable_update(&self, status: bool); } diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 3d919db..b45b09c 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -31,12 +31,19 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { current_set_queue: RefCell>>>, toggle_press_time: RefCell, is_enabled: RefCell, + was_previous_char_word_separator: RefCell, } +#[derive(Clone)] struct MatchEntry<'a> { start: usize, count: usize, - _match: &'a Match + _match: &'a Match, + + // Usually false, becomes true if the match was detected and has the "word" option. + // This is needed to trigger the replacement only if the next char is a + // word separator ( such as space ). + waiting_for_separator: bool, } impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { @@ -49,7 +56,8 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { receiver, current_set_queue, toggle_press_time, - is_enabled: RefCell::new(true) + is_enabled: RefCell::new(true), + was_previous_char_word_separator: RefCell::new(true), } } @@ -75,33 +83,72 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa return; } + // Obtain the configuration for the active application if present, + // otherwise get the default one + let active_config = self.config_manager.active_config(); + + // Check if the current char is a word separator + let is_current_word_separator = active_config.word_separators.contains( + &c.chars().nth(0).unwrap_or_default() + ); + if is_current_word_separator { + + } + + 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(); - let new_matches: Vec = self.config_manager.matches().iter() - .filter(|&x| x.trigger.starts_with(c)) + let new_matches: Vec = active_config.matches.iter() + .filter(|&x| { + if !x.trigger.starts_with(c) { + false + }else{ + if x.word { + *was_previous_word_separator + }else{ + true + } + } + }) .map(|x | MatchEntry{ start: 1, count: x.trigger.chars().count(), - _match: &x + _match: &x, + waiting_for_separator: false }) .collect(); // TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup. - let combined_matches: Vec = match current_set_queue.back() { + let mut combined_matches: Vec = match current_set_queue.back_mut() { Some(last_matches) => { let mut updated: Vec = last_matches.iter() .filter(|&x| { - let nchar = x._match.trigger.chars().nth(x.start); - if let Some(nchar) = nchar { - c.starts_with(nchar) + if x.waiting_for_separator { + // The match is only waiting for a separator to call the replacement + is_current_word_separator }else{ - false + let nchar = x._match.trigger.chars().nth(x.start); + if let Some(nchar) = nchar { + c.starts_with(nchar) + }else{ + false + } } }) - .map(|x | MatchEntry{ - start: x.start+1, - count: x.count, - _match: &x._match + .map(|x | { + let new_start = if x.waiting_for_separator { + x.start // Avoid incrementing, we are only waiting for a separator + }else{ + x.start+1 // Increment, we want to check if the next char matches + }; + + MatchEntry{ + start: new_start, + count: x.count, + _match: &x._match, + waiting_for_separator: x.waiting_for_separator + } }) .collect(); @@ -113,9 +160,24 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let mut found_match = None; - for entry in combined_matches.iter() { - if entry.start == entry.count { - found_match = Some(entry._match); + for entry in combined_matches.iter_mut() { + let is_found_match = if entry.start == entry.count { + if !entry._match.word { + true + }else{ + if entry.waiting_for_separator { + true + }else{ + entry.waiting_for_separator = true; + false + } + } + }else{ + false + }; + + if is_found_match { + found_match = Some(entry.clone()); break; } } @@ -126,12 +188,27 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa current_set_queue.pop_front(); } - if let Some(_match) = found_match { + if let Some(match_entry) = found_match { if let Some(last) = current_set_queue.back_mut() { last.clear(); } - self.receiver.on_match(_match); + + let trailing_separator = if !match_entry.waiting_for_separator { + None + }else{ + let as_char = c.chars().nth(0); + match as_char { + Some(c) => { + Some(c) // Current char is the trailing separator + }, + None => {None}, + } + }; + + self.receiver.on_match(match_entry._match, trailing_separator); } + + *was_previous_word_separator = is_current_word_separator; } fn handle_modifier(&self, m: KeyModifier) { From 2e60042b2bcd42d0461bc9202725972883cf0cb5 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 21 Oct 2019 23:52:03 +0200 Subject: [PATCH 02/16] Improve newline support on windows for Word matches --- src/engine.rs | 11 +++++++++-- src/matcher/scrolling.rs | 12 +++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 72ad643..4376c52 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -150,9 +150,16 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa // If a trailing separator was counted in the match, add it back to the target string if let Some(trailing_separator) = trailing_separator { - target_string.push(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"); + match config.backend { BackendType::Inject => { // Send the expected string. On linux, newlines are managed automatically @@ -162,7 +169,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.keyboard_manager.send_string(&target_string); }else{ // To handle newlines, substitute each "\n" char with an Enter key press. - let splits = target_string.lines(); + let splits = target_string.split('\n'); for (i, split) in splits.enumerate() { if i > 0 { diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index b45b09c..e3f7a69 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -91,9 +91,6 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let is_current_word_separator = active_config.word_separators.contains( &c.chars().nth(0).unwrap_or_default() ); - if is_current_word_separator { - - } let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut(); @@ -104,6 +101,8 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa if !x.trigger.starts_with(c) { false }else{ + // If word option is true, a match can only be started if the previous + // char was a word separator if x.word { *was_previous_word_separator }else{ @@ -188,6 +187,8 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa current_set_queue.pop_front(); } + *was_previous_word_separator = is_current_word_separator; + if let Some(match_entry) = found_match { if let Some(last) = current_set_queue.back_mut() { last.clear(); @@ -196,6 +197,9 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let trailing_separator = if !match_entry.waiting_for_separator { None }else{ + // Force espanso to consider the previous char a word separator after a match + *was_previous_word_separator = true; + let as_char = c.chars().nth(0); match as_char { Some(c) => { @@ -207,8 +211,6 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa self.receiver.on_match(match_entry._match, trailing_separator); } - - *was_previous_word_separator = is_current_word_separator; } fn handle_modifier(&self, m: KeyModifier) { From 34ce0ee9b14c77e94c218916a62143b398067e5e Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 22 Oct 2019 20:41:33 +0200 Subject: [PATCH 03/16] Refactor matcher to support word separators --- src/matcher/mod.rs | 53 ++++++++++++++++++++ src/matcher/scrolling.rs | 105 ++++++++++++++------------------------- 2 files changed, 89 insertions(+), 69 deletions(-) diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index a806edc..772f732 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -34,6 +34,10 @@ pub struct Match { #[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, } impl <'de> serde::Deserialize<'de> for Match { @@ -54,12 +58,23 @@ impl<'a> From<&'a AutoMatch> for Match{ // 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(); + trigger_sequence.extend(trigger_chars.into_iter().map(|c| { + TriggerEntry::Char(c) + })); + if other.word { // If it's a word match, end with a word separator + trigger_sequence.push(TriggerEntry::WordSeparator); + } + Self { trigger: other.trigger.clone(), replace: other.replace.clone(), vars: other.vars.clone(), word: other.word.clone(), _has_vars: has_vars, + _trigger_sequence: trigger_sequence, } } } @@ -90,6 +105,12 @@ pub struct MatchVariable { pub params: Mapping, } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum TriggerEntry { + Char(char), + WordSeparator +} + pub trait MatchReceiver { fn on_match(&self, m: &Match, trailing_separator: Option); fn on_enable_update(&self, status: bool); @@ -155,4 +176,36 @@ mod tests { assert_eq!(_match._has_vars, true); } + + #[test] + fn test_match_trigger_sequence_without_word() { + let match_str = r###" + trigger: "test" + replace: "This is a test" + "###; + + let _match : Match = serde_yaml::from_str(match_str).unwrap(); + + assert_eq!(_match._trigger_sequence[0], TriggerEntry::Char('t')); + assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e')); + assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s')); + assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t')); + } + + #[test] + fn test_match_trigger_sequence_with_word() { + let match_str = r###" + trigger: "test" + replace: "This is a test" + word: true + "###; + + let _match : Match = serde_yaml::from_str(match_str).unwrap(); + + assert_eq!(_match._trigger_sequence[0], TriggerEntry::Char('t')); + assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e')); + assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s')); + assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t')); + assert_eq!(_match._trigger_sequence[4], TriggerEntry::WordSeparator); + } } \ No newline at end of file diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index e3f7a69..dad77c1 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use crate::matcher::{Match, MatchReceiver}; +use crate::matcher::{Match, MatchReceiver, TriggerEntry}; use std::cell::RefCell; use crate::event::{KeyModifier, ActionEventReceiver, ActionType}; use crate::config::ConfigManager; @@ -38,12 +38,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { struct MatchEntry<'a> { start: usize, count: usize, - _match: &'a Match, - - // Usually false, becomes true if the match was detected and has the "word" option. - // This is needed to trigger the replacement only if the next char is a - // word separator ( such as space ). - waiting_for_separator: bool, + _match: &'a Match } impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { @@ -74,6 +69,17 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { self.receiver.on_enable_update(*is_enabled); } + + fn is_matching(mtc: &Match, current_char: &str, start: usize, is_current_word_separator: bool) -> bool { + match mtc._trigger_sequence[start] { + TriggerEntry::Char(c) => { + current_char.starts_with(c) + }, + TriggerEntry::WordSeparator => { + is_current_word_separator + }, + } + } } impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> { @@ -98,56 +104,32 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let new_matches: Vec = active_config.matches.iter() .filter(|&x| { - if !x.trigger.starts_with(c) { - false - }else{ - // If word option is true, a match can only be started if the previous - // char was a word separator - if x.word { - *was_previous_word_separator - }else{ - true - } + let mut result = Self::is_matching(x, c, 0, is_current_word_separator); + + if x.word { + result = result && *was_previous_word_separator } + + result }) .map(|x | MatchEntry{ start: 1, - count: x.trigger.chars().count(), - _match: &x, - waiting_for_separator: false + count: x._trigger_sequence.len(), + _match: &x }) .collect(); // TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup. - let mut combined_matches: Vec = match current_set_queue.back_mut() { + let combined_matches: Vec = match current_set_queue.back_mut() { Some(last_matches) => { let mut updated: Vec = last_matches.iter() .filter(|&x| { - if x.waiting_for_separator { - // The match is only waiting for a separator to call the replacement - is_current_word_separator - }else{ - let nchar = x._match.trigger.chars().nth(x.start); - if let Some(nchar) = nchar { - c.starts_with(nchar) - }else{ - false - } - } + Self::is_matching(x._match, c, x.start, is_current_word_separator) }) - .map(|x | { - let new_start = if x.waiting_for_separator { - x.start // Avoid incrementing, we are only waiting for a separator - }else{ - x.start+1 // Increment, we want to check if the next char matches - }; - - MatchEntry{ - start: new_start, - count: x.count, - _match: &x._match, - waiting_for_separator: x.waiting_for_separator - } + .map(|x | MatchEntry{ + start: x.start+1, + count: x.count, + _match: &x._match }) .collect(); @@ -159,24 +141,9 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let mut found_match = None; - for entry in combined_matches.iter_mut() { - let is_found_match = if entry.start == entry.count { - if !entry._match.word { - true - }else{ - if entry.waiting_for_separator { - true - }else{ - entry.waiting_for_separator = true; - false - } - } - }else{ - false - }; - - if is_found_match { - found_match = Some(entry.clone()); + for entry in combined_matches.iter() { + if entry.start == entry.count { + found_match = Some(entry._match); break; } } @@ -189,17 +156,14 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa *was_previous_word_separator = is_current_word_separator; - if let Some(match_entry) = found_match { + if let Some(mtc) = found_match { if let Some(last) = current_set_queue.back_mut() { last.clear(); } - let trailing_separator = if !match_entry.waiting_for_separator { + let trailing_separator = if !is_current_word_separator { None }else{ - // Force espanso to consider the previous char a word separator after a match - *was_previous_word_separator = true; - let as_char = c.chars().nth(0); match as_char { Some(c) => { @@ -209,7 +173,10 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa } }; - self.receiver.on_match(match_entry._match, trailing_separator); + // Force espanso to consider the last char as a separator + *was_previous_word_separator = true; + + self.receiver.on_match(mtc, trailing_separator); } } From bfee5a0a30b789088216022c65115c20c7c60cba Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 22 Oct 2019 20:53:25 +0200 Subject: [PATCH 04/16] Version bump 0.3.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 24c361d..0419878 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "0.3.1" +version = "0.3.2" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" From ea7e92e087d86873c392352471a944c788e58d7d Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 22 Oct 2019 20:54:44 +0200 Subject: [PATCH 05/16] Version bump 0.3.2 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4d46f31..ad44ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,7 +370,7 @@ dependencies = [ [[package]] name = "espanso" -version = "0.3.1" +version = "0.3.2" 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)", From 5f71d0ad2483dfb366aff4a069246fd95b1a7793 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 22 Oct 2019 21:43:40 +0200 Subject: [PATCH 06/16] Fix bug that prevented the user to type accents correctly on some keyboard layouts. Fix #86 --- native/libwinbridge/bridge.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index dfe6108..f7fa9e7 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -243,7 +243,12 @@ LRESULT CALLBACK window_procedure(HWND window, unsigned int msg, WPARAM wp, LPAR if (GetKeyboardState(lpKeyState.data())) { // Convert the virtual key to an unicode char std::array buffer; - int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), buffer.data(), buffer.size(), 0, currentKeyboardLayout); + + // This flag is needed to avoid chaning the keyboard state for some layouts. + // Refer to issue: https://github.com/federico-terzi/espanso/issues/86 + UINT flags = 1 << 2; + + int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), buffer.data(), buffer.size(), flags, currentKeyboardLayout); //std::cout << result << " " << buffer[0] << " " << raw->data.keyboard.VKey << std::endl; From 9bbdb8d95f78be7def6e906797d4ddbb9292cce1 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 22 Oct 2019 23:03:58 +0200 Subject: [PATCH 07/16] First draft of Cursor Position hint on Windows --- native/libwinbridge/bridge.cpp | 40 ++++++++-------- native/libwinbridge/bridge.h | 5 ++ src/bridge/windows.rs | 1 + src/engine.rs | 7 +++ src/keyboard/mod.rs | 1 + src/keyboard/windows.rs | 7 +++ src/matcher/mod.rs | 83 +++++++++++++++++++++++++++++++++- 7 files changed, 125 insertions(+), 19 deletions(-) diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index f7fa9e7..309395c 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -453,24 +453,7 @@ void send_string(const wchar_t * string) { * Send the backspace keypress, *count* times. */ void delete_string(int32_t count) { - std::vector vec; - - for (int i = 0; i < count; i++) { - INPUT input = { 0 }; - - input.type = INPUT_KEYBOARD; - input.ki.wScan = 0; - input.ki.time = 0; - input.ki.dwExtraInfo = 0; - input.ki.wVk = VK_BACK; - input.ki.dwFlags = 0; // 0 for key press - vec.push_back(input); - - input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release - vec.push_back(input); - } - - SendInput(vec.size(), vec.data(), sizeof(INPUT)); + send_multi_vkey(VK_BACK, count); } void send_vkey(int32_t vk) { @@ -492,6 +475,27 @@ void send_vkey(int32_t vk) { SendInput(vec.size(), vec.data(), sizeof(INPUT)); } +void send_multi_vkey(int32_t vk, int32_t count) { + std::vector vec; + + for (int i = 0; i < count; i++) { + INPUT input = { 0 }; + + input.type = INPUT_KEYBOARD; + input.ki.wScan = 0; + input.ki.time = 0; + input.ki.dwExtraInfo = 0; + input.ki.wVk = vk; + input.ki.dwFlags = 0; // 0 for key press + vec.push_back(input); + + input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release + vec.push_back(input); + } + + SendInput(vec.size(), vec.data(), sizeof(INPUT)); +} + void trigger_paste() { std::vector vec; diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h index fc0a80c..85d86ad 100644 --- a/native/libwinbridge/bridge.h +++ b/native/libwinbridge/bridge.h @@ -64,6 +64,11 @@ extern "C" void send_string(const wchar_t * string); */ extern "C" void send_vkey(int32_t vk); +/* + * Send the given Virtual Key press multiple times + */ +extern "C" void send_multi_vkey(int32_t vk, int32_t count); + /* * Send the backspace keypress, *count* times. */ diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs index 349230c..db9833e 100644 --- a/src/bridge/windows.rs +++ b/src/bridge/windows.rs @@ -55,6 +55,7 @@ extern { pub fn eventloop(); pub fn send_string(string: *const u16); pub fn send_vkey(vk: i32); + pub fn send_multi_vkey(vk: i32, count: i32); pub fn delete_string(count: i32); pub fn trigger_paste(); } \ No newline at end of file diff --git a/src/engine.rs b/src/engine.rs index 4376c52..458fc7e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -185,6 +185,13 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.keyboard_manager.trigger_paste(); }, } + + // Cursor Hint + if let Some(cursor_rewind) = m._cursor_rewind { + // Simulate left arrow key presses to bring the cursor into the desired position + + self.keyboard_manager.move_cursor_left(cursor_rewind); + } } fn on_enable_update(&self, status: bool) { diff --git a/src/keyboard/mod.rs b/src/keyboard/mod.rs index c48134c..5a9cc5b 100644 --- a/src/keyboard/mod.rs +++ b/src/keyboard/mod.rs @@ -31,6 +31,7 @@ pub trait KeyboardManager { fn send_enter(&self); fn trigger_paste(&self); fn delete_string(&self, count: i32); + fn move_cursor_left(&self, count: i32); } // WINDOWS IMPLEMENTATION diff --git a/src/keyboard/windows.rs b/src/keyboard/windows.rs index df3564d..8193006 100644 --- a/src/keyboard/windows.rs +++ b/src/keyboard/windows.rs @@ -55,4 +55,11 @@ impl super::KeyboardManager for WindowsKeyboardManager { delete_string(count) } } + + fn move_cursor_left(&self, count: i32) { + unsafe { + // Send the left arrow key multiple times + send_multi_vkey(0x25, count) + } + } } \ No newline at end of file diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 772f732..5eb1240 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -38,6 +38,11 @@ pub struct Match { // Automatically calculated from the trigger, used by the matcher to check for correspondences. #[serde(skip_serializing)] pub _trigger_sequence: Vec, + + // If a cursor position hint is present, this value contains the amount of "left" moves necessary + // to arrive to the target point + #[serde(skip_serializing)] + pub _cursor_rewind: Option, } impl <'de> serde::Deserialize<'de> for Match { @@ -55,6 +60,33 @@ impl<'a> From<&'a AutoMatch> for Match{ 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 mut new_replace = other.replace.clone(); + + // Check if the replace result contains a Cursor Hint + let cursor_rewind = if other.replace.contains("{{|}}") { + let index = other.replace.find("{{|}}"); + if let Some(index) = index { + // Convert the byte index to a char index + let char_str = &other.replace[0..index]; + let char_index = char_str.chars().count(); + let total_size = other.replace.chars().count(); + + // Remove the {{|}} placeholder + new_replace = other.replace.replace("{{|}}", ""); + + // Calculate the amount of rewind moves needed (LEFT ARROW). + // Subtract also 5, equal to the number of chars of the placeholder "{{|}}" + let moves = (total_size - char_index - 5) as i32; + Some(moves) + }else{ + None + } + }else{ + None + }; + // Check if the match contains variables let has_vars = VAR_REGEX.is_match(&other.replace); @@ -70,11 +102,12 @@ impl<'a> From<&'a AutoMatch> for Match{ Self { trigger: other.trigger.clone(), - replace: other.replace.clone(), + replace: new_replace, vars: other.vars.clone(), word: other.word.clone(), _has_vars: has_vars, _trigger_sequence: trigger_sequence, + _cursor_rewind: cursor_rewind, } } } @@ -208,4 +241,52 @@ mod tests { assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t')); assert_eq!(_match._trigger_sequence[4], TriggerEntry::WordSeparator); } + + #[test] + fn test_match_cursor_hint_not_present() { + let match_str = r###" + trigger: "test" + replace: "This is a test" + "###; + + let _match : Match = serde_yaml::from_str(match_str).unwrap(); + + assert!(_match._cursor_rewind.is_none()); + } + + #[test] + fn test_match_cursor_hint_should_be_removed() { + let match_str = r###" + trigger: "test" + replace: "Testing the {{|}} cursor position" + "###; + + let _match : Match = serde_yaml::from_str(match_str).unwrap(); + + assert_eq!(_match.replace, "Testing the cursor position"); + } + + #[test] + fn test_match_cursor_rewind_single_line() { + let match_str = r###" + trigger: "test" + replace: "Testing the {{|}} cursor position" + "###; + + let _match : Match = serde_yaml::from_str(match_str).unwrap(); + + assert_eq!(_match._cursor_rewind.unwrap(), 16); + } + + #[test] + fn test_match_cursor_rewind_multiple_line() { + let match_str = r###" + trigger: "test" + replace: "Testing the \n{{|}}\n cursor position" + "###; + + let _match : Match = serde_yaml::from_str(match_str).unwrap(); + + assert_eq!(_match._cursor_rewind.unwrap(), 17); + } } \ No newline at end of file From 4f9be699acd19f8613b2c6594cefb7559ea34af1 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 23 Oct 2019 19:13:21 +0200 Subject: [PATCH 08/16] Add cursor position implementation for MacOS --- native/libmacbridge/bridge.h | 5 +++++ native/libmacbridge/bridge.mm | 42 +++++++++++++++++++---------------- src/bridge/macos.rs | 1 + src/keyboard/macos.rs | 7 ++++++ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h index 8f414d9..b3bc3d1 100644 --- a/native/libmacbridge/bridge.h +++ b/native/libmacbridge/bridge.h @@ -60,6 +60,11 @@ void send_string(const char * string); */ void send_vkey(int32_t vk); +/* + * Send the Virtual Key press multiple times + */ +void send_multi_vkey(int32_t vk, int32_t count); + /* * Send the backspace keypress, *count* times. */ diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm index 361bfa3..d55ede4 100644 --- a/native/libmacbridge/bridge.mm +++ b/native/libmacbridge/bridge.mm @@ -101,23 +101,7 @@ void send_string(const char * string) { } void delete_string(int32_t count) { - dispatch_async(dispatch_get_main_queue(), ^(void) { - for (int i = 0; i < count; i++) { - CGEventRef keydown; - keydown = CGEventCreateKeyboardEvent(NULL, 0x33, true); - CGEventPost(kCGHIDEventTap, keydown); - CFRelease(keydown); - - usleep(2000); - - CGEventRef keyup; - keyup = CGEventCreateKeyboardEvent(NULL, 0x33, false); - CGEventPost(kCGHIDEventTap, keyup); - CFRelease(keyup); - - usleep(2000); - } - }); + send_multi_vkey(0x33, count); } void send_vkey(int32_t vk) { @@ -127,14 +111,34 @@ void send_vkey(int32_t vk) { CGEventPost(kCGHIDEventTap, keydown); CFRelease(keydown); - usleep(2000); + usleep(500); CGEventRef keyup; keyup = CGEventCreateKeyboardEvent(NULL, vk, false); CGEventPost(kCGHIDEventTap, keyup); CFRelease(keyup); - usleep(2000); + usleep(500); + }); +} + +void send_multi_vkey(int32_t vk, int32_t count) { + dispatch_async(dispatch_get_main_queue(), ^(void) { + for (int i = 0; i < count; i++) { + CGEventRef keydown; + keydown = CGEventCreateKeyboardEvent(NULL, vk, true); + CGEventPost(kCGHIDEventTap, keydown); + CFRelease(keydown); + + usleep(500); + + CGEventRef keyup; + keyup = CGEventCreateKeyboardEvent(NULL, vk, false); + CGEventPost(kCGHIDEventTap, keyup); + CFRelease(keyup); + + usleep(500); + } }); } diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs index 830e994..b229928 100644 --- a/src/bridge/macos.rs +++ b/src/bridge/macos.rs @@ -54,6 +54,7 @@ extern { pub fn send_string(string: *const c_char); pub fn send_vkey(vk: i32); + pub fn send_multi_vkey(vk: i32, count: i32); pub fn delete_string(count: i32); pub fn trigger_paste(); } \ No newline at end of file diff --git a/src/keyboard/macos.rs b/src/keyboard/macos.rs index 7381dc1..724845a 100644 --- a/src/keyboard/macos.rs +++ b/src/keyboard/macos.rs @@ -48,4 +48,11 @@ impl super::KeyboardManager for MacKeyboardManager { fn delete_string(&self, count: i32) { unsafe {delete_string(count)} } + + fn move_cursor_left(&self, count: i32) { + unsafe { + // Simulate the Left arrow count times + send_multi_vkey(0x7B, count); + } + } } \ No newline at end of file From 0c8812eb028340227bec9a135e0dd997236b3bf5 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 23 Oct 2019 19:26:06 +0200 Subject: [PATCH 09/16] Improve word matches support on macOS --- src/matcher/scrolling.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index dad77c1..330a01a 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -94,10 +94,17 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let active_config = self.config_manager.active_config(); // Check if the current char is a word separator - let is_current_word_separator = active_config.word_separators.contains( + let mut is_current_word_separator = active_config.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_word_separator = self.was_previous_char_word_separator.borrow_mut(); let mut current_set_queue = self.current_set_queue.borrow_mut(); From 1a3e2c711186cc2fd49149ac50dd29e093ad83b9 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 23 Oct 2019 20:17:38 +0200 Subject: [PATCH 10/16] Attempt to fix #98 --- src/context/macos.rs | 4 ++-- src/res/mac/icon@2x.png | Bin 0 -> 1395 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/res/mac/icon@2x.png diff --git a/src/context/macos.rs b/src/context/macos.rs index 1f46a50..3c8fafc 100644 --- a/src/context/macos.rs +++ b/src/context/macos.rs @@ -27,7 +27,7 @@ use std::fs; use log::{info, error}; use std::process::exit; -const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png"); +const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon@2x.png"); pub struct MacContext { pub send_channel: Sender @@ -53,7 +53,7 @@ impl MacContext { // Initialize the status icon path let espanso_dir = super::get_data_dir(); - let status_icon_target = espanso_dir.join("icon.png"); + let status_icon_target = espanso_dir.join("icon@2x.png"); if status_icon_target.exists() { info!("Status icon already initialized, skipping."); diff --git a/src/res/mac/icon@2x.png b/src/res/mac/icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..899084d2dc166913d0c247f302690eeac5a3ea71 GIT binary patch literal 1395 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofy`glX(f`a29w(7Bet# z3xhBt!>lP? zWt5Z@Sn2DRmzV368|&p4rRy77T3YHG80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;z zSx}OhpQivaH!&%{w8U0P31kr*K;4;J0JkWw80ssa|MV*o3-k^34D_*SD#=VkI1fb^ zNCu(}>???6Ho%~oq=sB5lWKw@CN86int66l`S@+}q^R?d5LL5~U*5$<^w#D4_d=$f8RfUP^*Kh27B= zN2~=qzrX&Uwtjx?xj7H#6uTE2YybRyD|_zV>rub&_s{f8*GXnelVmPV+adStJg>kj z^lgn{pnfPwKw54~-S;QHXPB5YBUzy`;YD!9ugX9@rraOax2=UD1p?1`OkT1| zV#Z6(dus9J$7Y+(Xi983x>d;k$(ufeztUPD=@>9g^yAqFE+kU$9{uOYJP~Bv{@_xwU=Q(!;-f@XM zj+mz$9p$$n1>5)BO*Ya*&F=xvQ-6-ipCSPZ4Xs`85*uASMIdDhnL@Sw3 za}QNF9*(&ewDVP`aLkntM$R`ZcNE^6b*Db`pD5F#ijz{8u1Rj$lceWa{3_D&!P=Lc z=B?U?Y8e&zUH=FF3ls1Z6ngq7DdEYU4?z#yOQL27&Epc=*5i~V`q=aEmhQc47)2gB zzy01mJM8GZ=T8fZ&5Hj!+z+pQ;QXvYrqA-DT7xjdNsk7Cwp* zSQVElo^W=j^{l67Z=SnzG;gh=Ve6tBd?{Nuc;C=lBf8_8!55(S^SJk1`?K=%d%4!W zm<41>euScXAGVf=&wngD*K Date: Wed, 23 Oct 2019 20:55:54 +0200 Subject: [PATCH 11/16] Fix bug that prevented espanso detect from working correctly on macOS. Fix #91 --- native/libmacbridge/bridge.h | 5 ++++ native/libmacbridge/bridge.mm | 6 +++++ src/bridge/macos.rs | 1 + src/main.rs | 47 +++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h index 8f414d9..bd01c30 100644 --- a/native/libmacbridge/bridge.h +++ b/native/libmacbridge/bridge.h @@ -37,6 +37,11 @@ int32_t initialize(void * context, const char * icon_path); */ int32_t eventloop(); +/* + * Initialize the application and start the headless eventloop, used for the espanso detect command + */ +int32_t headless_eventloop(); + /* * Called when a new keypress is made, the first argument is an char array, * while the second is the size of the array. diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm index 361bfa3..d5ea434 100644 --- a/native/libmacbridge/bridge.mm +++ b/native/libmacbridge/bridge.mm @@ -64,6 +64,12 @@ int32_t eventloop() { [NSApp run]; } +int32_t headless_eventloop() { + NSApplication * application = [NSApplication sharedApplication]; + [NSApp run]; + return 0; +} + void send_string(const char * string) { char * stringCopy = strdup(string); dispatch_async(dispatch_get_main_queue(), ^(void) { diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs index 830e994..40f2ee8 100644 --- a/src/bridge/macos.rs +++ b/src/bridge/macos.rs @@ -31,6 +31,7 @@ pub struct MacMenuItem { extern { pub fn initialize(s: *const c_void, icon_path: *const c_char); pub fn eventloop(); + pub fn headless_eventloop(); // System pub fn check_accessibility() -> i32; diff --git a/src/main.rs b/src/main.rs index 6718ee7..566b8a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -530,6 +530,7 @@ fn restart_main(config_set: ConfigSet) { /// Cli tool used to analyze active windows to extract useful information /// to create configuration filters. +#[cfg(not(target_os = "macos"))] fn detect_main() { let system_manager = system::get_manager(); @@ -562,6 +563,52 @@ fn detect_main() { } } +/// Cli tool used to analyze active windows to extract useful information +/// to create configuration filters. +/// On macOS version we need to start an event loop for the app to register changes. +#[cfg(target_os = "macos")] +fn detect_main() { + thread::spawn(|| { + use std::io::Write; + use std::io::stdout; + + let system_manager = system::get_manager(); + + println!("Listening for changes, now focus the window you want to analyze."); + println!("Warning: stay on the window for a few seconds, as it may take a while to register."); + println!("You can terminate with CTRL+C\n"); + + let mut last_title : String = "".to_owned(); + let mut last_class : String = "".to_owned(); + let mut last_exec : String = "".to_owned(); + + loop { + let curr_title = system_manager.get_current_window_title().unwrap_or_default(); + let curr_class = system_manager.get_current_window_class().unwrap_or_default(); + let curr_exec = system_manager.get_current_window_executable().unwrap_or_default(); + + // Check if a change occurred + if curr_title != last_title || curr_class != last_class || curr_exec != last_exec { + println!("Detected change, current window has properties:"); + println!("==> Title: '{}'", curr_title); + println!("==> Class: '{}'", curr_class); + println!("==> Executable: '{}'", curr_exec); + println!(); + } + + last_title = curr_title; + last_class = curr_class; + last_exec = curr_exec; + + thread::sleep(Duration::from_millis(500)); + } + }); + + unsafe { + crate::bridge::macos::headless_eventloop(); + } +} + /// Send the given command to the espanso daemon fn cmd_main(config_set: ConfigSet, matches: &ArgMatches) { let command = if matches.subcommand_matches("exit").is_some() { From 8afacf4efb4e75d6709664c83b1f45532fa11842 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Thu, 24 Oct 2019 18:48:55 +0200 Subject: [PATCH 12/16] Refactor cursor hint to support dynamic matches --- src/engine.rs | 25 ++++++++++++--- src/matcher/mod.rs | 79 +--------------------------------------------- 2 files changed, 22 insertions(+), 82 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 458fc7e..bc604c1 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -160,6 +160,25 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa // 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 @@ -186,11 +205,9 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa }, } - // Cursor Hint - if let Some(cursor_rewind) = m._cursor_rewind { + 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(cursor_rewind); + self.keyboard_manager.move_cursor_left(moves); } } diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 5eb1240..4667318 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -38,11 +38,6 @@ pub struct Match { // Automatically calculated from the trigger, used by the matcher to check for correspondences. #[serde(skip_serializing)] pub _trigger_sequence: Vec, - - // If a cursor position hint is present, this value contains the amount of "left" moves necessary - // to arrive to the target point - #[serde(skip_serializing)] - pub _cursor_rewind: Option, } impl <'de> serde::Deserialize<'de> for Match { @@ -62,30 +57,7 @@ impl<'a> From<&'a AutoMatch> for Match{ // TODO: may need to replace windows newline (\r\n) with newline only (\n) - let mut new_replace = other.replace.clone(); - - // Check if the replace result contains a Cursor Hint - let cursor_rewind = if other.replace.contains("{{|}}") { - let index = other.replace.find("{{|}}"); - if let Some(index) = index { - // Convert the byte index to a char index - let char_str = &other.replace[0..index]; - let char_index = char_str.chars().count(); - let total_size = other.replace.chars().count(); - - // Remove the {{|}} placeholder - new_replace = other.replace.replace("{{|}}", ""); - - // Calculate the amount of rewind moves needed (LEFT ARROW). - // Subtract also 5, equal to the number of chars of the placeholder "{{|}}" - let moves = (total_size - char_index - 5) as i32; - Some(moves) - }else{ - None - } - }else{ - None - }; + let new_replace = other.replace.clone(); // Check if the match contains variables let has_vars = VAR_REGEX.is_match(&other.replace); @@ -107,7 +79,6 @@ impl<'a> From<&'a AutoMatch> for Match{ word: other.word.clone(), _has_vars: has_vars, _trigger_sequence: trigger_sequence, - _cursor_rewind: cursor_rewind, } } } @@ -241,52 +212,4 @@ mod tests { assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t')); assert_eq!(_match._trigger_sequence[4], TriggerEntry::WordSeparator); } - - #[test] - fn test_match_cursor_hint_not_present() { - let match_str = r###" - trigger: "test" - replace: "This is a test" - "###; - - let _match : Match = serde_yaml::from_str(match_str).unwrap(); - - assert!(_match._cursor_rewind.is_none()); - } - - #[test] - fn test_match_cursor_hint_should_be_removed() { - let match_str = r###" - trigger: "test" - replace: "Testing the {{|}} cursor position" - "###; - - let _match : Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match.replace, "Testing the cursor position"); - } - - #[test] - fn test_match_cursor_rewind_single_line() { - let match_str = r###" - trigger: "test" - replace: "Testing the {{|}} cursor position" - "###; - - let _match : Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match._cursor_rewind.unwrap(), 16); - } - - #[test] - fn test_match_cursor_rewind_multiple_line() { - let match_str = r###" - trigger: "test" - replace: "Testing the \n{{|}}\n cursor position" - "###; - - let _match : Match = serde_yaml::from_str(match_str).unwrap(); - - assert_eq!(_match._cursor_rewind.unwrap(), 17); - } } \ No newline at end of file From 888a0f32ec06f426e7cb909b9a5b083597cdad91 Mon Sep 17 00:00:00 2001 From: Timo Runge Date: Thu, 24 Oct 2019 21:43:34 +0200 Subject: [PATCH 13/16] High res status icon for macOS. Fix #98. --- src/context/macos.rs | 6 +++--- src/res/mac/icon.png | Bin 239 -> 4207 bytes src/res/mac/icon@2x.png | Bin 1395 -> 0 bytes 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 src/res/mac/icon@2x.png diff --git a/src/context/macos.rs b/src/context/macos.rs index 3c8fafc..6c66ed3 100644 --- a/src/context/macos.rs +++ b/src/context/macos.rs @@ -27,7 +27,7 @@ use std::fs; use log::{info, error}; use std::process::exit; -const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon@2x.png"); +const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png"); pub struct MacContext { pub send_channel: Sender @@ -53,7 +53,7 @@ impl MacContext { // Initialize the status icon path let espanso_dir = super::get_data_dir(); - let status_icon_target = espanso_dir.join("icon@2x.png"); + let status_icon_target = espanso_dir.join("icon.png"); if status_icon_target.exists() { info!("Status icon already initialized, skipping."); @@ -142,4 +142,4 @@ extern fn context_menu_click_callback(_self: *mut c_void, id: i32) { let event = Event::Action(ActionType::from(id)); (*_self).send_channel.send(event).unwrap(); } -} \ No newline at end of file +} diff --git a/src/res/mac/icon.png b/src/res/mac/icon.png index 04d9593a33bad04d0cf2d2bdc3d95a47790a895b..cde3e917153f320d197f02c8ae513cc482e96ef9 100644 GIT binary patch literal 4207 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 Z>HRAYjI(FswRwNdRu*>V6?+IV{{jD#`Bwk{ literal 239 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G6FprVLn>}1B}jZcz}R(?iOa!8zT^Mmhjl@`mHkHrB>WVv zE%@s(>zs3}Wbd2{?>=prUL%ZB^Td)v+&m$K8bV3K09bNI-n zV8?NUdqK^j163;GYmN)mxim9vIndVX&ZNo6%f!TT=|WvlOlrg74c~X{Iqq>FmCr*; k-huh=;R_ma3`{%>mBk*@9r}Ex0UgKS>FVdQ&MBb@01jDGDF6Tf diff --git a/src/res/mac/icon@2x.png b/src/res/mac/icon@2x.png deleted file mode 100644 index 899084d2dc166913d0c247f302690eeac5a3ea71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1395 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofy`glX(f`a29w(7Bet# z3xhBt!>lP? zWt5Z@Sn2DRmzV368|&p4rRy77T3YHG80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;z zSx}OhpQivaH!&%{w8U0P31kr*K;4;J0JkWw80ssa|MV*o3-k^34D_*SD#=VkI1fb^ zNCu(}>???6Ho%~oq=sB5lWKw@CN86int66l`S@+}q^R?d5LL5~U*5$<^w#D4_d=$f8RfUP^*Kh27B= zN2~=qzrX&Uwtjx?xj7H#6uTE2YybRyD|_zV>rub&_s{f8*GXnelVmPV+adStJg>kj z^lgn{pnfPwKw54~-S;QHXPB5YBUzy`;YD!9ugX9@rraOax2=UD1p?1`OkT1| zV#Z6(dus9J$7Y+(Xi983x>d;k$(ufeztUPD=@>9g^yAqFE+kU$9{uOYJP~Bv{@_xwU=Q(!;-f@XM zj+mz$9p$$n1>5)BO*Ya*&F=xvQ-6-ipCSPZ4Xs`85*uASMIdDhnL@Sw3 za}QNF9*(&ewDVP`aLkntM$R`ZcNE^6b*Db`pD5F#ijz{8u1Rj$lceWa{3_D&!P=Lc z=B?U?Y8e&zUH=FF3ls1Z6ngq7DdEYU4?z#yOQL27&Epc=*5i~V`q=aEmhQc47)2gB zzy01mJM8GZ=T8fZ&5Hj!+z+pQ;QXvYrqA-DT7xjdNsk7Cwp* zSQVElo^W=j^{l67Z=SnzG;gh=Ve6tBd?{Nuc;C=lBf8_8!55(S^SJk1`?K=%d%4!W zm<41>euScXAGVf=&wngD*K Date: Fri, 25 Oct 2019 22:07:51 +0200 Subject: [PATCH 14/16] Add Terminator to Linux filter. Fix #102 --- native/liblinuxbridge/bridge.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 55d1c99..fdaa432 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -446,6 +446,8 @@ int32_t is_current_window_terminal() { return 1; }else if (strstr(class_buffer, "konsole") != NULL) { // KDE Konsole return 1; + }else if (strstr(class_buffer, "Terminator") != NULL) { // Terminator + return 1; } } From 433bd4253f8f5b6bd39d74141b09ffc53fcf1619 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 25 Oct 2019 22:22:51 +0200 Subject: [PATCH 15/16] Improve readme with credits --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 22abe01..1df359f 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Many people helped the project along the way, thanks to all of you. In particula * [Scrumplex](https://scrumplex.net/) - Official AUR repo mantainer and Linux Guru * [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 ## Remarks From b63e2b2592daebd746df924fab48407fb3dcabbb Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 25 Oct 2019 22:34:31 +0200 Subject: [PATCH 16/16] Add cursor position implementation on Linux --- native/liblinuxbridge/bridge.cpp | 8 +++++++- native/liblinuxbridge/bridge.h | 5 +++++ src/bridge/linux.rs | 1 + src/keyboard/linux.rs | 6 ++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 55d1c99..750c28f 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -278,6 +278,12 @@ void delete_string(int32_t count) { } } +void left_arrow(int32_t count) { + for (int i = 0; i