Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
		
						commit
						9139c9b3b4
					
				|  | @ -307,6 +307,14 @@ void trigger_alt_shift_ins_paste() { | ||||||
|     xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Alt+Insert", 8000); |     xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Alt+Insert", 8000); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void trigger_copy() { | ||||||
|  |     // Release the other keys, for an explanation, read the 'trigger_paste' method
 | ||||||
|  | 
 | ||||||
|  |     xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Alt", 8000); | ||||||
|  |     xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift", 8000); | ||||||
|  |     xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+c", 8000); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // SYSTEM MODULE
 | // SYSTEM MODULE
 | ||||||
| 
 | 
 | ||||||
| // Function taken from the wmlib tool source code
 | // Function taken from the wmlib tool source code
 | ||||||
|  |  | ||||||
|  | @ -92,6 +92,10 @@ extern "C" void trigger_shift_ins_paste(); | ||||||
|  */ |  */ | ||||||
| extern "C" void trigger_alt_shift_ins_paste(); | extern "C" void trigger_alt_shift_ins_paste(); | ||||||
| 
 | 
 | ||||||
|  | /*
 | ||||||
|  |  * Trigger copy shortcut ( Pressing CTRL+C ) | ||||||
|  |  */ | ||||||
|  | extern "C" void trigger_copy(); | ||||||
| 
 | 
 | ||||||
| // SYSTEM MODULE
 | // SYSTEM MODULE
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -80,6 +80,11 @@ void delete_string(int32_t count); | ||||||
|  */ |  */ | ||||||
| void trigger_paste(); | void trigger_paste(); | ||||||
| 
 | 
 | ||||||
|  | /*
 | ||||||
|  |  * Trigger normal copy ( Pressing CMD+C ) | ||||||
|  |  */ | ||||||
|  | void trigger_copy(); | ||||||
|  | 
 | ||||||
| // UI
 | // UI
 | ||||||
| 
 | 
 | ||||||
| /*
 | /*
 | ||||||
|  |  | ||||||
|  | @ -101,6 +101,14 @@ void send_string(const char * string) { | ||||||
| 
 | 
 | ||||||
|             usleep(2000); |             usleep(2000); | ||||||
| 
 | 
 | ||||||
|  |             // Some applications require an explicit release of the space key | ||||||
|  |             // For more information: https://github.com/federico-terzi/espanso/issues/159 | ||||||
|  |             CGEventRef e2 = CGEventCreateKeyboardEvent(NULL, 0x31, false); | ||||||
|  |             CGEventPost(kCGHIDEventTap, e2); | ||||||
|  |             CFRelease(e2); | ||||||
|  | 
 | ||||||
|  |             usleep(2000); | ||||||
|  | 
 | ||||||
|             i += chunk_size; |             i += chunk_size; | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  | @ -180,6 +188,39 @@ void trigger_paste() { | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | void trigger_copy() { | ||||||
|  |     dispatch_async(dispatch_get_main_queue(), ^(void) { | ||||||
|  |         CGEventRef keydown; | ||||||
|  |         keydown = CGEventCreateKeyboardEvent(NULL, 0x37, true);  // CMD | ||||||
|  |         CGEventPost(kCGHIDEventTap, keydown); | ||||||
|  |         CFRelease(keydown); | ||||||
|  | 
 | ||||||
|  |         usleep(2000); | ||||||
|  | 
 | ||||||
|  |         CGEventRef keydown2; | ||||||
|  |         keydown2 = CGEventCreateKeyboardEvent(NULL, 0x08, true);  // C key | ||||||
|  |         CGEventPost(kCGHIDEventTap, keydown2); | ||||||
|  |         CFRelease(keydown2); | ||||||
|  | 
 | ||||||
|  |         usleep(2000); | ||||||
|  | 
 | ||||||
|  |         CGEventRef keyup; | ||||||
|  |         keyup = CGEventCreateKeyboardEvent(NULL, 0x08, false); | ||||||
|  |         CGEventPost(kCGHIDEventTap, keyup); | ||||||
|  |         CFRelease(keyup); | ||||||
|  | 
 | ||||||
|  |         usleep(2000); | ||||||
|  | 
 | ||||||
|  |         CGEventRef keyup2; | ||||||
|  |         keyup2 = CGEventCreateKeyboardEvent(NULL, 0x37, false);  // CMD | ||||||
|  |         CGEventPost(kCGHIDEventTap, keyup2); | ||||||
|  |         CFRelease(keyup2); | ||||||
|  | 
 | ||||||
|  |         usleep(2000); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| int32_t get_active_app_bundle(char * buffer, int32_t size) { | int32_t get_active_app_bundle(char * buffer, int32_t size) { | ||||||
|     NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; |     NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; | ||||||
|     NSString *bundlePath = [frontApp bundleURL].path; |     NSString *bundlePath = [frontApp bundleURL].path; | ||||||
|  | @ -291,4 +332,5 @@ int32_t prompt_accessibility() { | ||||||
| void open_settings_panel() { | void open_settings_panel() { | ||||||
|     NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"; |     NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"; | ||||||
|     [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; |     [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -524,6 +524,31 @@ void trigger_paste() { | ||||||
|     SendInput(vec.size(), vec.data(), sizeof(INPUT)); |     SendInput(vec.size(), vec.data(), sizeof(INPUT)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void trigger_copy() { | ||||||
|  |     std::vector<INPUT> vec; | ||||||
|  | 
 | ||||||
|  |     INPUT input = { 0 }; | ||||||
|  | 
 | ||||||
|  |     input.type = INPUT_KEYBOARD; | ||||||
|  |     input.ki.wScan = 0; | ||||||
|  |     input.ki.time = 0; | ||||||
|  |     input.ki.dwExtraInfo = 0; | ||||||
|  |     input.ki.wVk = VK_CONTROL; | ||||||
|  |     input.ki.dwFlags = 0; // 0 for key press
 | ||||||
|  |     vec.push_back(input); | ||||||
|  | 
 | ||||||
|  |     input.ki.wVk = 0x43;  // C KEY
 | ||||||
|  |     vec.push_back(input); | ||||||
|  | 
 | ||||||
|  |     input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
 | ||||||
|  |     vec.push_back(input); | ||||||
|  | 
 | ||||||
|  |     input.ki.wVk = VK_CONTROL; | ||||||
|  |     vec.push_back(input); | ||||||
|  | 
 | ||||||
|  |     SendInput(vec.size(), vec.data(), sizeof(INPUT)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| // SYSTEM
 | // SYSTEM
 | ||||||
| 
 | 
 | ||||||
|  | @ -699,3 +724,4 @@ int32_t set_clipboard_image(wchar_t *path) { | ||||||
| 
 | 
 | ||||||
|     return result; |     return result; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -79,6 +79,11 @@ extern "C" void delete_string(int32_t count); | ||||||
|  */ |  */ | ||||||
| extern "C" void trigger_paste(); | extern "C" void trigger_paste(); | ||||||
| 
 | 
 | ||||||
|  | /*
 | ||||||
|  |  * Send the copy keyboard shortcut (CTRL+C) | ||||||
|  |  */ | ||||||
|  | extern "C" void trigger_copy(); | ||||||
|  | 
 | ||||||
| // Detect current application commands
 | // Detect current application commands
 | ||||||
| 
 | 
 | ||||||
| /*
 | /*
 | ||||||
|  |  | ||||||
|  | @ -44,4 +44,5 @@ extern { | ||||||
|     pub fn trigger_terminal_paste(); |     pub fn trigger_terminal_paste(); | ||||||
|     pub fn trigger_shift_ins_paste(); |     pub fn trigger_shift_ins_paste(); | ||||||
|     pub fn trigger_alt_shift_ins_paste(); |     pub fn trigger_alt_shift_ins_paste(); | ||||||
|  |     pub fn trigger_copy(); | ||||||
| } | } | ||||||
|  | @ -59,4 +59,5 @@ extern { | ||||||
|     pub fn send_multi_vkey(vk: i32, count: i32); |     pub fn send_multi_vkey(vk: i32, count: i32); | ||||||
|     pub fn delete_string(count: i32); |     pub fn delete_string(count: i32); | ||||||
|     pub fn trigger_paste(); |     pub fn trigger_paste(); | ||||||
|  |     pub fn trigger_copy(); | ||||||
| } | } | ||||||
|  | @ -59,4 +59,5 @@ extern { | ||||||
|     pub fn send_multi_vkey(vk: i32, count: i32); |     pub fn send_multi_vkey(vk: i32, count: i32); | ||||||
|     pub fn delete_string(count: i32); |     pub fn delete_string(count: i32); | ||||||
|     pub fn trigger_paste(); |     pub fn trigger_paste(); | ||||||
|  |     pub fn trigger_copy(); | ||||||
| } | } | ||||||
|  | @ -54,7 +54,13 @@ fn default_use_system_agent() -> bool { true } | ||||||
| fn default_config_caching_interval() -> i32 { 800 } | fn default_config_caching_interval() -> i32 { 800 } | ||||||
| fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8 as char] } | fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8 as char] } | ||||||
| fn default_toggle_interval() -> u32 { 230 } | fn default_toggle_interval() -> u32 { 230 } | ||||||
|  | fn default_toggle_key() -> KeyModifier { KeyModifier::ALT } | ||||||
| fn default_preserve_clipboard() -> bool {false} | fn default_preserve_clipboard() -> bool {false} | ||||||
|  | fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned() } | ||||||
|  | fn default_passive_arg_delimiter() -> char { '/' } | ||||||
|  | fn default_passive_arg_escape() -> char { '\\' } | ||||||
|  | fn default_passive_key() -> KeyModifier { KeyModifier::OFF } | ||||||
|  | fn default_action_noop_interval() -> u128 { 500 } | ||||||
| fn default_backspace_limit() -> i32 { 3 } | fn default_backspace_limit() -> i32 { 3 } | ||||||
| fn default_exclude_default_matches() -> bool {false} | fn default_exclude_default_matches() -> bool {false} | ||||||
| fn default_matches() -> Vec<Match> { Vec::new() } | fn default_matches() -> Vec<Match> { Vec::new() } | ||||||
|  | @ -97,7 +103,7 @@ pub struct Configs { | ||||||
|     #[serde(default = "default_word_separators")] |     #[serde(default = "default_word_separators")] | ||||||
|     pub word_separators: Vec<char>,  // TODO: add parsing test
 |     pub word_separators: Vec<char>,  // TODO: add parsing test
 | ||||||
| 
 | 
 | ||||||
|     #[serde(default)] |     #[serde(default = "default_toggle_key")] | ||||||
|     pub toggle_key: KeyModifier, |     pub toggle_key: KeyModifier, | ||||||
| 
 | 
 | ||||||
|     #[serde(default = "default_toggle_interval")] |     #[serde(default = "default_toggle_interval")] | ||||||
|  | @ -106,6 +112,21 @@ pub struct Configs { | ||||||
|     #[serde(default = "default_preserve_clipboard")] |     #[serde(default = "default_preserve_clipboard")] | ||||||
|     pub preserve_clipboard: bool, |     pub preserve_clipboard: bool, | ||||||
| 
 | 
 | ||||||
|  |     #[serde(default = "default_passive_match_regex")] | ||||||
|  |     pub passive_match_regex: String, | ||||||
|  | 
 | ||||||
|  |     #[serde(default = "default_passive_arg_delimiter")] | ||||||
|  |     pub passive_arg_delimiter: char, | ||||||
|  | 
 | ||||||
|  |     #[serde(default = "default_passive_arg_escape")] | ||||||
|  |     pub passive_arg_escape: char, | ||||||
|  | 
 | ||||||
|  |     #[serde(default = "default_passive_key")] | ||||||
|  |     pub passive_key: KeyModifier, | ||||||
|  | 
 | ||||||
|  |     #[serde(default = "default_action_noop_interval")] | ||||||
|  |     pub action_noop_interval: u128, | ||||||
|  | 
 | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     pub paste_shortcut: PasteShortcut, |     pub paste_shortcut: PasteShortcut, | ||||||
| 
 | 
 | ||||||
|  | @ -149,12 +170,17 @@ impl Configs { | ||||||
|         validate_field!(result, self.config_caching_interval, default_config_caching_interval()); |         validate_field!(result, self.config_caching_interval, default_config_caching_interval()); | ||||||
|         validate_field!(result, self.log_level, default_log_level()); |         validate_field!(result, self.log_level, default_log_level()); | ||||||
|         validate_field!(result, self.conflict_check, default_conflict_check()); |         validate_field!(result, self.conflict_check, default_conflict_check()); | ||||||
|         validate_field!(result, self.toggle_key, KeyModifier::default()); |         validate_field!(result, self.toggle_key, default_toggle_key()); | ||||||
|         validate_field!(result, self.toggle_interval, default_toggle_interval()); |         validate_field!(result, self.toggle_interval, default_toggle_interval()); | ||||||
|         validate_field!(result, self.backspace_limit, default_backspace_limit()); |         validate_field!(result, self.backspace_limit, default_backspace_limit()); | ||||||
|         validate_field!(result, self.ipc_server_port, default_ipc_server_port()); |         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.use_system_agent, default_use_system_agent()); | ||||||
|         validate_field!(result, self.preserve_clipboard, default_preserve_clipboard()); |         validate_field!(result, self.preserve_clipboard, default_preserve_clipboard()); | ||||||
|  |         validate_field!(result, self.passive_match_regex, default_passive_match_regex()); | ||||||
|  |         validate_field!(result, self.passive_arg_delimiter, default_passive_arg_delimiter()); | ||||||
|  |         validate_field!(result, self.passive_arg_escape, default_passive_arg_escape()); | ||||||
|  |         validate_field!(result, self.passive_key, default_passive_key()); | ||||||
|  |         validate_field!(result, self.action_noop_interval, default_action_noop_interval()); | ||||||
| 
 | 
 | ||||||
|         result |         result | ||||||
|     } |     } | ||||||
|  |  | ||||||
							
								
								
									
										155
									
								
								src/engine.rs
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								src/engine.rs
									
									
									
									
									
								
							|  | @ -26,43 +26,44 @@ use log::{info, warn, error}; | ||||||
| use crate::ui::{UIManager, MenuItem, MenuItemType}; | use crate::ui::{UIManager, MenuItem, MenuItemType}; | ||||||
| use crate::event::{ActionEventReceiver, ActionType}; | use crate::event::{ActionEventReceiver, ActionType}; | ||||||
| use crate::extension::Extension; | use crate::extension::Extension; | ||||||
|  | use crate::render::{Renderer, RenderResult}; | ||||||
| use std::cell::RefCell; | use std::cell::RefCell; | ||||||
| use std::process::exit; | use std::process::exit; | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
| use regex::{Regex, Captures}; | use regex::{Regex, Captures}; | ||||||
|  | use std::time::SystemTime; | ||||||
| 
 | 
 | ||||||
| pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, | pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, | ||||||
|                   U: UIManager> { |                   U: UIManager, R: Renderer> { | ||||||
|     keyboard_manager: &'a S, |     keyboard_manager: &'a S, | ||||||
|     clipboard_manager: &'a C, |     clipboard_manager: &'a C, | ||||||
|     config_manager: &'a M, |     config_manager: &'a M, | ||||||
|     ui_manager: &'a U, |     ui_manager: &'a U, | ||||||
| 
 |     renderer: &'a R, | ||||||
|     extension_map: HashMap<String, Box<dyn Extension>>, |  | ||||||
| 
 | 
 | ||||||
|     enabled: RefCell<bool>, |     enabled: RefCell<bool>, | ||||||
|  |     last_action_time: RefCell<SystemTime>,  // Used to block espanso from re-interpreting it's own inputs
 | ||||||
|  |     action_noop_interval: u128, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> | impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer> | ||||||
|     Engine<'a, S, C, M, U> { |     Engine<'a, S, C, M, U, R> { | ||||||
|     pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C, |     pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C, | ||||||
|                config_manager: &'a M, ui_manager: &'a U, |                config_manager: &'a M, ui_manager: &'a U, | ||||||
|                extensions: Vec<Box<dyn Extension>>) -> Engine<'a, S, C, M, U> { |                renderer: &'a R) -> Engine<'a, S, C, M, U, R> { | ||||||
|         // Register all the extensions
 |  | ||||||
|         let mut extension_map = HashMap::new(); |  | ||||||
|         for extension in extensions.into_iter() { |  | ||||||
|             extension_map.insert(extension.name(), extension); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let enabled = RefCell::new(true); |         let enabled = RefCell::new(true); | ||||||
|  |         let last_action_time = RefCell::new(SystemTime::now()); | ||||||
|  |         let action_noop_interval = config_manager.default_config().action_noop_interval; | ||||||
| 
 | 
 | ||||||
|         Engine{keyboard_manager, |         Engine{keyboard_manager, | ||||||
|             clipboard_manager, |             clipboard_manager, | ||||||
|             config_manager, |             config_manager, | ||||||
|             ui_manager, |             ui_manager, | ||||||
|             extension_map, |             renderer, | ||||||
|             enabled |             enabled, | ||||||
|  |             last_action_time, | ||||||
|  |             action_noop_interval, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -108,14 +109,28 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa | ||||||
|             None |             None | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /// Used to check if the last action has been executed within a specified interval.
 | ||||||
|  |     /// If so, return true (blocking the action), otherwise false.
 | ||||||
|  |     fn check_last_action_and_set(&self, interval: u128) -> bool { | ||||||
|  |         let mut last_action_time = self.last_action_time.borrow_mut(); | ||||||
|  |         if let Ok(elapsed) = last_action_time.elapsed() { | ||||||
|  |             if elapsed.as_millis() < interval { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         (*last_action_time) = SystemTime::now(); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| lazy_static! { | lazy_static! { | ||||||
|     static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap(); |     static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> | impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer> | ||||||
|     MatchReceiver for Engine<'a, S, C, M, U>{ |     MatchReceiver for Engine<'a, S, C, M, U, R>{ | ||||||
| 
 | 
 | ||||||
|     fn on_match(&self, m: &Match, trailing_separator: Option<char>) { |     fn on_match(&self, m: &Match, trailing_separator: Option<char>) { | ||||||
|         let config = self.config_manager.active_config(); |         let config = self.config_manager.active_config(); | ||||||
|  | @ -124,6 +139,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         // avoid espanso reinterpreting its own actions
 | ||||||
|  |         if self.check_last_action_and_set(self.action_noop_interval) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         let char_count = if trailing_separator.is_none() { |         let char_count = if trailing_separator.is_none() { | ||||||
|             m.trigger.chars().count() as i32 |             m.trigger.chars().count() as i32 | ||||||
|         }else{ |         }else{ | ||||||
|  | @ -134,40 +154,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa | ||||||
| 
 | 
 | ||||||
|         let mut previous_clipboard_content : Option<String> = None; |         let mut previous_clipboard_content : Option<String> = None; | ||||||
| 
 | 
 | ||||||
|         // Manage the different types of matches
 |         let rendered = self.renderer.render_match(m, config, vec![]); | ||||||
|         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 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); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     // 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() |  | ||||||
|                 }; |  | ||||||
| 
 | 
 | ||||||
|  |         match rendered { | ||||||
|  |             RenderResult::Text(mut target_string) => { | ||||||
|                 // If a trailing separator was counted in the match, add it back to the target string
 |                 // If a trailing separator was counted in the match, add it back to the target string
 | ||||||
|                 if let Some(trailing_separator) = trailing_separator { |                 if let Some(trailing_separator) = trailing_separator { | ||||||
|                     if trailing_separator == '\r' {   // If the trailing separator is a carriage return,
 |                     if trailing_separator == '\r' {   // If the trailing separator is a carriage return,
 | ||||||
|  | @ -234,20 +224,16 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa | ||||||
|                     self.keyboard_manager.move_cursor_left(moves); |                     self.keyboard_manager.move_cursor_left(moves); | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  |             RenderResult::Image(image_path) => { | ||||||
|  |                 // 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(); | ||||||
| 
 | 
 | ||||||
|             // Image Match
 |                 self.clipboard_manager.set_clipboard_image(&image_path); | ||||||
|             MatchContentType::Image(content) => { |                 self.keyboard_manager.trigger_paste(&config.paste_shortcut); | ||||||
|                 // Make sure the image exist beforehand
 |             }, | ||||||
|                 if content.path.exists() { |             RenderResult::Error => { | ||||||
|                     // If the preserve_clipboard option is enabled, save the current
 |                 error!("Could not render match: {}", m.trigger); | ||||||
|                     // 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{ |  | ||||||
|                     error!("Image not found in path: {:?}", content.path); |  | ||||||
|                 } |  | ||||||
|             }, |             }, | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -258,6 +244,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn on_enable_update(&self, status: bool) { |     fn on_enable_update(&self, status: bool) { | ||||||
|  |         // avoid espanso reinterpreting its own actions
 | ||||||
|  |         if self.check_last_action_and_set(self.action_noop_interval) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         let message = if status { |         let message = if status { | ||||||
|             "espanso enabled" |             "espanso enabled" | ||||||
|         }else{ |         }else{ | ||||||
|  | @ -271,10 +262,48 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa | ||||||
| 
 | 
 | ||||||
|         self.ui_manager.notify(message); |         self.ui_manager.notify(message); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     fn on_passive(&self) { | ||||||
|  |         // avoid espanso reinterpreting its own actions
 | ||||||
|  |         if self.check_last_action_and_set(self.action_noop_interval) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         info!("Passive mode activated"); | ||||||
|  | 
 | ||||||
|  |         // Trigger a copy shortcut to transfer the content of the selection to the clipboard
 | ||||||
|  |         self.keyboard_manager.trigger_copy(); | ||||||
|  | 
 | ||||||
|  |         // Sleep for a while, giving time to effectively copy the text
 | ||||||
|  |         std::thread::sleep(std::time::Duration::from_millis(100));  // TODO: avoid hardcoding
 | ||||||
|  | 
 | ||||||
|  |         // Then get the text from the clipboard and render the match output
 | ||||||
|  |         let clipboard = self.clipboard_manager.get_clipboard(); | ||||||
|  | 
 | ||||||
|  |         if let Some(clipboard) = clipboard { | ||||||
|  |             let config = self.config_manager.active_config(); | ||||||
|  | 
 | ||||||
|  |             let rendered = self.renderer.render_passive(&clipboard, | ||||||
|  |                                                         &config); | ||||||
|  | 
 | ||||||
|  |             match rendered { | ||||||
|  |                 RenderResult::Text(payload) => { | ||||||
|  |                     // Paste back the result in the field
 | ||||||
|  |                     self.clipboard_manager.set_clipboard(&payload); | ||||||
|  | 
 | ||||||
|  |                     std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
 | ||||||
|  |                     self.keyboard_manager.trigger_paste(&config.paste_shortcut); | ||||||
|  |                 }, | ||||||
|  |                 _ => { | ||||||
|  |                     warn!("Cannot expand passive match") | ||||||
|  |                 }, | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl <'a, S: KeyboardManager, C: ClipboardManager, | impl <'a, S: KeyboardManager, C: ClipboardManager, | ||||||
|     M: ConfigManager<'a>, U: UIManager> ActionEventReceiver for Engine<'a, S, C, M, U>{ |     M: ConfigManager<'a>, U: UIManager, R: Renderer> ActionEventReceiver for Engine<'a, S, C, M, U, R>{ | ||||||
| 
 | 
 | ||||||
|     fn on_action_event(&self, e: ActionType) { |     fn on_action_event(&self, e: ActionType) { | ||||||
|         match e { |         match e { | ||||||
|  |  | ||||||
|  | @ -66,12 +66,6 @@ pub enum KeyModifier { | ||||||
|     OFF, |     OFF, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Default for KeyModifier { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         KeyModifier::ALT |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Receivers
 | // Receivers
 | ||||||
| 
 | 
 | ||||||
| pub trait KeyEventReceiver { | pub trait KeyEventReceiver { | ||||||
|  |  | ||||||
|  | @ -33,7 +33,7 @@ impl super::Extension for DateExtension { | ||||||
|         String::from("date") |         String::from("date") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn calculate(&self, params: &Mapping) -> Option<String> { |     fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> { | ||||||
|         let now: DateTime<Local> = Local::now(); |         let now: DateTime<Local> = Local::now(); | ||||||
| 
 | 
 | ||||||
|         let format = params.get(&Value::from("format")); |         let format = params.get(&Value::from("format")); | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ mod random; | ||||||
| 
 | 
 | ||||||
| pub trait Extension { | pub trait Extension { | ||||||
|     fn name(&self) -> String; |     fn name(&self) -> String; | ||||||
|     fn calculate(&self, params: &Mapping) -> Option<String>; |     fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub fn get_extensions() -> Vec<Box<dyn Extension>> { | pub fn get_extensions() -> Vec<Box<dyn Extension>> { | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ impl super::Extension for RandomExtension { | ||||||
|         String::from("random") |         String::from("random") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn calculate(&self, params: &Mapping) -> Option<String> { |     fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String> { | ||||||
|         let choices = params.get(&Value::from("choices")); |         let choices = params.get(&Value::from("choices")); | ||||||
|         if choices.is_none() { |         if choices.is_none() { | ||||||
|             warn!("No 'choices' parameter specified for random variable"); |             warn!("No 'choices' parameter specified for random variable"); | ||||||
|  | @ -51,7 +51,10 @@ impl super::Extension for RandomExtension { | ||||||
| 
 | 
 | ||||||
|             match choice { |             match choice { | ||||||
|                 Some(output) => { |                 Some(output) => { | ||||||
|                     return Some(output.clone()) |                     // Render arguments
 | ||||||
|  |                     let output = crate::render::utils::render_args(output, args); | ||||||
|  | 
 | ||||||
|  |                     return Some(output) | ||||||
|                 }, |                 }, | ||||||
|                 None => { |                 None => { | ||||||
|                     error!("Could not select a random choice."); |                     error!("Could not select a random choice."); | ||||||
|  | @ -82,7 +85,7 @@ mod tests { | ||||||
|         params.insert(Value::from("choices"), Value::from(choices.clone())); |         params.insert(Value::from("choices"), Value::from(choices.clone())); | ||||||
| 
 | 
 | ||||||
|         let extension = RandomExtension::new(); |         let extension = RandomExtension::new(); | ||||||
|         let output = extension.calculate(¶ms); |         let output = extension.calculate(¶ms, &vec![]); | ||||||
| 
 | 
 | ||||||
|         assert!(output.is_some()); |         assert!(output.is_some()); | ||||||
| 
 | 
 | ||||||
|  | @ -90,4 +93,30 @@ mod tests { | ||||||
| 
 | 
 | ||||||
|         assert!(choices.iter().any(|x| x == &output)); |         assert!(choices.iter().any(|x| x == &output)); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_random_with_args() { | ||||||
|  |         let mut params = Mapping::new(); | ||||||
|  |         let choices = vec!( | ||||||
|  |             "first $0$", | ||||||
|  |             "second $0$", | ||||||
|  |             "$0$ third", | ||||||
|  |         ); | ||||||
|  |         params.insert(Value::from("choices"), Value::from(choices.clone())); | ||||||
|  | 
 | ||||||
|  |         let extension = RandomExtension::new(); | ||||||
|  |         let output = extension.calculate(¶ms, &vec!["test".to_owned()]); | ||||||
|  | 
 | ||||||
|  |         assert!(output.is_some()); | ||||||
|  | 
 | ||||||
|  |         let output = output.unwrap(); | ||||||
|  | 
 | ||||||
|  |         let rendered_choices = vec!( | ||||||
|  |             "first test", | ||||||
|  |             "second test", | ||||||
|  |             "test third", | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         assert!(rendered_choices.iter().any(|x| x == &output)); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -34,7 +34,7 @@ impl super::Extension for ScriptExtension { | ||||||
|         String::from("script") |         String::from("script") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn calculate(&self, params: &Mapping) -> Option<String> { |     fn calculate(&self, params: &Mapping, user_args: &Vec<String>) -> Option<String> { | ||||||
|         let args = params.get(&Value::from("args")); |         let args = params.get(&Value::from("args")); | ||||||
|         if args.is_none() { |         if args.is_none() { | ||||||
|             warn!("No 'args' parameter specified for script variable"); |             warn!("No 'args' parameter specified for script variable"); | ||||||
|  | @ -42,10 +42,17 @@ impl super::Extension for ScriptExtension { | ||||||
|         } |         } | ||||||
|         let args = args.unwrap().as_sequence(); |         let args = args.unwrap().as_sequence(); | ||||||
|         if let Some(args) = args { |         if let Some(args) = args { | ||||||
|             let str_args = args.iter().map(|arg| { |             let mut str_args = args.iter().map(|arg| { | ||||||
|                arg.as_str().unwrap_or_default().to_string() |                arg.as_str().unwrap_or_default().to_string() | ||||||
|             }).collect::<Vec<String>>(); |             }).collect::<Vec<String>>(); | ||||||
| 
 | 
 | ||||||
|  |             // The user has to enable argument concatenation explicitly
 | ||||||
|  |             let inject_args = params.get(&Value::from("inject_args")) | ||||||
|  |                 .unwrap_or(&Value::from(false)).as_bool().unwrap_or(false); | ||||||
|  |             if inject_args { | ||||||
|  |                 str_args.extend(user_args.clone()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             let output = if str_args.len() > 1 { |             let output = if str_args.len() > 1 { | ||||||
|                 Command::new(&str_args[0]) |                 Command::new(&str_args[0]) | ||||||
|                     .args(&str_args[1..]) |                     .args(&str_args[1..]) | ||||||
|  | @ -55,6 +62,7 @@ impl super::Extension for ScriptExtension { | ||||||
|                     .output() |                     .output() | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|  |             println!("{:?}", output); | ||||||
|             match output { |             match output { | ||||||
|                 Ok(output) => { |                 Ok(output) => { | ||||||
|                     let output_str = String::from_utf8_lossy(output.stdout.as_slice()); |                     let output_str = String::from_utf8_lossy(output.stdout.as_slice()); | ||||||
|  | @ -71,4 +79,50 @@ impl super::Extension for ScriptExtension { | ||||||
|         error!("Could not execute script with args '{:?}'", args); |         error!("Could not execute script with args '{:?}'", args); | ||||||
|         None |         None | ||||||
|     } |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  |     use crate::extension::Extension; | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     #[cfg(not(target_os = "windows"))] | ||||||
|  |     fn test_script_basic() { | ||||||
|  |         let mut params = Mapping::new(); | ||||||
|  |         params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"])); | ||||||
|  | 
 | ||||||
|  |         let extension = ScriptExtension::new(); | ||||||
|  |         let output = extension.calculate(¶ms, &vec![]); | ||||||
|  | 
 | ||||||
|  |         assert!(output.is_some()); | ||||||
|  |         assert_eq!(output.unwrap(), "hello world\n"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     #[cfg(not(target_os = "windows"))] | ||||||
|  |     fn test_script_inject_args_off() { | ||||||
|  |         let mut params = Mapping::new(); | ||||||
|  |         params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"])); | ||||||
|  | 
 | ||||||
|  |         let extension = ScriptExtension::new(); | ||||||
|  |         let output = extension.calculate(¶ms, &vec!["jon".to_owned()]); | ||||||
|  | 
 | ||||||
|  |         assert!(output.is_some()); | ||||||
|  |         assert_eq!(output.unwrap(), "hello world\n"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     #[cfg(not(target_os = "windows"))] | ||||||
|  |     fn test_script_inject_args_on() { | ||||||
|  |         let mut params = Mapping::new(); | ||||||
|  |         params.insert(Value::from("args"), Value::from(vec!["echo", "hello world"])); | ||||||
|  |         params.insert(Value::from("inject_args"), Value::from(true)); | ||||||
|  | 
 | ||||||
|  |         let extension = ScriptExtension::new(); | ||||||
|  |         let output = extension.calculate(¶ms, &vec!["jon".to_owned()]); | ||||||
|  | 
 | ||||||
|  |         assert!(output.is_some()); | ||||||
|  |         assert_eq!(output.unwrap(), "hello world jon\n"); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -20,6 +20,15 @@ | ||||||
| use serde_yaml::{Mapping, Value}; | use serde_yaml::{Mapping, Value}; | ||||||
| use std::process::Command; | use std::process::Command; | ||||||
| use log::{warn, error}; | use log::{warn, error}; | ||||||
|  | use regex::{Regex, Captures}; | ||||||
|  | 
 | ||||||
|  | lazy_static! { | ||||||
|  |     static ref POS_ARG_REGEX: Regex = if cfg!(target_os = "windows") { | ||||||
|  |         Regex::new("%(?P<pos>\\d+)").unwrap() | ||||||
|  |     }else{ | ||||||
|  |         Regex::new("\\$(?P<pos>\\d+)").unwrap() | ||||||
|  |     }; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| pub struct ShellExtension {} | pub struct ShellExtension {} | ||||||
| 
 | 
 | ||||||
|  | @ -34,7 +43,7 @@ impl super::Extension for ShellExtension { | ||||||
|         String::from("shell") |         String::from("shell") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn calculate(&self, params: &Mapping) -> Option<String> { |     fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String> { | ||||||
|         let cmd = params.get(&Value::from("cmd")); |         let cmd = params.get(&Value::from("cmd")); | ||||||
|         if cmd.is_none() { |         if cmd.is_none() { | ||||||
|             warn!("No 'cmd' parameter specified for shell variable"); |             warn!("No 'cmd' parameter specified for shell variable"); | ||||||
|  | @ -42,14 +51,25 @@ impl super::Extension for ShellExtension { | ||||||
|         } |         } | ||||||
|         let cmd = cmd.unwrap().as_str().unwrap(); |         let cmd = cmd.unwrap().as_str().unwrap(); | ||||||
| 
 | 
 | ||||||
|  |         // Render positional parameters in args
 | ||||||
|  |         let cmd = POS_ARG_REGEX.replace_all(&cmd, |caps: &Captures| { | ||||||
|  |             let position_str  = caps.name("pos").unwrap().as_str(); | ||||||
|  |             let position = position_str.parse::<i32>().unwrap_or(-1); | ||||||
|  |             if position >= 0 && position < args.len() as i32 { | ||||||
|  |                 args[position as usize].to_owned() | ||||||
|  |             }else{ | ||||||
|  |                 "".to_owned() | ||||||
|  |             } | ||||||
|  |         }).to_string(); | ||||||
|  | 
 | ||||||
|         let output = if cfg!(target_os = "windows") { |         let output = if cfg!(target_os = "windows") { | ||||||
|             Command::new("cmd") |             Command::new("cmd") | ||||||
|                 .args(&["/C", cmd]) |                 .args(&["/C", &cmd]) | ||||||
|                 .output() |                 .output() | ||||||
|         } else { |         } else { | ||||||
|             Command::new("sh") |             Command::new("sh") | ||||||
|                 .arg("-c") |                 .arg("-c") | ||||||
|                 .arg(cmd) |                 .arg(&cmd) | ||||||
|                 .output() |                 .output() | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|  | @ -90,7 +110,7 @@ mod tests { | ||||||
|         params.insert(Value::from("cmd"), Value::from("echo hello world")); |         params.insert(Value::from("cmd"), Value::from("echo hello world")); | ||||||
| 
 | 
 | ||||||
|         let extension = ShellExtension::new(); |         let extension = ShellExtension::new(); | ||||||
|         let output = extension.calculate(¶ms); |         let output = extension.calculate(¶ms, &vec![]); | ||||||
| 
 | 
 | ||||||
|         assert!(output.is_some()); |         assert!(output.is_some()); | ||||||
| 
 | 
 | ||||||
|  | @ -108,7 +128,7 @@ mod tests { | ||||||
|         params.insert(Value::from("trim"), Value::from(true)); |         params.insert(Value::from("trim"), Value::from(true)); | ||||||
| 
 | 
 | ||||||
|         let extension = ShellExtension::new(); |         let extension = ShellExtension::new(); | ||||||
|         let output = extension.calculate(¶ms); |         let output = extension.calculate(¶ms, &vec![]); | ||||||
| 
 | 
 | ||||||
|         assert!(output.is_some()); |         assert!(output.is_some()); | ||||||
|         assert_eq!(output.unwrap(), "hello world"); |         assert_eq!(output.unwrap(), "hello world"); | ||||||
|  | @ -126,7 +146,7 @@ mod tests { | ||||||
|         params.insert(Value::from("trim"), Value::from(true)); |         params.insert(Value::from("trim"), Value::from(true)); | ||||||
| 
 | 
 | ||||||
|         let extension = ShellExtension::new(); |         let extension = ShellExtension::new(); | ||||||
|         let output = extension.calculate(¶ms); |         let output = extension.calculate(¶ms, &vec![]); | ||||||
| 
 | 
 | ||||||
|         assert!(output.is_some()); |         assert!(output.is_some()); | ||||||
|         assert_eq!(output.unwrap(), "hello world"); |         assert_eq!(output.unwrap(), "hello world"); | ||||||
|  | @ -139,7 +159,7 @@ mod tests { | ||||||
|         params.insert(Value::from("trim"), Value::from("error")); |         params.insert(Value::from("trim"), Value::from("error")); | ||||||
| 
 | 
 | ||||||
|         let extension = ShellExtension::new(); |         let extension = ShellExtension::new(); | ||||||
|         let output = extension.calculate(¶ms); |         let output = extension.calculate(¶ms, &vec![]); | ||||||
| 
 | 
 | ||||||
|         assert!(output.is_some()); |         assert!(output.is_some()); | ||||||
|         if cfg!(target_os = "windows") { |         if cfg!(target_os = "windows") { | ||||||
|  | @ -157,9 +177,37 @@ mod tests { | ||||||
|         params.insert(Value::from("trim"), Value::from(true)); |         params.insert(Value::from("trim"), Value::from(true)); | ||||||
| 
 | 
 | ||||||
|         let extension = ShellExtension::new(); |         let extension = ShellExtension::new(); | ||||||
|         let output = extension.calculate(¶ms); |         let output = extension.calculate(¶ms, &vec![]); | ||||||
| 
 | 
 | ||||||
|         assert!(output.is_some()); |         assert!(output.is_some()); | ||||||
|         assert_eq!(output.unwrap(), "hello world"); |         assert_eq!(output.unwrap(), "hello world"); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     #[cfg(not(target_os = "windows"))] | ||||||
|  |     fn test_shell_args_unix() { | ||||||
|  |         let mut params = Mapping::new(); | ||||||
|  |         params.insert(Value::from("cmd"), Value::from("echo $0")); | ||||||
|  | 
 | ||||||
|  |         let extension = ShellExtension::new(); | ||||||
|  |         let output = extension.calculate(¶ms, &vec!["hello".to_owned()]); | ||||||
|  | 
 | ||||||
|  |         assert!(output.is_some()); | ||||||
|  | 
 | ||||||
|  |         assert_eq!(output.unwrap(), "hello\n"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     #[cfg(target_os = "windows")] | ||||||
|  |     fn test_shell_args_windows() { | ||||||
|  |         let mut params = Mapping::new(); | ||||||
|  |         params.insert(Value::from("cmd"), Value::from("echo %0")); | ||||||
|  | 
 | ||||||
|  |         let extension = ShellExtension::new(); | ||||||
|  |         let output = extension.calculate(¶ms, &vec!["hello".to_owned()]); | ||||||
|  | 
 | ||||||
|  |         assert!(output.is_some()); | ||||||
|  | 
 | ||||||
|  |         assert_eq!(output.unwrap(), "hello\r\n"); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -81,4 +81,10 @@ impl super::KeyboardManager for LinuxKeyboardManager { | ||||||
|             left_arrow(count); |             left_arrow(count); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     fn trigger_copy(&self) { | ||||||
|  |         unsafe { | ||||||
|  |             trigger_copy(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -56,6 +56,12 @@ impl super::KeyboardManager for MacKeyboardManager { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fn trigger_copy(&self) { | ||||||
|  |         unsafe { | ||||||
|  |             trigger_copy(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     fn delete_string(&self, count: i32) { |     fn delete_string(&self, count: i32) { | ||||||
|         unsafe {delete_string(count)} |         unsafe {delete_string(count)} | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -34,6 +34,7 @@ pub trait KeyboardManager { | ||||||
|     fn trigger_paste(&self, shortcut: &PasteShortcut); |     fn trigger_paste(&self, shortcut: &PasteShortcut); | ||||||
|     fn delete_string(&self, count: i32); |     fn delete_string(&self, count: i32); | ||||||
|     fn move_cursor_left(&self, count: i32); |     fn move_cursor_left(&self, count: i32); | ||||||
|  |     fn trigger_copy(&self); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | #[derive(Debug, Serialize, Deserialize, Clone)] | ||||||
|  |  | ||||||
|  | @ -73,4 +73,10 @@ impl super::KeyboardManager for WindowsKeyboardManager { | ||||||
|             send_multi_vkey(0x25, count) |             send_multi_vkey(0x25, count) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     fn trigger_copy(&self) { | ||||||
|  |         unsafe { | ||||||
|  |             trigger_copy(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -32,7 +32,7 @@ use fs2::FileExt; | ||||||
| use log::{info, warn, LevelFilter}; | use log::{info, warn, LevelFilter}; | ||||||
| use simplelog::{CombinedLogger, SharedLogger, TerminalMode, TermLogger, WriteLogger}; | use simplelog::{CombinedLogger, SharedLogger, TerminalMode, TermLogger, WriteLogger}; | ||||||
| 
 | 
 | ||||||
| use crate::config::ConfigSet; | use crate::config::{ConfigSet, ConfigManager}; | ||||||
| use crate::config::runtime::RuntimeConfigManager; | use crate::config::runtime::RuntimeConfigManager; | ||||||
| use crate::engine::Engine; | use crate::engine::Engine; | ||||||
| use crate::event::*; | use crate::event::*; | ||||||
|  | @ -52,6 +52,7 @@ mod utils; | ||||||
| mod bridge; | mod bridge; | ||||||
| mod engine; | mod engine; | ||||||
| mod config; | mod config; | ||||||
|  | mod render; | ||||||
| mod system; | mod system; | ||||||
| mod context; | mod context; | ||||||
| mod matcher; | mod matcher; | ||||||
|  | @ -332,11 +333,14 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet) { | ||||||
| 
 | 
 | ||||||
|     let extensions = extension::get_extensions(); |     let extensions = extension::get_extensions(); | ||||||
| 
 | 
 | ||||||
|  |     let renderer = render::default::DefaultRenderer::new(extensions, | ||||||
|  |                                                           config_manager.default_config().clone()); | ||||||
|  | 
 | ||||||
|     let engine = Engine::new(&keyboard_manager, |     let engine = Engine::new(&keyboard_manager, | ||||||
|                              &clipboard_manager, |                              &clipboard_manager, | ||||||
|                              &config_manager, |                              &config_manager, | ||||||
|                              &ui_manager, |                              &ui_manager, | ||||||
|                              extensions, |                              &renderer, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     let matcher = ScrollingMatcher::new(&config_manager, &engine); |     let matcher = ScrollingMatcher::new(&config_manager, &engine); | ||||||
|  |  | ||||||
|  | @ -32,6 +32,7 @@ pub struct Match { | ||||||
|     pub trigger: String, |     pub trigger: String, | ||||||
|     pub content: MatchContentType, |     pub content: MatchContentType, | ||||||
|     pub word: bool, |     pub word: bool, | ||||||
|  |     pub passive_only: bool, | ||||||
| 
 | 
 | ||||||
|     // Automatically calculated from the trigger, used by the matcher to check for correspondences.
 |     // Automatically calculated from the trigger, used by the matcher to check for correspondences.
 | ||||||
|     #[serde(skip_serializing)] |     #[serde(skip_serializing)] | ||||||
|  | @ -133,7 +134,8 @@ impl<'a> From<&'a AutoMatch> for Match{ | ||||||
|         Self { |         Self { | ||||||
|             trigger: other.trigger.clone(), |             trigger: other.trigger.clone(), | ||||||
|             content, |             content, | ||||||
|             word: other.word.clone(), |             word: other.word, | ||||||
|  |             passive_only: other.passive_only, | ||||||
|             _trigger_sequence: trigger_sequence, |             _trigger_sequence: trigger_sequence, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -155,10 +157,14 @@ struct AutoMatch { | ||||||
| 
 | 
 | ||||||
|     #[serde(default = "default_word")] |     #[serde(default = "default_word")] | ||||||
|     pub word: bool, |     pub word: bool, | ||||||
|  | 
 | ||||||
|  |     #[serde(default = "default_passive_only")] | ||||||
|  |     pub passive_only: bool, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn default_vars() -> Vec<MatchVariable> {Vec::new()} | fn default_vars() -> Vec<MatchVariable> {Vec::new()} | ||||||
| fn default_word() -> bool {false} | fn default_word() -> bool {false} | ||||||
|  | fn default_passive_only() -> bool {false} | ||||||
| fn default_replace() -> Option<String> {None} | fn default_replace() -> Option<String> {None} | ||||||
| fn default_image_path() -> Option<String> {None} | fn default_image_path() -> Option<String> {None} | ||||||
| 
 | 
 | ||||||
|  | @ -181,6 +187,7 @@ pub enum TriggerEntry { | ||||||
| pub trait MatchReceiver { | pub trait MatchReceiver { | ||||||
|     fn on_match(&self, m: &Match, trailing_separator: Option<char>); |     fn on_match(&self, m: &Match, trailing_separator: Option<char>); | ||||||
|     fn on_enable_update(&self, status: bool); |     fn on_enable_update(&self, status: bool); | ||||||
|  |     fn on_passive(&self); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub trait Matcher : KeyEventReceiver { | pub trait Matcher : KeyEventReceiver { | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| use crate::matcher::{Match, MatchReceiver, TriggerEntry}; | use crate::matcher::{Match, MatchReceiver, TriggerEntry}; | ||||||
| use std::cell::RefCell; | use std::cell::{RefCell, Ref}; | ||||||
| use crate::event::{KeyModifier, ActionEventReceiver, ActionType}; | use crate::event::{KeyModifier, ActionEventReceiver, ActionType}; | ||||||
| use crate::config::ConfigManager; | use crate::config::ConfigManager; | ||||||
| use crate::event::KeyModifier::BACKSPACE; | use crate::event::KeyModifier::BACKSPACE; | ||||||
|  | @ -30,6 +30,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { | ||||||
|     receiver: &'a R, |     receiver: &'a R, | ||||||
|     current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>, |     current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>, | ||||||
|     toggle_press_time: RefCell<SystemTime>, |     toggle_press_time: RefCell<SystemTime>, | ||||||
|  |     passive_press_time: RefCell<SystemTime>, | ||||||
|     is_enabled: RefCell<bool>, |     is_enabled: RefCell<bool>, | ||||||
|     was_previous_char_word_separator: RefCell<bool>, |     was_previous_char_word_separator: RefCell<bool>, | ||||||
| } | } | ||||||
|  | @ -45,12 +46,14 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { | ||||||
|     pub fn new(config_manager: &'a M, receiver: &'a R) -> ScrollingMatcher<'a, R, M> { |     pub fn new(config_manager: &'a M, receiver: &'a R) -> ScrollingMatcher<'a, R, M> { | ||||||
|         let current_set_queue = RefCell::new(VecDeque::new()); |         let current_set_queue = RefCell::new(VecDeque::new()); | ||||||
|         let toggle_press_time = RefCell::new(SystemTime::now()); |         let toggle_press_time = RefCell::new(SystemTime::now()); | ||||||
|  |         let passive_press_time = RefCell::new(SystemTime::now()); | ||||||
| 
 | 
 | ||||||
|         ScrollingMatcher{ |         ScrollingMatcher{ | ||||||
|             config_manager, |             config_manager, | ||||||
|             receiver, |             receiver, | ||||||
|             current_set_queue, |             current_set_queue, | ||||||
|             toggle_press_time, |             toggle_press_time, | ||||||
|  |             passive_press_time, | ||||||
|             is_enabled: RefCell::new(true), |             is_enabled: RefCell::new(true), | ||||||
|             was_previous_char_word_separator: RefCell::new(true), |             was_previous_char_word_separator: RefCell::new(true), | ||||||
|         } |         } | ||||||
|  | @ -111,6 +114,11 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa | ||||||
| 
 | 
 | ||||||
|         let new_matches: Vec<MatchEntry> = active_config.matches.iter() |         let new_matches: Vec<MatchEntry> = active_config.matches.iter() | ||||||
|             .filter(|&x| { |             .filter(|&x| { | ||||||
|  |                 // only active-enabled matches are considered
 | ||||||
|  |                 if x.passive_only { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|                 let mut result = Self::is_matching(x, c, 0, is_current_word_separator); |                 let mut result = Self::is_matching(x, c, 0, is_current_word_separator); | ||||||
| 
 | 
 | ||||||
|                 if x.word { |                 if x.word { | ||||||
|  | @ -193,22 +201,25 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa | ||||||
|     fn handle_modifier(&self, m: KeyModifier) { |     fn handle_modifier(&self, m: KeyModifier) { | ||||||
|         let config = self.config_manager.default_config(); |         let config = self.config_manager.default_config(); | ||||||
| 
 | 
 | ||||||
|  |         // TODO: at the moment, activating the passive key triggers the toggle key
 | ||||||
|  |         // study a mechanism to avoid this problem
 | ||||||
|  | 
 | ||||||
|         if m == config.toggle_key { |         if m == config.toggle_key { | ||||||
|             if m == KeyModifier::OFF { return } |             check_interval(&self.toggle_press_time, | ||||||
|             let mut toggle_press_time = self.toggle_press_time.borrow_mut(); |                            u128::from(config.toggle_interval), || { | ||||||
|             if let Ok(elapsed) = toggle_press_time.elapsed() { |                 self.toggle(); | ||||||
|                 if elapsed.as_millis() < u128::from(config.toggle_interval) { |  | ||||||
|                     self.toggle(); |  | ||||||
| 
 | 
 | ||||||
|                     let is_enabled = self.is_enabled.borrow(); |                 let is_enabled = self.is_enabled.borrow(); | ||||||
| 
 | 
 | ||||||
|                     if !*is_enabled { |                 if !*is_enabled { | ||||||
|                         self.current_set_queue.borrow_mut().clear(); |                     self.current_set_queue.borrow_mut().clear(); | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|             } |             }); | ||||||
| 
 |         }else if m == config.passive_key { | ||||||
|             (*toggle_press_time) = SystemTime::now(); |             check_interval(&self.passive_press_time, | ||||||
|  |                            u128::from(config.toggle_interval), || { | ||||||
|  |                 self.receiver.on_passive(); | ||||||
|  |             }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Backspace handling, basically "rewinding history"
 |         // Backspace handling, basically "rewinding history"
 | ||||||
|  | @ -234,4 +245,15 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ActionEventReceiver for Scroll | ||||||
|             _ => {} |             _ => {} | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn check_interval<F>(state_var: &RefCell<SystemTime>, interval: u128, elapsed_callback: F) where F:Fn() { | ||||||
|  |     let mut press_time = state_var.borrow_mut(); | ||||||
|  |     if let Ok(elapsed) = press_time.elapsed() { | ||||||
|  |         if elapsed.as_millis() < interval { | ||||||
|  |             elapsed_callback(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     (*press_time) = SystemTime::now(); | ||||||
| } | } | ||||||
							
								
								
									
										411
									
								
								src/render/default.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										411
									
								
								src/render/default.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,411 @@ | ||||||
|  | /* | ||||||
|  |  * 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 <https://www.gnu.org/licenses/>.
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | use serde_yaml::{Mapping, Value}; | ||||||
|  | use std::path::PathBuf; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use regex::{Regex, Captures}; | ||||||
|  | use log::{warn, error}; | ||||||
|  | use super::*; | ||||||
|  | use crate::matcher::{Match, MatchContentType}; | ||||||
|  | use crate::config::Configs; | ||||||
|  | use crate::extension::Extension; | ||||||
|  | 
 | ||||||
|  | lazy_static! { | ||||||
|  |     static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct DefaultRenderer { | ||||||
|  |     extension_map: HashMap<String, Box<dyn Extension>>, | ||||||
|  | 
 | ||||||
|  |     // Regex used to identify matches (and arguments) in passive expansions
 | ||||||
|  |     passive_match_regex: Regex, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl DefaultRenderer { | ||||||
|  |     pub fn new(extensions: Vec<Box<dyn Extension>>, config: Configs) -> DefaultRenderer { | ||||||
|  |         // Register all the extensions
 | ||||||
|  |         let mut extension_map = HashMap::new(); | ||||||
|  |         for extension in extensions.into_iter() { | ||||||
|  |             extension_map.insert(extension.name(), extension); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Compile the regexes
 | ||||||
|  |         let passive_match_regex = Regex::new(&config.passive_match_regex) | ||||||
|  |                                         .unwrap_or_else(|e| { | ||||||
|  |                                             panic!("Invalid passive match regex"); | ||||||
|  |                                         }); | ||||||
|  | 
 | ||||||
|  |         DefaultRenderer{ | ||||||
|  |             extension_map, | ||||||
|  |             passive_match_regex, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn find_match(config: &Configs, trigger: &str) -> Option<Match> { | ||||||
|  |         let mut result = None; | ||||||
|  | 
 | ||||||
|  |         // TODO: if performances become a problem, implement a more efficient lookup
 | ||||||
|  |         for m in config.matches.iter() { | ||||||
|  |             if m.trigger == trigger { | ||||||
|  |                 result = Some(m.clone()); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         result | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl super::Renderer for DefaultRenderer { | ||||||
|  |     fn render_match(&self, m: &Match, config: &Configs, args: Vec<String>) -> RenderResult { | ||||||
|  |         // Manage the different types of matches
 | ||||||
|  |         match &m.content { | ||||||
|  |             // Text Match
 | ||||||
|  |             MatchContentType::Text(content) => { | ||||||
|  |                 let target_string = if content._has_vars { | ||||||
|  |                     let mut output_map = HashMap::new(); | ||||||
|  | 
 | ||||||
|  |                     for variable in content.vars.iter() { | ||||||
|  |                         // In case of variables of type match, we need to recursively call
 | ||||||
|  |                         // the render function
 | ||||||
|  |                         if variable.var_type == "match" { | ||||||
|  |                             // Extract the match trigger from the variable params
 | ||||||
|  |                             let trigger = variable.params.get(&Value::from("trigger")); | ||||||
|  |                             if trigger.is_none() { | ||||||
|  |                                 warn!("Missing param 'trigger' in match variable: {}", variable.name); | ||||||
|  |                                 continue; | ||||||
|  |                             } | ||||||
|  |                             let trigger = trigger.unwrap(); | ||||||
|  | 
 | ||||||
|  |                             // Find the given match from the active configs
 | ||||||
|  |                             let inner_match = DefaultRenderer::find_match(config, trigger.as_str().unwrap_or("")); | ||||||
|  | 
 | ||||||
|  |                             if inner_match.is_none() { | ||||||
|  |                                 warn!("Could not find inner match with trigger: '{}'", trigger.as_str().unwrap_or("undefined")); | ||||||
|  |                                 continue
 | ||||||
|  |                             } | ||||||
|  | 
 | ||||||
|  |                             let inner_match = inner_match.unwrap(); | ||||||
|  | 
 | ||||||
|  |                             // Render the inner match
 | ||||||
|  |                             // TODO: inner arguments
 | ||||||
|  |                             let result = self.render_match(&inner_match, config, vec![]); | ||||||
|  | 
 | ||||||
|  |                             // Inner matches are only supported for text-expansions, warn the user otherwise
 | ||||||
|  |                             match result { | ||||||
|  |                                 RenderResult::Text(inner_content) => { | ||||||
|  |                                     output_map.insert(variable.name.clone(), inner_content); | ||||||
|  |                                 }, | ||||||
|  |                                 _ => { | ||||||
|  |                                     warn!("Inner matches must be of TEXT type. Mixing images is not supported yet.") | ||||||
|  |                                 }, | ||||||
|  |                             } | ||||||
|  |                         }else{  // Normal extension variables
 | ||||||
|  |                             // TODO: pass the arguments to the extension
 | ||||||
|  |                             let extension = self.extension_map.get(&variable.var_type); | ||||||
|  |                             if let Some(extension) = extension { | ||||||
|  |                                 let ext_out = extension.calculate(&variable.params, &args); | ||||||
|  |                                 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(&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() | ||||||
|  |                 }; | ||||||
|  | 
 | ||||||
|  |                 // Render any argument that may be present
 | ||||||
|  |                 let target_string = utils::render_args(&target_string, &args); | ||||||
|  | 
 | ||||||
|  |                 RenderResult::Text(target_string) | ||||||
|  |             }, | ||||||
|  | 
 | ||||||
|  |             // Image Match
 | ||||||
|  |             MatchContentType::Image(content) => { | ||||||
|  |                 // Make sure the image exist beforehand
 | ||||||
|  |                 if content.path.exists() { | ||||||
|  |                     RenderResult::Image(content.path.clone()) | ||||||
|  |                 }else{ | ||||||
|  |                     error!("Image not found in path: {:?}", content.path); | ||||||
|  |                     RenderResult::Error | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn render_passive(&self, text: &str, config: &Configs) -> RenderResult { | ||||||
|  |         // Render the matches
 | ||||||
|  |         let result = self.passive_match_regex.replace_all(&text, |caps: &Captures| { | ||||||
|  |             let match_name = if let Some(name) = caps.name("name") { | ||||||
|  |                 name.as_str() | ||||||
|  |             }else{ | ||||||
|  |                 "" | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             // Get the original matching string, useful to return the match untouched
 | ||||||
|  |             let original_match = caps.get(0).unwrap().as_str(); | ||||||
|  | 
 | ||||||
|  |             // Find the corresponding match
 | ||||||
|  |             let m = DefaultRenderer::find_match(config, match_name); | ||||||
|  | 
 | ||||||
|  |             // If no match is found, leave the match without modifications
 | ||||||
|  |             if m.is_none() { | ||||||
|  |                 return original_match.to_owned(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Compute the args by separating them
 | ||||||
|  |             let match_args = if let Some(args) = caps.name("args") { | ||||||
|  |                 args.as_str() | ||||||
|  |             }else{ | ||||||
|  |                 "" | ||||||
|  |             }; | ||||||
|  |             let args : Vec<String> = utils::split_args(match_args, | ||||||
|  |                                                        config.passive_arg_delimiter, | ||||||
|  |                                                        config.passive_arg_escape); | ||||||
|  | 
 | ||||||
|  |             let m = m.unwrap(); | ||||||
|  |             // Render the actual match
 | ||||||
|  |             let result = self.render_match(&m, &config, args); | ||||||
|  | 
 | ||||||
|  |             match result { | ||||||
|  |                 RenderResult::Text(out) => { | ||||||
|  |                     out | ||||||
|  |                 }, | ||||||
|  |                 _ => { | ||||||
|  |                     original_match.to_owned() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         RenderResult::Text(result.into_owned()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TESTS
 | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  | 
 | ||||||
|  |     fn get_renderer(config: Configs) -> DefaultRenderer { | ||||||
|  |         DefaultRenderer::new(crate::extension::get_extensions(), config) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn get_config_for(s: &str) -> Configs { | ||||||
|  |         let config : Configs = serde_yaml::from_str(s).unwrap(); | ||||||
|  |         config | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn verify_render(rendered: RenderResult, target: &str) { | ||||||
|  |         match rendered { | ||||||
|  |             RenderResult::Text(rendered) => { | ||||||
|  |                 assert_eq!(rendered, target); | ||||||
|  |             }, | ||||||
|  |             _ => { | ||||||
|  |                 assert!(false) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_passive_no_matches() { | ||||||
|  |         let text = r###" | ||||||
|  |         this text contains no matches | ||||||
|  |         "###;
 | ||||||
|  | 
 | ||||||
|  |         let config = get_config_for(r###" | ||||||
|  |         matches: | ||||||
|  |             - trigger: test | ||||||
|  |               replace: result | ||||||
|  |         "###);
 | ||||||
|  | 
 | ||||||
|  |         let renderer = get_renderer(config.clone()); | ||||||
|  | 
 | ||||||
|  |         let rendered = renderer.render_passive(text, &config); | ||||||
|  | 
 | ||||||
|  |         verify_render(rendered, text); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_passive_simple_match_no_args() { | ||||||
|  |         let text = "this is a :test"; | ||||||
|  | 
 | ||||||
|  |         let config = get_config_for(r###" | ||||||
|  |         matches: | ||||||
|  |             - trigger: ':test' | ||||||
|  |               replace: result | ||||||
|  |         "###);
 | ||||||
|  | 
 | ||||||
|  |         let renderer = get_renderer(config.clone()); | ||||||
|  | 
 | ||||||
|  |         let rendered = renderer.render_passive(text, &config); | ||||||
|  | 
 | ||||||
|  |         verify_render(rendered, "this is a result"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_passive_multiple_match_no_args() { | ||||||
|  |         let text = "this is a :test and then another :test"; | ||||||
|  | 
 | ||||||
|  |         let config = get_config_for(r###" | ||||||
|  |         matches: | ||||||
|  |             - trigger: ':test' | ||||||
|  |               replace: result | ||||||
|  |         "###);
 | ||||||
|  | 
 | ||||||
|  |         let renderer = get_renderer(config.clone()); | ||||||
|  | 
 | ||||||
|  |         let rendered = renderer.render_passive(text, &config); | ||||||
|  | 
 | ||||||
|  |         verify_render(rendered, "this is a result and then another result"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_passive_simple_match_multiline_no_args() { | ||||||
|  |         let text = r###"this is a
 | ||||||
|  |         :test | ||||||
|  |         "###;
 | ||||||
|  | 
 | ||||||
|  |         let result= r###"this is a
 | ||||||
|  |         result | ||||||
|  |         "###;
 | ||||||
|  | 
 | ||||||
|  |         let config = get_config_for(r###" | ||||||
|  |         matches: | ||||||
|  |             - trigger: ':test' | ||||||
|  |               replace: result | ||||||
|  |         "###);
 | ||||||
|  | 
 | ||||||
|  |         let renderer = get_renderer(config.clone()); | ||||||
|  | 
 | ||||||
|  |         let rendered = renderer.render_passive(text, &config); | ||||||
|  | 
 | ||||||
|  |         verify_render(rendered, result); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_passive_nested_matches_no_args() { | ||||||
|  |         let text = ":greet"; | ||||||
|  | 
 | ||||||
|  |         let config = get_config_for(r###" | ||||||
|  |         matches: | ||||||
|  |             - trigger: ':greet' | ||||||
|  |               replace: "hi {{name}}" | ||||||
|  |               vars: | ||||||
|  |                 - name: name | ||||||
|  |                   type: match | ||||||
|  |                   params: | ||||||
|  |                     trigger: ":name" | ||||||
|  | 
 | ||||||
|  |             - trigger: ':name' | ||||||
|  |               replace: john | ||||||
|  |         "###);
 | ||||||
|  | 
 | ||||||
|  |         let renderer = get_renderer(config.clone()); | ||||||
|  | 
 | ||||||
|  |         let rendered = renderer.render_passive(text, &config); | ||||||
|  | 
 | ||||||
|  |         verify_render(rendered, "hi john"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_passive_simple_match_with_args() { | ||||||
|  |         let text = ":greet/Jon/"; | ||||||
|  | 
 | ||||||
|  |         let config = get_config_for(r###" | ||||||
|  |         matches: | ||||||
|  |             - trigger: ':greet' | ||||||
|  |               replace: "Hi $0$" | ||||||
|  |         "###);
 | ||||||
|  | 
 | ||||||
|  |         let renderer = get_renderer(config.clone()); | ||||||
|  | 
 | ||||||
|  |         let rendered = renderer.render_passive(text, &config); | ||||||
|  | 
 | ||||||
|  |         verify_render(rendered, "Hi Jon"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_passive_simple_match_with_multiple_args() { | ||||||
|  |         let text = ":greet/Jon/Snow/"; | ||||||
|  | 
 | ||||||
|  |         let config = get_config_for(r###" | ||||||
|  |         matches: | ||||||
|  |             - trigger: ':greet' | ||||||
|  |               replace: "Hi $0$, there is $1$ outside" | ||||||
|  |         "###);
 | ||||||
|  | 
 | ||||||
|  |         let renderer = get_renderer(config.clone()); | ||||||
|  | 
 | ||||||
|  |         let rendered = renderer.render_passive(text, &config); | ||||||
|  | 
 | ||||||
|  |         verify_render(rendered, "Hi Jon, there is Snow outside"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_passive_simple_match_with_escaped_args() { | ||||||
|  |         let text = ":greet/Jon/10\\/12/"; | ||||||
|  | 
 | ||||||
|  |         let config = get_config_for(r###" | ||||||
|  |         matches: | ||||||
|  |             - trigger: ':greet' | ||||||
|  |               replace: "Hi $0$, today is $1$" | ||||||
|  |         "###);
 | ||||||
|  | 
 | ||||||
|  |         let renderer = get_renderer(config.clone()); | ||||||
|  | 
 | ||||||
|  |         let rendered = renderer.render_passive(text, &config); | ||||||
|  | 
 | ||||||
|  |         verify_render(rendered, "Hi Jon, today is 10/12"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_passive_simple_match_with_args_not_closed() { | ||||||
|  |         let text = ":greet/Jon/Snow"; | ||||||
|  | 
 | ||||||
|  |         let config = get_config_for(r###" | ||||||
|  |         matches: | ||||||
|  |             - trigger: ':greet' | ||||||
|  |               replace: "Hi $0$" | ||||||
|  |         "###);
 | ||||||
|  | 
 | ||||||
|  |         let renderer = get_renderer(config.clone()); | ||||||
|  | 
 | ||||||
|  |         let rendered = renderer.render_passive(text, &config); | ||||||
|  | 
 | ||||||
|  |         verify_render(rendered, "Hi JonSnow"); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								src/render/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/render/mod.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | /* | ||||||
|  |  * 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 <https://www.gnu.org/licenses/>.
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | use std::path::PathBuf; | ||||||
|  | use crate::matcher::{Match}; | ||||||
|  | use crate::config::Configs; | ||||||
|  | 
 | ||||||
|  | pub(crate) mod default; | ||||||
|  | pub(crate) mod utils; | ||||||
|  | 
 | ||||||
|  | pub trait Renderer { | ||||||
|  |     // Render a match output
 | ||||||
|  |     fn render_match(&self, m: &Match, config: &Configs, args: Vec<String>) -> RenderResult; | ||||||
|  | 
 | ||||||
|  |     // Render a passive expansion text
 | ||||||
|  |     fn render_passive(&self, text: &str, config: &Configs) -> RenderResult; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub enum RenderResult { | ||||||
|  |     Text(String), | ||||||
|  |     Image(PathBuf), | ||||||
|  |     Error | ||||||
|  | } | ||||||
							
								
								
									
										130
									
								
								src/render/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/render/utils.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,130 @@ | ||||||
|  | /* | ||||||
|  |  * This file is part of espanso. | ||||||
|  |  * | ||||||
|  |  * Copyright (C) 2020 Federico Terzi | ||||||
|  |  * | ||||||
|  |  * espanso is free software: you can redistribute it and/or modify | ||||||
|  |  * it under the terms of the GNU General Public License as published by | ||||||
|  |  * the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  * (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  * espanso is distributed in the hope that it will be useful, | ||||||
|  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  * GNU General Public License for more details. | ||||||
|  |  * | ||||||
|  |  * You should have received a copy of the GNU General Public License | ||||||
|  |  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | use regex::{Regex, Captures}; | ||||||
|  | 
 | ||||||
|  | lazy_static! { | ||||||
|  |     static ref ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)\\$").unwrap(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn render_args(text: &str, args: &Vec<String>) -> String { | ||||||
|  |     let result = ARG_REGEX.replace_all(text, |caps: &Captures| { | ||||||
|  |         let position_str  = caps.name("pos").unwrap().as_str(); | ||||||
|  |         let position = position_str.parse::<i32>().unwrap_or(-1); | ||||||
|  | 
 | ||||||
|  |         if position >= 0 && position < args.len() as i32 { | ||||||
|  |             args[position as usize].to_owned() | ||||||
|  |         }else{ | ||||||
|  |             "".to_owned() | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     result.to_string() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn split_args(text: &str, delimiter: char, escape: char) -> Vec<String> { | ||||||
|  |     let mut output = vec![]; | ||||||
|  | 
 | ||||||
|  |     // Make sure the text is not empty
 | ||||||
|  |     if text.is_empty() { | ||||||
|  |         return output | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let mut last = String::from(""); | ||||||
|  |     let mut previous : char = char::from(0); | ||||||
|  |     text.chars().into_iter().for_each(|c| { | ||||||
|  |         if c == delimiter { | ||||||
|  |             if previous != escape { | ||||||
|  |                 output.push(last.clone()); | ||||||
|  |                 last = String::from(""); | ||||||
|  |             }else{ | ||||||
|  |                 last.push(c); | ||||||
|  |             } | ||||||
|  |         }else if c == escape { | ||||||
|  |             if previous == escape { | ||||||
|  |                 last.push(c); | ||||||
|  |             } | ||||||
|  |         }else{ | ||||||
|  |             last.push(c); | ||||||
|  |         } | ||||||
|  |         previous = c; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Add the last one
 | ||||||
|  |     output.push(last); | ||||||
|  | 
 | ||||||
|  |     output | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TESTS
 | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_args_no_args() { | ||||||
|  |         let args = vec!("hello".to_owned()); | ||||||
|  |         assert_eq!(render_args("no args", &args), "no args") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_args_one_arg() { | ||||||
|  |         let args = vec!("jon".to_owned()); | ||||||
|  |         assert_eq!(render_args("hello $0$", &args), "hello jon") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_args_one_multiple_args() { | ||||||
|  |         let args = vec!("jon".to_owned(), "snow".to_owned()); | ||||||
|  |         assert_eq!(render_args("hello $0$, the $1$ is white", &args), "hello jon, the snow is white") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_render_args_out_of_range() { | ||||||
|  |         let args = vec!("jon".to_owned()); | ||||||
|  |         assert_eq!(render_args("hello $10$", &args), "hello ") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_split_args_one_arg() { | ||||||
|  |         assert_eq!(split_args("jon", '/', '\\'), vec!["jon"]) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_split_args_two_args() { | ||||||
|  |         assert_eq!(split_args("jon/snow", '/', '\\'), vec!["jon", "snow"]) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_split_args_escaping() { | ||||||
|  |         assert_eq!(split_args("jon\\/snow", '/', '\\'), vec!["jon/snow"]) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_split_args_escaping_escape() { | ||||||
|  |         assert_eq!(split_args("jon\\\\snow", '/', '\\'), vec!["jon\\snow"]) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn test_split_args_empty() { | ||||||
|  |         let empty_vec : Vec<String> = vec![]; | ||||||
|  |         assert_eq!(split_args("", '/', '\\'), empty_vec) | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user