espanso/src/engine.rs

266 lines
9.8 KiB
Rust
Raw Normal View History

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/>.
*/
use crate::matcher::{Match, MatchReceiver, MatchContentType};
2019-09-13 13:03:03 +00:00
use crate::keyboard::KeyboardManager;
2019-09-07 14:13:13 +00:00
use crate::config::ConfigManager;
use crate::config::BackendType;
use crate::clipboard::ClipboardManager;
use log::{info, warn, error};
2019-09-12 21:24:55 +00:00
use crate::ui::{UIManager, MenuItem, MenuItemType};
2019-09-14 10:19:11 +00:00
use crate::event::{ActionEventReceiver, ActionType};
use crate::extension::Extension;
2019-09-12 21:24:55 +00:00
use std::cell::RefCell;
2019-09-13 13:03:03 +00:00
use std::process::exit;
use std::collections::HashMap;
use std::path::PathBuf;
use regex::{Regex, Captures};
2019-09-13 13:03:03 +00:00
pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>,
2019-09-08 11:37:58 +00:00
U: UIManager> {
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,
extension_map: HashMap<String, Box<dyn Extension>>,
2019-09-12 21:24:55 +00:00
enabled: RefCell<bool>,
}
2019-09-13 13:03:03 +00:00
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
2019-09-08 11:37:58 +00:00
Engine<'a, S, C, M, U> {
pub fn new(keyboard_manager: &'a S, clipboard_manager: &'a C,
config_manager: &'a M, ui_manager: &'a U,
extensions: Vec<Box<dyn Extension>>) -> Engine<'a, S, C, M, U> {
// Register all the extensions
let mut extension_map = HashMap::new();
for extension in extensions.into_iter() {
extension_map.insert(extension.name(), extension);
}
2019-09-12 21:24:55 +00:00
let enabled = RefCell::new(true);
Engine{keyboard_manager,
clipboard_manager,
config_manager,
ui_manager,
extension_map,
enabled
}
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,
item_name: "Exit".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
}
}
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-13 13:03:03 +00:00
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
2019-09-08 11:37:58 +00:00
MatchReceiver for Engine<'a, S, C, M, U>{
fn on_match(&self, m: &Match, trailing_separator: Option<char>) {
2019-09-09 15:13:58 +00:00
let config = self.config_manager.active_config();
if config.disabled {
return;
}
2019-09-07 15:59:34 +00:00
let char_count = if trailing_separator.is_none() {
m.trigger.chars().count() as i32
}else{
m.trigger.chars().count() as i32 + 1 // Count also the separator
};
self.keyboard_manager.delete_string(char_count);
// Manage the different types of matches
match &m.content {
// Text Match
MatchContentType::Text(content) => {
let mut target_string = if content._has_vars {
let mut output_map = HashMap::new();
for variable in 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()
};
// If a trailing separator was counted in the match, add it back to the target string
if let Some(trailing_separator) = trailing_separator {
if trailing_separator == '\r' { // If the trailing separator is a carriage return,
target_string.push('\n'); // convert it to new line
}else{
target_string.push(trailing_separator);
}
}
// Convert Windows style newlines into unix styles
target_string = target_string.replace("\r\n", "\n");
// Calculate cursor rewind moves if a Cursor Hint is present
let index = target_string.find("$|$");
let cursor_rewind = if let Some(index) = index {
// Convert the byte index to a char index
let char_str = &target_string[0..index];
let char_index = char_str.chars().count();
let total_size = target_string.chars().count();
// Remove the $|$ placeholder
target_string = target_string.replace("$|$", "");
// Calculate the amount of rewind moves needed (LEFT ARROW).
// Subtract also 3, equal to the number of chars of the placeholder "$|$"
let moves = (total_size - char_index - 3) as i32;
Some(moves)
}else{
None
};
match config.backend {
BackendType::Inject => {
// Send the expected string. On linux, newlines are managed automatically
// while on windows and macos, we need to emulate a Enter key press.
if cfg!(target_os = "linux") {
self.keyboard_manager.send_string(&target_string);
}else{
// To handle newlines, substitute each "\n" char with an Enter key press.
let splits = target_string.split('\n');
for (i, split) in splits.enumerate() {
if i > 0 {
self.keyboard_manager.send_enter();
}
self.keyboard_manager.send_string(split);
}
}
},
BackendType::Clipboard => {
let previous_clipboard_content = self.clipboard_manager.get_clipboard().unwrap_or(String::from(""));
self.clipboard_manager.set_clipboard(&target_string);
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
self.clipboard_manager.set_clipboard(&previous_clipboard_content);
},
}
if let Some(moves) = cursor_rewind {
// Simulate left arrow key presses to bring the cursor into the desired position
self.keyboard_manager.move_cursor_left(moves);
}
},
// Image Match
MatchContentType::Image(content) => {
2019-11-28 22:06:12 +00:00
// Make sure the image exist beforehand
2019-11-28 23:01:26 +00:00
if content.path.exists() {
self.clipboard_manager.set_clipboard_image(&content.path);
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
2019-11-28 22:06:12 +00:00
}else{
2019-11-28 23:01:26 +00:00
error!("Image not found in path: {:?}", content.path);
2019-11-28 22:06:12 +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;
2019-09-08 11:37:58 +00:00
self.ui_manager.notify(message);
}
2019-09-12 20:14:41 +00:00
}
2019-09-13 13:03:03 +00:00
impl <'a, S: KeyboardManager, C: ClipboardManager,
2019-09-12 20:14:41 +00:00
M: ConfigManager<'a>, U: UIManager> ActionEventReceiver for Engine<'a, S, C, M, U>{
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.");
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
}
}