From b70d99c7abc2c910fa4f1be9d9684483895c5f44 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 15 Sep 2019 13:03:21 +0200 Subject: [PATCH] Add extension mechanism and date extension --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ src/engine.rs | 69 +++++++++++++++++++++++++++++++++++++++---- src/extension/date.rs | 30 +++++++++++++++++++ src/extension/mod.rs | 14 +++++++++ src/main.rs | 13 ++++++-- src/matcher/mod.rs | 61 ++++++++++++++++++++++++++++++++++++-- 7 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 src/extension/date.rs create mode 100644 src/extension/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f02183a..80d9ea2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -226,10 +226,12 @@ name = "espanso" version = "0.1.0" dependencies = [ "backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "cmake 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "fs2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log-panics 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index ec21038..d647c93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ fs2 = "0.4.3" serde_json = "1.0.40" log-panics = {version = "2.0.0", features = ["with-backtrace"]} backtrace = "0.3.37" +chrono = "0.4.9" +lazy_static = "1.4.0" [target.'cfg(unix)'.dependencies] libc = "0.2.62" diff --git a/src/engine.rs b/src/engine.rs index db2cfc8..0cf624f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,11 +3,15 @@ use crate::keyboard::KeyboardManager; use crate::config::ConfigManager; use crate::config::BackendType; use crate::clipboard::ClipboardManager; -use log::{info}; +use log::{info, warn, error}; use crate::ui::{UIManager, MenuItem, MenuItemType}; use crate::event::{ActionEventReceiver, ActionType}; +use crate::extension::Extension; use std::cell::RefCell; use std::process::exit; +use std::collections::HashMap; +use serde_yaml::Mapping; +use regex::{Regex, Captures}; pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> { @@ -15,14 +19,32 @@ pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager< clipboard_manager: &'a C, config_manager: &'a M, ui_manager: &'a U, + + extension_map: HashMap>, + enabled: RefCell, } impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> 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) -> 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>) -> 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); + } + let enabled = RefCell::new(true); - Engine{keyboard_manager, clipboard_manager, config_manager, ui_manager, enabled } + + Engine{keyboard_manager, + clipboard_manager, + config_manager, + ui_manager, + extension_map, + enabled + } } fn build_menu(&self) -> Vec { @@ -56,6 +78,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa } } +lazy_static! { + static ref VarRegex: Regex = Regex::new("\\{\\{\\s*(?P\\w+)\\s*\\}\\}").unwrap(); +} + impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> MatchReceiver for Engine<'a, S, C, M, U>{ @@ -68,16 +94,47 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.keyboard_manager.delete_string(m.trigger.len() as i32); + let target_string = if m._has_vars { + //self.extension_map.get("date").unwrap().calculate(Mapping::new()).unwrap() + let mut output_map = HashMap::new(); + + for variable in m.vars.iter() { + let extension = self.extension_map.get(&variable.var_type); + if let Some(extension) = extension { + let ext_out = extension.calculate(&variable.params); + if let Some(output) = ext_out { + output_map.insert(variable.name.clone(), output); + }else{ + output_map.insert(variable.name.clone(), "".to_owned()); + warn!("Could not generate output for variable: {}", variable.name); + } + }else{ + error!("No extension found for variable type: {}", variable.var_type); + } + } + + // Replace the variables + let result = VarRegex.replace_all(&m.replace, |caps: &Captures| { + let var_name = caps.name("name").unwrap().as_str(); + let output = output_map.get(var_name); + output.unwrap() + }); + + result.to_string() + }else{ // No variables, simple text substitution + m.replace.clone() + }; + 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(m.replace.as_str()); + self.keyboard_manager.send_string(&target_string); }else{ // To handle newlines, substitute each "\n" char with an Enter key press. - let splits = m.replace.lines(); + let splits = target_string.lines(); for (i, split) in splits.enumerate() { if i > 0 { @@ -89,7 +146,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa } }, BackendType::Clipboard => { - self.clipboard_manager.set_clipboard(m.replace.as_str()); + self.clipboard_manager.set_clipboard(&target_string); self.keyboard_manager.trigger_paste(); }, } diff --git a/src/extension/date.rs b/src/extension/date.rs new file mode 100644 index 0000000..bbd53db --- /dev/null +++ b/src/extension/date.rs @@ -0,0 +1,30 @@ +use serde_yaml::{Mapping, Value}; +use chrono::{DateTime, Utc}; + +pub struct DateExtension {} + +impl DateExtension { + pub fn new() -> DateExtension { + DateExtension{} + } +} + +impl super::Extension for DateExtension { + fn name(&self) -> String { + String::from("date") + } + + fn calculate(&self, params: &Mapping) -> Option { + let now: DateTime = Utc::now(); + + let format = params.get(&Value::from("format")); + + let date = if let Some(format) = format { + now.format(format.as_str().unwrap()).to_string() + }else{ + now.to_rfc2822() + }; + + Some(date) + } +} \ No newline at end of file diff --git a/src/extension/mod.rs b/src/extension/mod.rs new file mode 100644 index 0000000..e90f62b --- /dev/null +++ b/src/extension/mod.rs @@ -0,0 +1,14 @@ +use serde_yaml::Mapping; + +mod date; + +pub trait Extension { + fn name(&self) -> String; + fn calculate(&self, params: &Mapping) -> Option; +} + +pub fn get_extensions() -> Vec> { + vec![ + Box::new(date::DateExtension::new()), + ] +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 86adb86..6230963 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +#[macro_use] +extern crate lazy_static; + use std::thread; use std::fs::{File, OpenOptions}; use std::path::Path; @@ -33,6 +36,7 @@ mod matcher; mod keyboard; mod protocol; mod clipboard; +mod extension; const VERSION: &'static str = env!("CARGO_PKG_VERSION"); const LOG_FILE: &str = "espanso.log"; @@ -223,12 +227,15 @@ fn daemon_background(receive_channel: Receiver, config_set: ConfigSet) { let clipboard_manager = clipboard::get_manager(); - let manager = keyboard::get_manager(); + let keyboard_manager = keyboard::get_manager(); - let engine = Engine::new(&manager, + let extensions = extension::get_extensions(); + + let engine = Engine::new(&keyboard_manager, &clipboard_manager, &config_manager, - &ui_manager + &ui_manager, + extensions, ); let matcher = ScrollingMatcher::new(&config_manager, &engine); diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index e033b01..bda160c 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -1,13 +1,68 @@ -use serde::{Serialize, Deserialize}; +use serde::{Serialize, Deserialize, Deserializer}; use crate::event::{KeyEvent, KeyModifier}; use crate::event::KeyEventReceiver; +use serde_yaml::Mapping; +use regex::Regex; pub(crate) mod scrolling; -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Clone)] pub struct Match { pub trigger: String, - pub replace: String + pub replace: String, + pub vars: Vec, + + #[serde(skip_serializing)] + pub _has_vars: bool, +} + +impl <'de> serde::Deserialize<'de> for Match { + fn deserialize(deserializer: D) -> Result where + D: Deserializer<'de> { + + let auto_match = AutoMatch::deserialize(deserializer)?; + Ok(Match::from(&auto_match)) + } +} + +impl<'a> From<&'a AutoMatch> for Match{ + fn from(other: &'a AutoMatch) -> Self { + lazy_static! { + static ref VarRegex: Regex = Regex::new("\\{\\{\\s*(\\w+)\\s*\\}\\}").unwrap(); + } + + // Check if the match contains variables + let has_vars = VarRegex.is_match(&other.replace); + + Self { + trigger: other.trigger.clone(), + replace: other.replace.clone(), + vars: other.vars.clone(), + _has_vars: has_vars, + } + } +} + +/// Used to deserialize the Match struct before applying some custom elaboration. +#[derive(Debug, Serialize, Deserialize, Clone)] +struct AutoMatch { + pub trigger: String, + pub replace: String, + + #[serde(default = "default_vars")] + pub vars: Vec, +} + +fn default_vars() -> Vec {Vec::new()} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MatchVariable { + pub name: String, + + #[serde(rename = "type")] + pub var_type: String, + + pub params: Mapping, } pub trait MatchReceiver {