2019-09-15 16:29:11 +00:00
|
|
|
/*
|
|
|
|
* 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/>.
|
|
|
|
*/
|
|
|
|
|
2020-04-03 18:10:06 +00:00
|
|
|
use crate::matcher::{Match, MatchReceiver};
|
2019-09-13 13:03:03 +00:00
|
|
|
use crate::keyboard::KeyboardManager;
|
2019-09-07 14:13:13 +00:00
|
|
|
use crate::config::ConfigManager;
|
2019-09-07 08:43:23 +00:00
|
|
|
use crate::config::BackendType;
|
2019-09-06 22:38:13 +00:00
|
|
|
use crate::clipboard::ClipboardManager;
|
2020-03-02 20:43:26 +00:00
|
|
|
use log::{info, warn, debug, error};
|
2019-09-12 21:24:55 +00:00
|
|
|
use crate::ui::{UIManager, MenuItem, MenuItemType};
|
2020-04-03 16:22:31 +00:00
|
|
|
use crate::event::{ActionEventReceiver, ActionType, SystemEventReceiver, SystemEvent};
|
2019-12-21 23:06:55 +00:00
|
|
|
use crate::render::{Renderer, RenderResult};
|
2019-09-12 21:24:55 +00:00
|
|
|
use std::cell::RefCell;
|
2019-09-13 13:03:03 +00:00
|
|
|
use std::process::exit;
|
2020-04-03 18:10:06 +00:00
|
|
|
use regex::{Regex};
|
2020-03-07 23:23:26 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
use std::sync::atomic::AtomicBool;
|
2020-04-03 18:10:06 +00:00
|
|
|
use std::sync::atomic::Ordering::Release;
|
2019-08-31 14:07:45 +00:00
|
|
|
|
2019-09-13 13:03:03 +00:00
|
|
|
pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>,
|
2019-12-21 23:06:55 +00:00
|
|
|
U: UIManager, R: Renderer> {
|
2019-09-13 13:03:03 +00:00
|
|
|
keyboard_manager: &'a S,
|
2019-09-07 14:13:13 +00:00
|
|
|
clipboard_manager: &'a C,
|
|
|
|
config_manager: &'a M,
|
2019-09-08 11:37:58 +00:00
|
|
|
ui_manager: &'a U,
|
2019-12-21 23:06:55 +00:00
|
|
|
renderer: &'a R,
|
2020-03-07 23:23:26 +00:00
|
|
|
is_injecting: Arc<AtomicBool>,
|
2019-09-15 11:03:21 +00:00
|
|
|
|
2019-09-12 21:24:55 +00:00
|
|
|
enabled: RefCell<bool>,
|
2019-08-31 14:07:45 +00:00
|
|
|
}
|
|
|
|
|
2019-12-21 23:06:55 +00:00
|
|
|
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
|
|
|
Engine<'a, S, C, M, U, R> {
|
2019-09-15 11:03:21 +00:00
|
|
|
pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C,
|
|
|
|
config_manager: &'a M, ui_manager: &'a U,
|
2020-03-07 23:23:26 +00:00
|
|
|
renderer: &'a R, is_injecting: Arc<AtomicBool>) -> Engine<'a, S, C, M, U, R> {
|
2019-09-12 21:24:55 +00:00
|
|
|
let enabled = RefCell::new(true);
|
2019-09-15 11:03:21 +00:00
|
|
|
|
|
|
|
Engine{keyboard_manager,
|
|
|
|
clipboard_manager,
|
|
|
|
config_manager,
|
|
|
|
ui_manager,
|
2019-12-21 23:06:55 +00:00
|
|
|
renderer,
|
2020-03-07 23:23:26 +00:00
|
|
|
is_injecting,
|
2020-01-19 22:41:11 +00:00
|
|
|
enabled,
|
2019-09-15 11:03:21 +00:00
|
|
|
}
|
2019-09-12 21:24:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn build_menu(&self) -> Vec<MenuItem> {
|
|
|
|
let mut menu = Vec::new();
|
|
|
|
|
|
|
|
let enabled = self.enabled.borrow();
|
|
|
|
let toggle_text = if *enabled {
|
|
|
|
"Disable"
|
|
|
|
}else{
|
|
|
|
"Enable"
|
|
|
|
}.to_owned();
|
|
|
|
menu.push(MenuItem{
|
|
|
|
item_type: MenuItemType::Button,
|
|
|
|
item_name: toggle_text,
|
2019-09-12 21:53:17 +00:00
|
|
|
item_id: ActionType::Toggle as i32,
|
2019-09-12 21:24:55 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
menu.push(MenuItem{
|
|
|
|
item_type: MenuItemType::Separator,
|
|
|
|
item_name: "".to_owned(),
|
|
|
|
item_id: 999,
|
|
|
|
});
|
|
|
|
|
|
|
|
menu.push(MenuItem{
|
|
|
|
item_type: MenuItemType::Button,
|
2020-04-03 16:33:56 +00:00
|
|
|
item_name: "Exit espanso".to_owned(),
|
2019-09-12 21:53:17 +00:00
|
|
|
item_id: ActionType::Exit as i32,
|
2019-09-12 21:24:55 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
menu
|
2019-08-31 14:07:45 +00:00
|
|
|
}
|
2019-12-13 22:17:53 +00:00
|
|
|
|
|
|
|
fn return_content_if_preserve_clipboard_is_enabled(&self) -> Option<String> {
|
|
|
|
// If the preserve_clipboard option is enabled, first save the current
|
|
|
|
// clipboard content in order to restore it later.
|
|
|
|
if self.config_manager.default_config().preserve_clipboard {
|
|
|
|
match self.clipboard_manager.get_clipboard() {
|
|
|
|
Some(clipboard) => {Some(clipboard)},
|
|
|
|
None => {None},
|
|
|
|
}
|
|
|
|
}else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
2019-08-31 14:07:45 +00:00
|
|
|
}
|
|
|
|
|
2019-09-15 11:03:21 +00:00
|
|
|
lazy_static! {
|
2019-09-15 13:46:24 +00:00
|
|
|
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
|
2019-09-15 11:03:21 +00:00
|
|
|
}
|
|
|
|
|
2019-12-21 23:06:55 +00:00
|
|
|
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
|
|
|
MatchReceiver for Engine<'a, S, C, M, U, R>{
|
2019-09-08 11:37:58 +00:00
|
|
|
|
2020-03-02 20:43:26 +00:00
|
|
|
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) {
|
2019-09-09 15:13:58 +00:00
|
|
|
let config = self.config_manager.active_config();
|
|
|
|
|
2020-01-26 21:30:54 +00:00
|
|
|
if !config.enable_active {
|
2019-09-09 15:13:58 +00:00
|
|
|
return;
|
|
|
|
}
|
2019-09-07 15:59:34 +00:00
|
|
|
|
2020-03-07 23:23:26 +00:00
|
|
|
// Block espanso from reinterpreting its own actions
|
|
|
|
self.is_injecting.store(true, Release);
|
2020-01-19 22:41:11 +00:00
|
|
|
|
2019-10-19 21:31:05 +00:00
|
|
|
let char_count = if trailing_separator.is_none() {
|
2020-03-02 20:43:26 +00:00
|
|
|
m.triggers[trigger_offset].chars().count() as i32
|
2019-10-19 21:31:05 +00:00
|
|
|
}else{
|
2020-03-02 20:43:26 +00:00
|
|
|
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
|
2019-10-19 21:31:05 +00:00
|
|
|
};
|
|
|
|
|
2020-04-18 17:31:24 +00:00
|
|
|
self.keyboard_manager.delete_string(&config, char_count);
|
2019-09-06 08:21:33 +00:00
|
|
|
|
2019-12-13 22:17:53 +00:00
|
|
|
let mut previous_clipboard_content : Option<String> = None;
|
|
|
|
|
2020-03-02 20:43:26 +00:00
|
|
|
let rendered = self.renderer.render_match(m, trigger_offset, config, vec![]);
|
2019-11-28 21:56:00 +00:00
|
|
|
|
2019-12-21 23:06:55 +00:00
|
|
|
match rendered {
|
|
|
|
RenderResult::Text(mut target_string) => {
|
2019-11-28 21:56:00 +00:00
|
|
|
// If a trailing separator was counted in the match, add it back to the target string
|
|
|
|
if let Some(trailing_separator) = trailing_separator {
|
|
|
|
if trailing_separator == '\r' { // If the trailing separator is a carriage return,
|
|
|
|
target_string.push('\n'); // convert it to new line
|
2019-09-15 11:03:21 +00:00
|
|
|
}else{
|
2019-11-28 21:56:00 +00:00
|
|
|
target_string.push(trailing_separator);
|
2019-09-15 11:03:21 +00:00
|
|
|
}
|
|
|
|
}
|
2019-10-24 16:48:55 +00:00
|
|
|
|
2019-11-28 21:56:00 +00:00
|
|
|
// Convert Windows style newlines into unix styles
|
|
|
|
target_string = target_string.replace("\r\n", "\n");
|
2019-10-24 16:48:55 +00:00
|
|
|
|
2019-11-28 21:56:00 +00:00
|
|
|
// 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();
|
2019-10-24 16:48:55 +00:00
|
|
|
|
2019-11-28 21:56:00 +00:00
|
|
|
// Remove the $|$ placeholder
|
|
|
|
target_string = target_string.replace("$|$", "");
|
2019-09-06 08:21:33 +00:00
|
|
|
|
2019-11-28 21:56:00 +00:00
|
|
|
// 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)
|
2019-09-07 08:43:23 +00:00
|
|
|
}else{
|
2019-11-28 21:56:00 +00:00
|
|
|
None
|
|
|
|
};
|
|
|
|
|
2020-04-03 17:20:57 +00:00
|
|
|
let backend = if m.force_clipboard {
|
|
|
|
&BackendType::Clipboard
|
|
|
|
}else if config.backend == BackendType::Auto {
|
2020-03-08 18:06:01 +00:00
|
|
|
if cfg!(target_os = "linux") {
|
|
|
|
let all_ascii = target_string.chars().all(|c| c.is_ascii());
|
|
|
|
if all_ascii {
|
|
|
|
debug!("All elements of the replacement are ascii, using Inject backend");
|
|
|
|
&BackendType::Inject
|
|
|
|
}else{
|
|
|
|
debug!("There are non-ascii characters, using Clipboard backend");
|
|
|
|
&BackendType::Clipboard
|
|
|
|
}
|
|
|
|
}else{
|
|
|
|
&BackendType::Inject
|
|
|
|
}
|
|
|
|
}else{
|
|
|
|
&config.backend
|
|
|
|
};
|
|
|
|
|
|
|
|
match backend {
|
2019-11-28 21:56:00 +00:00
|
|
|
BackendType::Inject => {
|
2020-03-08 18:10:26 +00:00
|
|
|
// To handle newlines, substitute each "\n" char with an Enter key press.
|
|
|
|
let splits = target_string.split('\n');
|
2019-11-28 21:56:00 +00:00
|
|
|
|
2020-03-08 18:10:26 +00:00
|
|
|
for (i, split) in splits.enumerate() {
|
|
|
|
if i > 0 {
|
2020-04-18 17:31:24 +00:00
|
|
|
self.keyboard_manager.send_enter(&config);
|
2019-11-28 21:56:00 +00:00
|
|
|
}
|
2020-03-08 18:10:26 +00:00
|
|
|
|
2020-04-18 17:31:24 +00:00
|
|
|
self.keyboard_manager.send_string(&config, split);
|
2019-09-07 08:43:23 +00:00
|
|
|
}
|
2019-11-28 21:56:00 +00:00
|
|
|
},
|
|
|
|
BackendType::Clipboard => {
|
2019-12-13 22:17:53 +00:00
|
|
|
// 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();
|
|
|
|
|
2019-11-28 21:56:00 +00:00
|
|
|
self.clipboard_manager.set_clipboard(&target_string);
|
2020-04-18 17:31:24 +00:00
|
|
|
self.keyboard_manager.trigger_paste(&config);
|
2019-11-28 21:56:00 +00:00
|
|
|
},
|
2020-03-08 18:06:01 +00:00
|
|
|
_ => {
|
|
|
|
error!("Unsupported backend type evaluation.");
|
|
|
|
return;
|
|
|
|
}
|
2019-11-28 21:56:00 +00:00
|
|
|
}
|
2019-09-06 08:21:33 +00:00
|
|
|
|
2019-11-28 21:56:00 +00:00
|
|
|
if let Some(moves) = cursor_rewind {
|
|
|
|
// Simulate left arrow key presses to bring the cursor into the desired position
|
2020-04-18 17:31:24 +00:00
|
|
|
self.keyboard_manager.move_cursor_left(&config, moves);
|
2019-09-07 08:43:23 +00:00
|
|
|
}
|
|
|
|
},
|
2019-12-21 23:06:55 +00:00
|
|
|
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();
|
2019-11-28 21:56:00 +00:00
|
|
|
|
2019-12-21 23:06:55 +00:00
|
|
|
self.clipboard_manager.set_clipboard_image(&image_path);
|
2020-04-18 17:31:24 +00:00
|
|
|
self.keyboard_manager.trigger_paste(&config);
|
2019-12-21 23:06:55 +00:00
|
|
|
},
|
|
|
|
RenderResult::Error => {
|
2020-03-02 20:43:26 +00:00
|
|
|
error!("Could not render match: {}", m.triggers[trigger_offset]);
|
2019-09-07 08:43:23 +00:00
|
|
|
},
|
2019-09-06 08:21:33 +00:00
|
|
|
}
|
2019-12-13 22:17:53 +00:00
|
|
|
|
|
|
|
// Restore previous clipboard content
|
|
|
|
if let Some(previous_clipboard_content) = previous_clipboard_content {
|
2020-01-26 21:40:32 +00:00
|
|
|
// Sometimes an expansion gets overwritten before pasting by the previous content
|
|
|
|
// A delay is needed to mitigate the problem
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(config.restore_clipboard_delay as u64));
|
|
|
|
|
2019-12-13 22:17:53 +00:00
|
|
|
self.clipboard_manager.set_clipboard(&previous_clipboard_content);
|
|
|
|
}
|
2020-03-07 23:23:26 +00:00
|
|
|
|
|
|
|
// Re-allow espanso to interpret actions
|
|
|
|
self.is_injecting.store(false, Release);
|
2019-08-31 14:07:45 +00:00
|
|
|
}
|
2019-09-08 11:37:58 +00:00
|
|
|
|
2019-09-14 18:13:09 +00:00
|
|
|
fn on_enable_update(&self, status: bool) {
|
2019-09-08 11:37:58 +00:00
|
|
|
let message = if status {
|
|
|
|
"espanso enabled"
|
|
|
|
}else{
|
|
|
|
"espanso disabled"
|
|
|
|
};
|
|
|
|
|
|
|
|
info!("Toggled: {}", message);
|
|
|
|
|
2019-09-12 21:24:55 +00:00
|
|
|
let mut enabled_ref = self.enabled.borrow_mut();
|
|
|
|
*enabled_ref = status;
|
|
|
|
|
2020-04-15 16:56:39 +00:00
|
|
|
let config = self.config_manager.default_config();
|
|
|
|
|
|
|
|
if config.show_notifications {
|
|
|
|
self.ui_manager.notify(message);
|
|
|
|
}
|
2019-09-08 11:37:58 +00:00
|
|
|
}
|
2020-01-18 23:30:30 +00:00
|
|
|
|
|
|
|
fn on_passive(&self) {
|
2020-01-26 21:30:54 +00:00
|
|
|
let config = self.config_manager.active_config();
|
|
|
|
|
|
|
|
if !config.enable_passive {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-03 17:38:59 +00:00
|
|
|
// Block espanso from reinterpreting its own actions
|
|
|
|
self.is_injecting.store(true, Release);
|
|
|
|
|
|
|
|
// In order to avoid pasting previous clipboard contents, we need to check if
|
|
|
|
// a new clipboard was effectively copied.
|
|
|
|
// See issue: https://github.com/federico-terzi/espanso/issues/213
|
|
|
|
let previous_clipboard = self.clipboard_manager.get_clipboard();
|
|
|
|
|
|
|
|
// Sleep for a while, giving time to effectively copy the text
|
|
|
|
std::thread::sleep(std::time::Duration::from_millis(100)); // TODO: avoid hardcoding
|
2020-01-18 23:30:30 +00:00
|
|
|
|
|
|
|
// Trigger a copy shortcut to transfer the content of the selection to the clipboard
|
2020-04-18 17:31:24 +00:00
|
|
|
self.keyboard_manager.trigger_copy(&config);
|
2020-01-18 23:30:30 +00:00
|
|
|
|
|
|
|
// 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 {
|
2020-04-03 17:38:59 +00:00
|
|
|
// Don't expand empty clipboards, as usually they are the result of an empty passive selection
|
|
|
|
if clipboard.trim().is_empty() {
|
|
|
|
info!("Avoiding passive expansion, as the user didn't select anything");
|
|
|
|
}else{
|
|
|
|
if let Some(previous_content) = previous_clipboard {
|
|
|
|
// Because of issue #213, we need to make sure the user selected something.
|
|
|
|
if clipboard == previous_content {
|
|
|
|
info!("Avoiding passive expansion, as the user didn't select anything");
|
|
|
|
} else {
|
|
|
|
info!("Passive mode activated");
|
|
|
|
|
|
|
|
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
|
2020-04-18 17:31:24 +00:00
|
|
|
self.keyboard_manager.trigger_paste(&config);
|
2020-04-03 17:38:59 +00:00
|
|
|
},
|
|
|
|
_ => {
|
|
|
|
warn!("Cannot expand passive match")
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-01-18 23:30:30 +00:00
|
|
|
}
|
|
|
|
}
|
2020-04-03 17:38:59 +00:00
|
|
|
|
|
|
|
// Re-allow espanso to interpret actions
|
|
|
|
self.is_injecting.store(false, Release);
|
2020-01-18 23:30:30 +00:00
|
|
|
}
|
2019-09-12 20:14:41 +00:00
|
|
|
}
|
|
|
|
|
2019-09-13 13:03:03 +00:00
|
|
|
impl <'a, S: KeyboardManager, C: ClipboardManager,
|
2019-12-21 23:06:55 +00:00
|
|
|
M: ConfigManager<'a>, U: UIManager, R: Renderer> ActionEventReceiver for Engine<'a, S, C, M, U, R>{
|
2019-09-12 20:14:41 +00:00
|
|
|
|
2019-09-14 10:19:11 +00:00
|
|
|
fn on_action_event(&self, e: ActionType) {
|
2019-09-12 21:24:55 +00:00
|
|
|
match e {
|
2019-09-14 10:19:11 +00:00
|
|
|
ActionType::IconClick => {
|
2019-09-12 21:24:55 +00:00
|
|
|
self.ui_manager.show_menu(self.build_menu());
|
|
|
|
},
|
2019-09-14 10:19:11 +00:00
|
|
|
ActionType::Exit => {
|
|
|
|
info!("Terminating espanso.");
|
2019-09-16 09:59:23 +00:00
|
|
|
self.ui_manager.cleanup();
|
2019-09-14 10:19:11 +00:00
|
|
|
exit(0);
|
|
|
|
},
|
|
|
|
_ => {}
|
2019-09-12 21:24:55 +00:00
|
|
|
}
|
2019-09-12 20:14:41 +00:00
|
|
|
}
|
2020-04-03 16:22:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl <'a, S: KeyboardManager, C: ClipboardManager,
|
|
|
|
M: ConfigManager<'a>, U: UIManager, R: Renderer> SystemEventReceiver for Engine<'a, S, C, M, U, R>{
|
|
|
|
|
|
|
|
fn on_system_event(&self, e: SystemEvent) {
|
|
|
|
match e {
|
|
|
|
// MacOS specific
|
|
|
|
SystemEvent::SecureInputEnabled(app_name, path) => {
|
|
|
|
info!("SecureInput has been acquired by {}, preventing espanso from working correctly. Full path: {}", app_name, path);
|
|
|
|
|
2020-04-15 16:56:39 +00:00
|
|
|
let config = self.config_manager.default_config();
|
|
|
|
if config.secure_input_notification && config.show_notifications {
|
2020-04-03 16:22:31 +00:00
|
|
|
self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
SystemEvent::SecureInputDisabled => {
|
|
|
|
info!("SecureInput has been disabled.");
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2019-08-31 14:07:45 +00:00
|
|
|
}
|