Add extension mechanism and date extension

This commit is contained in:
Federico Terzi 2019-09-15 13:03:21 +02:00
parent 43dc66c25e
commit b70d99c7ab
7 changed files with 179 additions and 12 deletions

2
Cargo.lock generated
View File

@ -226,10 +226,12 @@ name = "espanso"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "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)", "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 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)", "log-panics 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -19,6 +19,8 @@ fs2 = "0.4.3"
serde_json = "1.0.40" serde_json = "1.0.40"
log-panics = {version = "2.0.0", features = ["with-backtrace"]} log-panics = {version = "2.0.0", features = ["with-backtrace"]}
backtrace = "0.3.37" backtrace = "0.3.37"
chrono = "0.4.9"
lazy_static = "1.4.0"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.62" libc = "0.2.62"

View File

@ -3,11 +3,15 @@ use crate::keyboard::KeyboardManager;
use crate::config::ConfigManager; use crate::config::ConfigManager;
use crate::config::BackendType; use crate::config::BackendType;
use crate::clipboard::ClipboardManager; use crate::clipboard::ClipboardManager;
use log::{info}; 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 std::cell::RefCell; use std::cell::RefCell;
use std::process::exit; 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>, pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>,
U: UIManager> { U: UIManager> {
@ -15,14 +19,32 @@ pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<
clipboard_manager: &'a C, clipboard_manager: &'a C,
config_manager: &'a M, config_manager: &'a M,
ui_manager: &'a U, ui_manager: &'a U,
extension_map: HashMap<String, Box<dyn Extension>>,
enabled: RefCell<bool>, enabled: RefCell<bool>,
} }
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
Engine<'a, S, C, M, 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) -> 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);
}
let enabled = RefCell::new(true); 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<MenuItem> { fn build_menu(&self) -> Vec<MenuItem> {
@ -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<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>
MatchReceiver for Engine<'a, S, C, M, U>{ 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); 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 { match config.backend {
BackendType::Inject => { BackendType::Inject => {
// Send the expected string. On linux, newlines are managed automatically // Send the expected string. On linux, newlines are managed automatically
// while on windows and macos, we need to emulate a Enter key press. // while on windows and macos, we need to emulate a Enter key press.
if cfg!(target_os = "linux") { if cfg!(target_os = "linux") {
self.keyboard_manager.send_string(m.replace.as_str()); self.keyboard_manager.send_string(&target_string);
}else{ }else{
// To handle newlines, substitute each "\n" char with an Enter key press. // 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() { for (i, split) in splits.enumerate() {
if i > 0 { if i > 0 {
@ -89,7 +146,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
} }
}, },
BackendType::Clipboard => { BackendType::Clipboard => {
self.clipboard_manager.set_clipboard(m.replace.as_str()); self.clipboard_manager.set_clipboard(&target_string);
self.keyboard_manager.trigger_paste(); self.keyboard_manager.trigger_paste();
}, },
} }

30
src/extension/date.rs Normal file
View File

@ -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<String> {
let now: DateTime<Utc> = 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)
}
}

14
src/extension/mod.rs Normal file
View File

@ -0,0 +1,14 @@
use serde_yaml::Mapping;
mod date;
pub trait Extension {
fn name(&self) -> String;
fn calculate(&self, params: &Mapping) -> Option<String>;
}
pub fn get_extensions() -> Vec<Box<dyn Extension>> {
vec![
Box::new(date::DateExtension::new()),
]
}

View File

@ -1,3 +1,6 @@
#[macro_use]
extern crate lazy_static;
use std::thread; use std::thread;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::path::Path; use std::path::Path;
@ -33,6 +36,7 @@ mod matcher;
mod keyboard; mod keyboard;
mod protocol; mod protocol;
mod clipboard; mod clipboard;
mod extension;
const VERSION: &'static str = env!("CARGO_PKG_VERSION"); const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const LOG_FILE: &str = "espanso.log"; const LOG_FILE: &str = "espanso.log";
@ -223,12 +227,15 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet) {
let clipboard_manager = clipboard::get_manager(); 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, &clipboard_manager,
&config_manager, &config_manager,
&ui_manager &ui_manager,
extensions,
); );
let matcher = ScrollingMatcher::new(&config_manager, &engine); let matcher = ScrollingMatcher::new(&config_manager, &engine);

View File

@ -1,13 +1,68 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize, Deserializer};
use crate::event::{KeyEvent, KeyModifier}; use crate::event::{KeyEvent, KeyModifier};
use crate::event::KeyEventReceiver; use crate::event::KeyEventReceiver;
use serde_yaml::Mapping;
use regex::Regex;
pub(crate) mod scrolling; pub(crate) mod scrolling;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Clone)]
pub struct Match { pub struct Match {
pub trigger: String, pub trigger: String,
pub replace: String pub replace: String,
pub vars: Vec<MatchVariable>,
#[serde(skip_serializing)]
pub _has_vars: bool,
}
impl <'de> serde::Deserialize<'de> for Match {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 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<MatchVariable>,
}
fn default_vars() -> Vec<MatchVariable> {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 { pub trait MatchReceiver {