Merge branch 'dev' into snap
This commit is contained in:
commit
aaad61a044
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -370,7 +370,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "espanso"
|
||||
version = "0.4.1"
|
||||
version = "0.5.1"
|
||||
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)",
|
||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "espanso"
|
||||
version = "0.4.1"
|
||||
version = "0.5.1"
|
||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||
license = "GPL-3.0"
|
||||
description = "Cross-platform Text Expander written in Rust"
|
||||
|
@ -38,4 +38,10 @@ libc = "0.2.62"
|
|||
zip = "0.5.3"
|
||||
|
||||
[build-dependencies]
|
||||
cmake = "0.1.31"
|
||||
cmake = "0.1.31"
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "Federico Terzi <federicoterzi96@gmail.com>"
|
||||
depends = "$auto, systemd, libxtst6, libxdo3, xclip, libnotify-bin"
|
||||
section = "utility"
|
||||
license-file = ["LICENSE", "1"]
|
|
@ -7,4 +7,12 @@ steps:
|
|||
cp target/release/espanso-*.gz .
|
||||
sha256sum espanso-*.gz | awk '{ print $1 }' > espanso-linux-sha256.txt
|
||||
ls -la
|
||||
displayName: "Cargo build and packaging for Linux"
|
||||
displayName: "Cargo build and packaging for Linux"
|
||||
|
||||
- script: |
|
||||
cargo install cargo-deb
|
||||
cargo deb
|
||||
cp target/release/debian/espanso*amd64.deb espanso-debian-amd64.deb
|
||||
sha256sum espanso-*amd64.deb | awk '{ print $1 }' > espanso-debian-amd64-sha256.txt
|
||||
ls -la
|
||||
displayName: "Packaging deb package"
|
|
@ -15,7 +15,7 @@ steps:
|
|||
# Windows.
|
||||
- script: |
|
||||
curl -sSf -o rustup-init.exe https://win.rustup.rs
|
||||
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN%
|
||||
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% --default-host x86_64-pc-windows-msvc
|
||||
set PATH=%PATH%;%USERPROFILE%\.cargo\bin
|
||||
echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin"
|
||||
env:
|
||||
|
|
|
@ -307,6 +307,18 @@ void trigger_alt_shift_ins_paste() {
|
|||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Alt+Insert", 8000);
|
||||
}
|
||||
|
||||
void trigger_ctrl_alt_paste() {
|
||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Alt+v", 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
|
||||
|
||||
// Function taken from the wmlib tool source code
|
||||
|
@ -459,8 +471,8 @@ int32_t is_current_window_special() {
|
|||
if (res > 0) {
|
||||
if (strstr(class_buffer, "terminal") != NULL) {
|
||||
return 1;
|
||||
}else if (strstr(class_buffer, "URxvt") != NULL) { // Manjaro terminal
|
||||
return 1;
|
||||
}else if (strstr(class_buffer, "URxvt") != NULL) { // urxvt terminal
|
||||
return 4;
|
||||
}else if (strstr(class_buffer, "XTerm") != NULL) { // XTerm and UXTerm
|
||||
return 1;
|
||||
}else if (strstr(class_buffer, "Termite") != NULL) { // Termite
|
||||
|
|
|
@ -92,6 +92,15 @@ extern "C" void trigger_shift_ins_paste();
|
|||
*/
|
||||
extern "C" void trigger_alt_shift_ins_paste();
|
||||
|
||||
/*
|
||||
* Trigger CTRL+ALT+V pasting
|
||||
*/
|
||||
extern "C" void trigger_ctrl_alt_paste();
|
||||
|
||||
/*
|
||||
* Trigger copy shortcut ( Pressing CTRL+C )
|
||||
*/
|
||||
extern "C" void trigger_copy();
|
||||
|
||||
// SYSTEM MODULE
|
||||
|
||||
|
|
|
@ -80,6 +80,11 @@ void delete_string(int32_t count);
|
|||
*/
|
||||
void trigger_paste();
|
||||
|
||||
/*
|
||||
* Trigger normal copy ( Pressing CMD+C )
|
||||
*/
|
||||
void trigger_copy();
|
||||
|
||||
// UI
|
||||
|
||||
/*
|
||||
|
|
|
@ -101,6 +101,14 @@ void send_string(const char * string) {
|
|||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
@ -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) {
|
||||
NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
NSString *bundlePath = [frontApp bundleURL].path;
|
||||
|
@ -291,4 +332,5 @@ int32_t prompt_accessibility() {
|
|||
void open_settings_panel() {
|
||||
NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -524,6 +524,31 @@ void trigger_paste() {
|
|||
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
|
||||
|
||||
|
@ -699,3 +724,4 @@ int32_t set_clipboard_image(wchar_t *path) {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,11 @@ extern "C" void delete_string(int32_t count);
|
|||
*/
|
||||
extern "C" void trigger_paste();
|
||||
|
||||
/*
|
||||
* Send the copy keyboard shortcut (CTRL+C)
|
||||
*/
|
||||
extern "C" void trigger_copy();
|
||||
|
||||
// Detect current application commands
|
||||
|
||||
/*
|
||||
|
|
|
@ -44,4 +44,6 @@ extern {
|
|||
pub fn trigger_terminal_paste();
|
||||
pub fn trigger_shift_ins_paste();
|
||||
pub fn trigger_alt_shift_ins_paste();
|
||||
pub fn trigger_ctrl_alt_paste();
|
||||
pub fn trigger_copy();
|
||||
}
|
|
@ -59,4 +59,5 @@ extern {
|
|||
pub fn send_multi_vkey(vk: i32, count: i32);
|
||||
pub fn delete_string(count: i32);
|
||||
pub fn trigger_paste();
|
||||
pub fn trigger_copy();
|
||||
}
|
|
@ -59,4 +59,5 @@ extern {
|
|||
pub fn send_multi_vkey(vk: i32, count: i32);
|
||||
pub fn delete_string(count: i32);
|
||||
pub fn trigger_paste();
|
||||
pub fn trigger_copy();
|
||||
}
|
|
@ -21,7 +21,7 @@ extern crate dirs;
|
|||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{fs};
|
||||
use crate::matcher::Match;
|
||||
use crate::matcher::{Match, MatchVariable};
|
||||
use std::fs::{File, create_dir_all};
|
||||
use std::io::Read;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
@ -38,7 +38,7 @@ pub(crate) mod runtime;
|
|||
const DEFAULT_CONFIG_FILE_CONTENT : &str = include_str!("../res/config.yml");
|
||||
|
||||
pub const DEFAULT_CONFIG_FILE_NAME : &str = "default.yml";
|
||||
const USER_CONFIGS_FOLDER_NAME: &str = "user";
|
||||
pub const USER_CONFIGS_FOLDER_NAME: &str = "user";
|
||||
|
||||
// Default values for primitives
|
||||
fn default_name() -> String{ "default".to_owned() }
|
||||
|
@ -46,7 +46,6 @@ fn default_parent() -> String{ "self".to_owned() }
|
|||
fn default_filter_title() -> String{ "".to_owned() }
|
||||
fn default_filter_class() -> String{ "".to_owned() }
|
||||
fn default_filter_exec() -> String{ "".to_owned() }
|
||||
fn default_disabled() -> bool{ false }
|
||||
fn default_log_level() -> i32 { 0 }
|
||||
fn default_conflict_check() -> bool{ true }
|
||||
fn default_ipc_server_port() -> i32 { 34982 }
|
||||
|
@ -54,10 +53,27 @@ fn default_use_system_agent() -> bool { true }
|
|||
fn default_config_caching_interval() -> i32 { 800 }
|
||||
fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8 as char] }
|
||||
fn default_toggle_interval() -> u32 { 230 }
|
||||
fn default_preserve_clipboard() -> bool {false}
|
||||
fn default_toggle_key() -> KeyModifier { KeyModifier::ALT }
|
||||
fn default_preserve_clipboard() -> bool {true}
|
||||
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_enable_passive() -> bool { false }
|
||||
fn default_enable_active() -> bool { true }
|
||||
fn default_action_noop_interval() -> u128 { 500 }
|
||||
fn default_backspace_limit() -> i32 { 3 }
|
||||
fn default_exclude_default_matches() -> bool {false}
|
||||
fn default_restore_clipboard_delay() -> i32 { 300 }
|
||||
fn default_exclude_default_entries() -> bool {false}
|
||||
fn default_matches() -> Vec<Match> { Vec::new() }
|
||||
fn default_global_vars() -> Vec<MatchVariable> { Vec::new() }
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn default_editor() -> String{ "/bin/nano".to_owned() }
|
||||
#[cfg(target_os = "macos")]
|
||||
fn default_editor() -> String{ "/usr/bin/nano".to_owned() } // TODO: change
|
||||
#[cfg(target_os = "windows")]
|
||||
fn default_editor() -> String{ "C:\\Windows\\System32\\notepad.exe".to_owned() }
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Configs {
|
||||
|
@ -76,9 +92,6 @@ pub struct Configs {
|
|||
#[serde(default = "default_filter_exec")]
|
||||
pub filter_exec: String,
|
||||
|
||||
#[serde(default = "default_disabled")]
|
||||
pub disabled: bool,
|
||||
|
||||
#[serde(default = "default_log_level")]
|
||||
pub log_level: i32,
|
||||
|
||||
|
@ -97,7 +110,7 @@ pub struct Configs {
|
|||
#[serde(default = "default_word_separators")]
|
||||
pub word_separators: Vec<char>, // TODO: add parsing test
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(default = "default_toggle_key")]
|
||||
pub toggle_key: KeyModifier,
|
||||
|
||||
#[serde(default = "default_toggle_interval")]
|
||||
|
@ -106,20 +119,51 @@ pub struct Configs {
|
|||
#[serde(default = "default_preserve_clipboard")]
|
||||
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_enable_passive")]
|
||||
pub enable_passive: bool,
|
||||
|
||||
#[serde(default = "default_enable_active")]
|
||||
pub enable_active: bool,
|
||||
|
||||
#[serde(default = "default_action_noop_interval")]
|
||||
pub action_noop_interval: u128,
|
||||
|
||||
#[serde(default)]
|
||||
pub paste_shortcut: PasteShortcut,
|
||||
|
||||
#[serde(default = "default_backspace_limit")]
|
||||
pub backspace_limit: i32,
|
||||
|
||||
#[serde(default = "default_restore_clipboard_delay")]
|
||||
pub restore_clipboard_delay: i32,
|
||||
|
||||
#[serde(default)]
|
||||
pub backend: BackendType,
|
||||
|
||||
#[serde(default = "default_exclude_default_matches")]
|
||||
pub exclude_default_matches: bool,
|
||||
#[serde(default = "default_exclude_default_entries")]
|
||||
pub exclude_default_entries: bool,
|
||||
|
||||
#[serde(default = "default_editor")]
|
||||
pub editor: String,
|
||||
|
||||
#[serde(default = "default_matches")]
|
||||
pub matches: Vec<Match>
|
||||
pub matches: Vec<Match>,
|
||||
|
||||
#[serde(default = "default_global_vars")]
|
||||
pub global_vars: Vec<MatchVariable>
|
||||
|
||||
}
|
||||
|
||||
// Macro used to validate config fields
|
||||
|
@ -149,12 +193,18 @@ impl Configs {
|
|||
validate_field!(result, self.config_caching_interval, default_config_caching_interval());
|
||||
validate_field!(result, self.log_level, default_log_level());
|
||||
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.backspace_limit, default_backspace_limit());
|
||||
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.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());
|
||||
validate_field!(result, self.restore_clipboard_delay, default_restore_clipboard_delay());
|
||||
|
||||
result
|
||||
}
|
||||
|
@ -209,29 +259,56 @@ impl Configs {
|
|||
}
|
||||
|
||||
fn merge_config(&mut self, new_config: Configs) {
|
||||
// Merge matches
|
||||
let mut merged_matches = new_config.matches;
|
||||
let mut trigger_set = HashSet::new();
|
||||
let mut match_trigger_set = HashSet::new();
|
||||
merged_matches.iter().for_each(|m| {
|
||||
trigger_set.insert(m.trigger.clone());
|
||||
match_trigger_set.insert(m.trigger.clone());
|
||||
});
|
||||
let parent_matches : Vec<Match> = self.matches.iter().filter(|&m| {
|
||||
!trigger_set.contains(&m.trigger)
|
||||
!match_trigger_set.contains(&m.trigger)
|
||||
}).cloned().collect();
|
||||
|
||||
merged_matches.extend(parent_matches);
|
||||
self.matches = merged_matches;
|
||||
|
||||
// Merge global variables
|
||||
let mut merged_global_vars = new_config.global_vars;
|
||||
let mut vars_name_set = HashSet::new();
|
||||
merged_global_vars.iter().for_each(|m| {
|
||||
vars_name_set.insert(m.name.clone());
|
||||
});
|
||||
let parent_vars : Vec<MatchVariable> = self.global_vars.iter().filter(|&m| {
|
||||
!vars_name_set.contains(&m.name)
|
||||
}).cloned().collect();
|
||||
|
||||
merged_global_vars.extend(parent_vars);
|
||||
self.global_vars = merged_global_vars;
|
||||
}
|
||||
|
||||
fn merge_default(&mut self, default: &Configs) {
|
||||
let mut trigger_set = HashSet::new();
|
||||
// Merge matches
|
||||
let mut match_trigger_set = HashSet::new();
|
||||
self.matches.iter().for_each(|m| {
|
||||
trigger_set.insert(m.trigger.clone());
|
||||
match_trigger_set.insert(m.trigger.clone());
|
||||
});
|
||||
let default_matches : Vec<Match> = default.matches.iter().filter(|&m| {
|
||||
!trigger_set.contains(&m.trigger)
|
||||
!match_trigger_set.contains(&m.trigger)
|
||||
}).cloned().collect();
|
||||
|
||||
self.matches.extend(default_matches);
|
||||
|
||||
// Merge global variables
|
||||
let mut vars_name_set = HashSet::new();
|
||||
self.global_vars.iter().for_each(|m| {
|
||||
vars_name_set.insert(m.name.clone());
|
||||
});
|
||||
let default_vars : Vec<MatchVariable> = default.global_vars.iter().filter(|&m| {
|
||||
!vars_name_set.contains(&m.name)
|
||||
}).cloned().collect();
|
||||
|
||||
self.global_vars.extend(default_vars);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -322,9 +399,9 @@ impl ConfigSet {
|
|||
let default= configs.get(0).unwrap().clone();
|
||||
let mut specific = (&configs[1..]).to_vec().clone();
|
||||
|
||||
// Add default matches to specific configs when needed
|
||||
// Add default entries to specific configs when needed
|
||||
for config in specific.iter_mut() {
|
||||
if !config.exclude_default_matches {
|
||||
if !config.exclude_default_entries {
|
||||
config.merge_default(&default);
|
||||
}
|
||||
}
|
||||
|
@ -814,7 +891,7 @@ mod tests {
|
|||
let user_defined_path2 = create_user_config_file(data_dir.path(), "specific2.yml", r###"
|
||||
name: specific1
|
||||
|
||||
exclude_default_matches: true
|
||||
exclude_default_entries: true
|
||||
|
||||
matches:
|
||||
- trigger: "hello"
|
||||
|
@ -849,7 +926,7 @@ mod tests {
|
|||
let user_defined_path2 = create_user_config_file(data_dir.path(), "specific.zzz", r###"
|
||||
name: specific1
|
||||
|
||||
exclude_default_matches: true
|
||||
exclude_default_entries: true
|
||||
|
||||
matches:
|
||||
- trigger: "hello"
|
||||
|
@ -1166,4 +1243,83 @@ mod tests {
|
|||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(ConfigSet::has_conflicts(&config_set.default, &config_set.specific), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_set_specific_inherits_default_global_vars() {
|
||||
let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###"
|
||||
global_vars:
|
||||
- name: testvar
|
||||
type: date
|
||||
params:
|
||||
format: "%m"
|
||||
"###);
|
||||
|
||||
let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###"
|
||||
global_vars:
|
||||
- name: specificvar
|
||||
type: date
|
||||
params:
|
||||
format: "%m"
|
||||
"###);
|
||||
|
||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.len(), 1);
|
||||
assert_eq!(config_set.default.global_vars.len(), 1);
|
||||
assert_eq!(config_set.specific[0].global_vars.len(), 2);
|
||||
assert!(config_set.specific[0].global_vars.iter().any(|m| m.name == "testvar"));
|
||||
assert!(config_set.specific[0].global_vars.iter().any(|m| m.name == "specificvar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_set_default_get_variables_from_specific() {
|
||||
let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###"
|
||||
global_vars:
|
||||
- name: testvar
|
||||
type: date
|
||||
params:
|
||||
format: "%m"
|
||||
"###);
|
||||
|
||||
let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###"
|
||||
parent: default
|
||||
global_vars:
|
||||
- name: specificvar
|
||||
type: date
|
||||
params:
|
||||
format: "%m"
|
||||
"###);
|
||||
|
||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.len(), 0);
|
||||
assert_eq!(config_set.default.global_vars.len(), 2);
|
||||
assert!(config_set.default.global_vars.iter().any(|m| m.name == "testvar"));
|
||||
assert!(config_set.default.global_vars.iter().any(|m| m.name == "specificvar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_set_specific_dont_inherits_default_global_vars_when_exclude_is_on() {
|
||||
let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###"
|
||||
global_vars:
|
||||
- name: testvar
|
||||
type: date
|
||||
params:
|
||||
format: "%m"
|
||||
"###);
|
||||
|
||||
let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###"
|
||||
exclude_default_entries: true
|
||||
|
||||
global_vars:
|
||||
- name: specificvar
|
||||
type: date
|
||||
params:
|
||||
format: "%m"
|
||||
"###);
|
||||
|
||||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.len(), 1);
|
||||
assert_eq!(config_set.default.global_vars.len(), 1);
|
||||
assert_eq!(config_set.specific[0].global_vars.len(), 1);
|
||||
assert!(config_set.specific[0].global_vars.iter().any(|m| m.name == "specificvar"));
|
||||
}
|
||||
}
|
57
src/edit.rs
Normal file
57
src/edit.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 crate::config::ConfigSet;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn open_editor(config: &ConfigSet, file_path: &Path) -> bool {
|
||||
use std::process::Command;
|
||||
|
||||
// Check if another editor is defined in the environment variables
|
||||
let editor_var = std::env::var_os("EDITOR");
|
||||
let visual_var = std::env::var_os("VISUAL");
|
||||
|
||||
// Prioritize the editors specified by the environment variable, otherwise use the config
|
||||
let editor : String = if let Some(editor_var) = editor_var {
|
||||
editor_var.to_string_lossy().to_string()
|
||||
}else if let Some(visual_var) = visual_var {
|
||||
visual_var.to_string_lossy().to_string()
|
||||
}else{
|
||||
config.default.editor.clone()
|
||||
};
|
||||
|
||||
// Start the editor and wait for its termination
|
||||
let status = Command::new(editor)
|
||||
.arg(file_path)
|
||||
.spawn();
|
||||
|
||||
if let Ok(mut child) = status {
|
||||
// Wait for the user to edit the configuration
|
||||
let result = child.wait();
|
||||
|
||||
if let Ok(exit_status) = result {
|
||||
exit_status.success()
|
||||
}else{
|
||||
false
|
||||
}
|
||||
}else{
|
||||
println!("Error: could not start editor at: {}", config.default.editor);
|
||||
false
|
||||
}
|
||||
}
|
165
src/engine.rs
165
src/engine.rs
|
@ -26,43 +26,44 @@ use log::{info, warn, error};
|
|||
use crate::ui::{UIManager, MenuItem, MenuItemType};
|
||||
use crate::event::{ActionEventReceiver, ActionType};
|
||||
use crate::extension::Extension;
|
||||
use crate::render::{Renderer, RenderResult};
|
||||
use std::cell::RefCell;
|
||||
use std::process::exit;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use regex::{Regex, Captures};
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>,
|
||||
U: UIManager> {
|
||||
U: UIManager, R: Renderer> {
|
||||
keyboard_manager: &'a S,
|
||||
clipboard_manager: &'a C,
|
||||
config_manager: &'a M,
|
||||
ui_manager: &'a U,
|
||||
|
||||
extension_map: HashMap<String, Box<dyn Extension>>,
|
||||
renderer: &'a R,
|
||||
|
||||
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>
|
||||
Engine<'a, S, C, M, U> {
|
||||
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
||||
Engine<'a, S, C, M, U, R> {
|
||||
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);
|
||||
}
|
||||
|
||||
renderer: &'a R) -> Engine<'a, S, C, M, U, R> {
|
||||
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,
|
||||
clipboard_manager,
|
||||
config_manager,
|
||||
ui_manager,
|
||||
extension_map,
|
||||
enabled
|
||||
renderer,
|
||||
enabled,
|
||||
last_action_time,
|
||||
action_noop_interval,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,19 +109,38 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
|||
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! {
|
||||
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
|
||||
}
|
||||
|
||||
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
|
||||
MatchReceiver for Engine<'a, S, C, M, U>{
|
||||
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
||||
MatchReceiver for Engine<'a, S, C, M, U, R>{
|
||||
|
||||
fn on_match(&self, m: &Match, trailing_separator: Option<char>) {
|
||||
let config = self.config_manager.active_config();
|
||||
|
||||
if config.disabled {
|
||||
if !config.enable_active {
|
||||
return;
|
||||
}
|
||||
|
||||
// avoid espanso reinterpreting its own actions
|
||||
if self.check_last_action_and_set(self.action_noop_interval) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -134,40 +154,10 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
|||
|
||||
let mut previous_clipboard_content : Option<String> = None;
|
||||
|
||||
// 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()
|
||||
};
|
||||
let rendered = self.renderer.render_match(m, config, vec![]);
|
||||
|
||||
match rendered {
|
||||
RenderResult::Text(mut 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 trailing_separator == '\r' { // If the trailing separator is a carriage return,
|
||||
|
@ -234,30 +224,35 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
|||
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
|
||||
MatchContentType::Image(content) => {
|
||||
// Make sure the image exist beforehand
|
||||
if content.path.exists() {
|
||||
// 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();
|
||||
|
||||
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);
|
||||
}
|
||||
self.clipboard_manager.set_clipboard_image(&image_path);
|
||||
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
|
||||
},
|
||||
RenderResult::Error => {
|
||||
error!("Could not render match: {}", m.trigger);
|
||||
},
|
||||
}
|
||||
|
||||
// Restore previous clipboard content
|
||||
if let Some(previous_clipboard_content) = previous_clipboard_content {
|
||||
// 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));
|
||||
|
||||
self.clipboard_manager.set_clipboard(&previous_clipboard_content);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
"espanso enabled"
|
||||
}else{
|
||||
|
@ -271,10 +266,52 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
let config = self.config_manager.active_config();
|
||||
|
||||
if !config.enable_passive {
|
||||
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 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,
|
||||
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) {
|
||||
match e {
|
||||
|
|
|
@ -66,12 +66,6 @@ pub enum KeyModifier {
|
|||
OFF,
|
||||
}
|
||||
|
||||
impl Default for KeyModifier {
|
||||
fn default() -> Self {
|
||||
KeyModifier::ALT
|
||||
}
|
||||
}
|
||||
|
||||
// Receivers
|
||||
|
||||
pub trait KeyEventReceiver {
|
||||
|
|
|
@ -33,7 +33,7 @@ impl super::Extension for DateExtension {
|
|||
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 format = params.get(&Value::from("format"));
|
||||
|
|
44
src/extension/dummy.rs
Normal file
44
src/extension/dummy.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 serde_yaml::{Mapping, Value};
|
||||
|
||||
pub struct DummyExtension {}
|
||||
|
||||
impl DummyExtension {
|
||||
pub fn new() -> DummyExtension {
|
||||
DummyExtension{}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Extension for DummyExtension {
|
||||
fn name(&self) -> String {
|
||||
String::from("dummy")
|
||||
}
|
||||
|
||||
fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {
|
||||
let echo = params.get(&Value::from("echo"));
|
||||
|
||||
if let Some(echo) = echo {
|
||||
Some(echo.as_str().unwrap_or_default().to_owned())
|
||||
}else{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,10 +23,11 @@ mod date;
|
|||
mod shell;
|
||||
mod script;
|
||||
mod random;
|
||||
mod dummy;
|
||||
|
||||
pub trait Extension {
|
||||
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>> {
|
||||
|
@ -35,5 +36,6 @@ pub fn get_extensions() -> Vec<Box<dyn Extension>> {
|
|||
Box::new(shell::ShellExtension::new()),
|
||||
Box::new(script::ScriptExtension::new()),
|
||||
Box::new(random::RandomExtension::new()),
|
||||
Box::new(dummy::DummyExtension::new()),
|
||||
]
|
||||
}
|
|
@ -34,7 +34,7 @@ impl super::Extension for RandomExtension {
|
|||
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"));
|
||||
if choices.is_none() {
|
||||
warn!("No 'choices' parameter specified for random variable");
|
||||
|
@ -51,7 +51,10 @@ impl super::Extension for RandomExtension {
|
|||
|
||||
match choice {
|
||||
Some(output) => {
|
||||
return Some(output.clone())
|
||||
// Render arguments
|
||||
let output = crate::render::utils::render_args(output, args);
|
||||
|
||||
return Some(output)
|
||||
},
|
||||
None => {
|
||||
error!("Could not select a random choice.");
|
||||
|
@ -82,7 +85,7 @@ mod tests {
|
|||
params.insert(Value::from("choices"), Value::from(choices.clone()));
|
||||
|
||||
let extension = RandomExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
|
||||
|
@ -90,4 +93,30 @@ mod tests {
|
|||
|
||||
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")
|
||||
}
|
||||
|
||||
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"));
|
||||
if args.is_none() {
|
||||
warn!("No 'args' parameter specified for script variable");
|
||||
|
@ -42,10 +42,17 @@ impl super::Extension for ScriptExtension {
|
|||
}
|
||||
let args = args.unwrap().as_sequence();
|
||||
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()
|
||||
}).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 {
|
||||
Command::new(&str_args[0])
|
||||
.args(&str_args[1..])
|
||||
|
@ -55,6 +62,7 @@ impl super::Extension for ScriptExtension {
|
|||
.output()
|
||||
};
|
||||
|
||||
println!("{:?}", output);
|
||||
match output {
|
||||
Ok(output) => {
|
||||
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);
|
||||
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 std::process::Command;
|
||||
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 {}
|
||||
|
||||
|
@ -34,7 +43,7 @@ impl super::Extension for ShellExtension {
|
|||
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"));
|
||||
if cmd.is_none() {
|
||||
warn!("No 'cmd' parameter specified for shell variable");
|
||||
|
@ -42,14 +51,25 @@ impl super::Extension for ShellExtension {
|
|||
}
|
||||
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") {
|
||||
Command::new("cmd")
|
||||
.args(&["/C", cmd])
|
||||
.args(&["/C", &cmd])
|
||||
.output()
|
||||
} else {
|
||||
Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.arg(&cmd)
|
||||
.output()
|
||||
};
|
||||
|
||||
|
@ -90,7 +110,7 @@ mod tests {
|
|||
params.insert(Value::from("cmd"), Value::from("echo hello world"));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
|
||||
|
@ -108,7 +128,7 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from(true));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
assert_eq!(output.unwrap(), "hello world");
|
||||
|
@ -126,7 +146,7 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from(true));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
assert_eq!(output.unwrap(), "hello world");
|
||||
|
@ -139,7 +159,7 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from("error"));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
if cfg!(target_os = "windows") {
|
||||
|
@ -157,9 +177,37 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from(true));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -52,6 +52,8 @@ impl super::KeyboardManager for LinuxKeyboardManager {
|
|||
trigger_alt_shift_ins_paste();
|
||||
}else if is_special == 3 { // Special case for Emacs
|
||||
trigger_shift_ins_paste();
|
||||
}else if is_special == 4 { // CTRL+ALT+V used in some terminals (urxvt)
|
||||
trigger_ctrl_alt_paste();
|
||||
}else{
|
||||
trigger_terminal_paste();
|
||||
}
|
||||
|
@ -65,6 +67,9 @@ impl super::KeyboardManager for LinuxKeyboardManager {
|
|||
PasteShortcut::ShiftInsert=> {
|
||||
trigger_shift_ins_paste();
|
||||
},
|
||||
PasteShortcut::CtrlAltV => {
|
||||
trigger_ctrl_alt_paste();
|
||||
},
|
||||
_ => {
|
||||
error!("Linux backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
|
||||
}
|
||||
|
@ -81,4 +86,10 @@ impl super::KeyboardManager for LinuxKeyboardManager {
|
|||
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) {
|
||||
unsafe {delete_string(count)}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ pub trait KeyboardManager {
|
|||
fn trigger_paste(&self, shortcut: &PasteShortcut);
|
||||
fn delete_string(&self, count: i32);
|
||||
fn move_cursor_left(&self, count: i32);
|
||||
fn trigger_copy(&self);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
@ -42,6 +43,7 @@ pub enum PasteShortcut {
|
|||
CtrlV, // Classic Ctrl+V shortcut
|
||||
CtrlShiftV, // Could be used to paste without formatting in many applications
|
||||
ShiftInsert, // Often used in Linux systems
|
||||
CtrlAltV, // Used in some Linux terminals (urxvt)
|
||||
MetaV, // Corresponding to Win+V on Windows and Linux, CMD+V on macOS
|
||||
}
|
||||
|
||||
|
|
|
@ -73,4 +73,10 @@ impl super::KeyboardManager for WindowsKeyboardManager {
|
|||
send_multi_vkey(0x25, count)
|
||||
}
|
||||
}
|
||||
|
||||
fn trigger_copy(&self) {
|
||||
unsafe {
|
||||
trigger_copy();
|
||||
}
|
||||
}
|
||||
}
|
79
src/main.rs
79
src/main.rs
|
@ -32,7 +32,7 @@ use fs2::FileExt;
|
|||
use log::{info, warn, LevelFilter};
|
||||
use simplelog::{CombinedLogger, SharedLogger, TerminalMode, TermLogger, WriteLogger};
|
||||
|
||||
use crate::config::ConfigSet;
|
||||
use crate::config::{ConfigSet, ConfigManager};
|
||||
use crate::config::runtime::RuntimeConfigManager;
|
||||
use crate::engine::Engine;
|
||||
use crate::event::*;
|
||||
|
@ -46,12 +46,14 @@ use crate::package::default::DefaultPackageManager;
|
|||
use crate::package::{PackageManager, InstallResult, UpdateResult, RemoveResult};
|
||||
|
||||
mod ui;
|
||||
mod edit;
|
||||
mod event;
|
||||
mod check;
|
||||
mod utils;
|
||||
mod bridge;
|
||||
mod engine;
|
||||
mod config;
|
||||
mod render;
|
||||
mod system;
|
||||
mod context;
|
||||
mod matcher;
|
||||
|
@ -95,6 +97,10 @@ fn main() {
|
|||
.subcommand(SubCommand::with_name("toggle")
|
||||
.about("Toggle the status of the espanso replacement engine."))
|
||||
)
|
||||
.subcommand(SubCommand::with_name("edit")
|
||||
.about("Open the default text editor to edit config files and reload them automatically when exiting")
|
||||
.arg(Arg::with_name("config")
|
||||
.help("Defaults to \"default\". The configuration file name to edit (without the .yml extension).")))
|
||||
.subcommand(SubCommand::with_name("dump")
|
||||
.about("Prints all current configuration options."))
|
||||
.subcommand(SubCommand::with_name("detect")
|
||||
|
@ -162,6 +168,11 @@ fn main() {
|
|||
return;
|
||||
}
|
||||
|
||||
if let Some(matches) = matches.subcommand_matches("edit") {
|
||||
edit_main(config_set, matches);
|
||||
return;
|
||||
}
|
||||
|
||||
if matches.subcommand_matches("dump").is_some() {
|
||||
println!("{:#?}", config_set);
|
||||
return;
|
||||
|
@ -332,11 +343,14 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet) {
|
|||
|
||||
let extensions = extension::get_extensions();
|
||||
|
||||
let renderer = render::default::DefaultRenderer::new(extensions,
|
||||
config_manager.default_config().clone());
|
||||
|
||||
let engine = Engine::new(&keyboard_manager,
|
||||
&clipboard_manager,
|
||||
&config_manager,
|
||||
&ui_manager,
|
||||
extensions,
|
||||
&renderer,
|
||||
);
|
||||
|
||||
let matcher = ScrollingMatcher::new(&config_manager, &engine);
|
||||
|
@ -869,6 +883,67 @@ fn path_main(_config_set: ConfigSet, matches: &ArgMatches) {
|
|||
}
|
||||
}
|
||||
|
||||
fn edit_main(config_set: ConfigSet, matches: &ArgMatches) {
|
||||
// Determine which is the file to edit
|
||||
let config = matches.value_of("config").unwrap_or("default");
|
||||
|
||||
let config_dir = crate::context::get_config_dir();
|
||||
|
||||
let config_path = match config {
|
||||
"default" => {
|
||||
config_dir.join(crate::config::DEFAULT_CONFIG_FILE_NAME)
|
||||
},
|
||||
name => { // Otherwise, search in the user/ config folder
|
||||
config_dir.join(crate::config::USER_CONFIGS_FOLDER_NAME)
|
||||
.join(name.to_owned() + ".yml")
|
||||
}
|
||||
};
|
||||
|
||||
println!("Editing file: {:?}", &config_path);
|
||||
|
||||
// Based on the fact that the file already exists or not, we should detect in different
|
||||
// ways if a reload is needed
|
||||
let should_reload =if config_path.exists() {
|
||||
// Get the last modified date, so that we can detect if the user actually edits the file
|
||||
// before reloading
|
||||
let metadata = std::fs::metadata(&config_path).expect("cannot gather file metadata");
|
||||
let last_modified = metadata.modified().expect("cannot read file last modified date");
|
||||
|
||||
let result = crate::edit::open_editor(&config_set, &config_path);
|
||||
if result {
|
||||
let new_metadata = std::fs::metadata(&config_path).expect("cannot gather file metadata");
|
||||
let new_last_modified = new_metadata.modified().expect("cannot read file last modified date");
|
||||
|
||||
if last_modified != new_last_modified {
|
||||
println!("File has been modified, reloading configuration");
|
||||
true
|
||||
}else{
|
||||
println!("File has not been modified, avoiding reload");
|
||||
false
|
||||
}
|
||||
}else{
|
||||
false
|
||||
}
|
||||
}else{
|
||||
let result = crate::edit::open_editor(&config_set, &config_path);
|
||||
if result {
|
||||
// If the file has been created, we should reload the espanso config
|
||||
if config_path.exists() {
|
||||
println!("A new file has been created, reloading configuration");
|
||||
true
|
||||
}else{
|
||||
println!("No file has been created, avoiding reload");
|
||||
false
|
||||
}
|
||||
}else{
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if should_reload {
|
||||
restart_main(config_set)
|
||||
}
|
||||
}
|
||||
|
||||
fn acquire_lock() -> Option<File> {
|
||||
let espanso_dir = context::get_data_dir();
|
||||
|
|
|
@ -32,6 +32,7 @@ pub struct Match {
|
|||
pub trigger: String,
|
||||
pub content: MatchContentType,
|
||||
pub word: bool,
|
||||
pub passive_only: bool,
|
||||
|
||||
// Automatically calculated from the trigger, used by the matcher to check for correspondences.
|
||||
#[serde(skip_serializing)]
|
||||
|
@ -133,7 +134,8 @@ impl<'a> From<&'a AutoMatch> for Match{
|
|||
Self {
|
||||
trigger: other.trigger.clone(),
|
||||
content,
|
||||
word: other.word.clone(),
|
||||
word: other.word,
|
||||
passive_only: other.passive_only,
|
||||
_trigger_sequence: trigger_sequence,
|
||||
}
|
||||
}
|
||||
|
@ -155,10 +157,14 @@ struct AutoMatch {
|
|||
|
||||
#[serde(default = "default_word")]
|
||||
pub word: bool,
|
||||
|
||||
#[serde(default = "default_passive_only")]
|
||||
pub passive_only: bool,
|
||||
}
|
||||
|
||||
fn default_vars() -> Vec<MatchVariable> {Vec::new()}
|
||||
fn default_word() -> bool {false}
|
||||
fn default_passive_only() -> bool {false}
|
||||
fn default_replace() -> Option<String> {None}
|
||||
fn default_image_path() -> Option<String> {None}
|
||||
|
||||
|
@ -181,6 +187,7 @@ pub enum TriggerEntry {
|
|||
pub trait MatchReceiver {
|
||||
fn on_match(&self, m: &Match, trailing_separator: Option<char>);
|
||||
fn on_enable_update(&self, status: bool);
|
||||
fn on_passive(&self);
|
||||
}
|
||||
|
||||
pub trait Matcher : KeyEventReceiver {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
use crate::matcher::{Match, MatchReceiver, TriggerEntry};
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{RefCell, Ref};
|
||||
use crate::event::{KeyModifier, ActionEventReceiver, ActionType};
|
||||
use crate::config::ConfigManager;
|
||||
use crate::event::KeyModifier::BACKSPACE;
|
||||
|
@ -30,6 +30,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
|
|||
receiver: &'a R,
|
||||
current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>,
|
||||
toggle_press_time: RefCell<SystemTime>,
|
||||
passive_press_time: RefCell<SystemTime>,
|
||||
is_enabled: 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> {
|
||||
let current_set_queue = RefCell::new(VecDeque::new());
|
||||
let toggle_press_time = RefCell::new(SystemTime::now());
|
||||
let passive_press_time = RefCell::new(SystemTime::now());
|
||||
|
||||
ScrollingMatcher{
|
||||
config_manager,
|
||||
receiver,
|
||||
current_set_queue,
|
||||
toggle_press_time,
|
||||
passive_press_time,
|
||||
is_enabled: 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()
|
||||
.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);
|
||||
|
||||
if x.word {
|
||||
|
@ -193,22 +201,25 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
|||
fn handle_modifier(&self, m: KeyModifier) {
|
||||
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 == KeyModifier::OFF { return }
|
||||
let mut toggle_press_time = self.toggle_press_time.borrow_mut();
|
||||
if let Ok(elapsed) = toggle_press_time.elapsed() {
|
||||
if elapsed.as_millis() < u128::from(config.toggle_interval) {
|
||||
self.toggle();
|
||||
check_interval(&self.toggle_press_time,
|
||||
u128::from(config.toggle_interval), || {
|
||||
self.toggle();
|
||||
|
||||
let is_enabled = self.is_enabled.borrow();
|
||||
let is_enabled = self.is_enabled.borrow();
|
||||
|
||||
if !*is_enabled {
|
||||
self.current_set_queue.borrow_mut().clear();
|
||||
}
|
||||
if !*is_enabled {
|
||||
self.current_set_queue.borrow_mut().clear();
|
||||
}
|
||||
}
|
||||
|
||||
(*toggle_press_time) = SystemTime::now();
|
||||
});
|
||||
}else if m == config.passive_key {
|
||||
check_interval(&self.passive_press_time,
|
||||
u128::from(config.toggle_interval), || {
|
||||
self.receiver.on_passive();
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
524
src/render/default.rs
Normal file
524
src/render/default.rs
Normal file
|
@ -0,0 +1,524 @@
|
|||
/*
|
||||
* 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();
|
||||
static ref UNKNOWN_VARIABLE : String = "".to_string();
|
||||
}
|
||||
|
||||
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 || !config.global_vars.is_empty(){
|
||||
let mut output_map = HashMap::new();
|
||||
|
||||
// Cycle through both the local and global variables
|
||||
for variable in config.global_vars.iter().chain(&content.vars) {
|
||||
// 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
|
||||
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_or(&UNKNOWN_VARIABLE)
|
||||
});
|
||||
|
||||
result.to_string()
|
||||
}else{ // No variables, simple text substitution
|
||||
content.replace.clone()
|
||||
};
|
||||
|
||||
// Unescape any brackets (needed to be able to insert double brackets in replacement
|
||||
// text, without triggering the variable system). See issue #187
|
||||
let target_string = target_string.replace("\\{", "{")
|
||||
.replace("\\}", "}");
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_passive_local_var() {
|
||||
let text = "this is :test";
|
||||
|
||||
let config = get_config_for(r###"
|
||||
matches:
|
||||
- trigger: ':test'
|
||||
replace: "my {{output}}"
|
||||
vars:
|
||||
- name: output
|
||||
type: dummy
|
||||
params:
|
||||
echo: "result"
|
||||
"###);
|
||||
|
||||
let renderer = get_renderer(config.clone());
|
||||
|
||||
let rendered = renderer.render_passive(text, &config);
|
||||
|
||||
verify_render(rendered, "this is my result");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_passive_global_var() {
|
||||
let text = "this is :test";
|
||||
|
||||
let config = get_config_for(r###"
|
||||
global_vars:
|
||||
- name: output
|
||||
type: dummy
|
||||
params:
|
||||
echo: "result"
|
||||
matches:
|
||||
- trigger: ':test'
|
||||
replace: "my {{output}}"
|
||||
|
||||
"###);
|
||||
|
||||
let renderer = get_renderer(config.clone());
|
||||
|
||||
let rendered = renderer.render_passive(text, &config);
|
||||
|
||||
verify_render(rendered, "this is my result");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_passive_global_var_is_overridden_by_local() {
|
||||
let text = "this is :test";
|
||||
|
||||
let config = get_config_for(r###"
|
||||
global_vars:
|
||||
- name: output
|
||||
type: dummy
|
||||
params:
|
||||
echo: "result"
|
||||
matches:
|
||||
- trigger: ':test'
|
||||
replace: "my {{output}}"
|
||||
vars:
|
||||
- name: "output"
|
||||
type: dummy
|
||||
params:
|
||||
echo: "local"
|
||||
|
||||
"###);
|
||||
|
||||
let renderer = get_renderer(config.clone());
|
||||
|
||||
let rendered = renderer.render_passive(text, &config);
|
||||
|
||||
verify_render(rendered, "this is my local");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_match_with_unknown_variable_does_not_crash() {
|
||||
let text = "this is :test";
|
||||
|
||||
let config = get_config_for(r###"
|
||||
matches:
|
||||
- trigger: ':test'
|
||||
replace: "my {{unknown}}"
|
||||
"###);
|
||||
|
||||
let renderer = get_renderer(config.clone());
|
||||
|
||||
let rendered = renderer.render_passive(text, &config);
|
||||
|
||||
verify_render(rendered, "this is my ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_escaped_double_brackets_should_not_consider_them_variable() {
|
||||
let text = "this is :test";
|
||||
|
||||
let config = get_config_for(r###"
|
||||
matches:
|
||||
- trigger: ':test'
|
||||
replace: "my \\{\\{unknown\\}\\}"
|
||||
"###);
|
||||
|
||||
let renderer = get_renderer(config.clone());
|
||||
|
||||
let rendered = renderer.render_passive(text, &config);
|
||||
|
||||
verify_render(rendered, "this is my {{unknown}}");
|
||||
}
|
||||
}
|
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