commit
386a351df7
1631
Cargo.lock
generated
1631
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
description = "Cross-platform Text Expander written in Rust"
|
description = "Cross-platform Text Expander written in Rust"
|
||||||
|
@ -14,7 +14,7 @@ version = "0.1.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
widestring = "0.4.0"
|
widestring = "0.4.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0.117", features = ["derive"] }
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
dirs = "2.0.2"
|
dirs = "2.0.2"
|
||||||
clap = "2.33.0"
|
clap = "2.33.0"
|
||||||
|
@ -22,7 +22,7 @@ regex = "1.3.1"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
simplelog = "0.7.1"
|
simplelog = "0.7.1"
|
||||||
fs2 = "0.4.3"
|
fs2 = "0.4.3"
|
||||||
serde_json = "1.0.40"
|
serde_json = "1.0.60"
|
||||||
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"
|
chrono = "0.4.9"
|
||||||
|
@ -34,6 +34,8 @@ dialoguer = "0.4.0"
|
||||||
rand = "0.7.2"
|
rand = "0.7.2"
|
||||||
zip = "0.5.3"
|
zip = "0.5.3"
|
||||||
notify = "4.0.13"
|
notify = "4.0.13"
|
||||||
|
markdown = "0.3.0"
|
||||||
|
html2text = "0.2.1"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2.62"
|
libc = "0.2.62"
|
||||||
|
|
|
@ -34,6 +34,7 @@ ___
|
||||||
* **Custom scripts** support
|
* **Custom scripts** support
|
||||||
* **Shell commands** support
|
* **Shell commands** support
|
||||||
* **App-specific** configurations
|
* **App-specific** configurations
|
||||||
|
* Support [Forms](https://espanso.org/docs/forms/)
|
||||||
* Expandable with **packages**
|
* Expandable with **packages**
|
||||||
* Built-in **package manager** for [espanso hub](https://hub.espanso.org/)
|
* Built-in **package manager** for [espanso hub](https://hub.espanso.org/)
|
||||||
* File based configuration
|
* File based configuration
|
||||||
|
|
4
build.rs
4
build.rs
|
@ -28,6 +28,10 @@ fn get_config() -> PathBuf {
|
||||||
fn print_config() {
|
fn print_config() {
|
||||||
println!("cargo:rustc-link-lib=static=winbridge");
|
println!("cargo:rustc-link-lib=static=winbridge");
|
||||||
println!("cargo:rustc-link-lib=dylib=user32");
|
println!("cargo:rustc-link-lib=dylib=user32");
|
||||||
|
#[cfg(target_env = "gnu")]
|
||||||
|
println!("cargo:rustc-link-lib=dylib=gdiplus");
|
||||||
|
#[cfg(target_env = "gnu")]
|
||||||
|
println!("cargo:rustc-link-lib=dylib=stdc++");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
// Setup status icon
|
// Setup status icon
|
||||||
if (show_icon) {
|
if (show_icon) {
|
||||||
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
||||||
|
|
||||||
[self setIcon: icon_path];
|
[self setIcon: icon_path];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,16 +60,12 @@
|
||||||
|
|
||||||
- (void) updateIcon: (char *)iconPath {
|
- (void) updateIcon: (char *)iconPath {
|
||||||
if (show_icon) {
|
if (show_icon) {
|
||||||
[myStatusItem release];
|
|
||||||
|
|
||||||
[self setIcon: iconPath];
|
[self setIcon: iconPath];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) setIcon: (char *)iconPath {
|
- (void) setIcon: (char *)iconPath {
|
||||||
if (show_icon) {
|
if (show_icon) {
|
||||||
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
|
||||||
|
|
||||||
NSString *nsIconPath = [NSString stringWithUTF8String:iconPath];
|
NSString *nsIconPath = [NSString stringWithUTF8String:iconPath];
|
||||||
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
|
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
|
||||||
[statusImage setTemplate:YES];
|
[statusImage setTemplate:YES];
|
||||||
|
|
|
@ -170,6 +170,11 @@ int32_t set_clipboard(char * text);
|
||||||
*/
|
*/
|
||||||
int32_t set_clipboard_image(char * path);
|
int32_t set_clipboard_image(char * path);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set the clipboard html
|
||||||
|
*/
|
||||||
|
int32_t set_clipboard_html(char * html, char * fallback);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If a process is currently holding SecureInput, then return 1 and set the pid pointer to the corresponding PID.
|
* If a process is currently holding SecureInput, then return 1 and set the pid pointer to the corresponding PID.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -324,6 +324,24 @@ int32_t set_clipboard_image(char *path) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32_t set_clipboard_html(char * html, char * fallback_text) {
|
||||||
|
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
|
||||||
|
NSArray *array = @[NSRTFPboardType, NSPasteboardTypeString];
|
||||||
|
[pasteboard declareTypes:array owner:nil];
|
||||||
|
|
||||||
|
NSString *nsHtml = [NSString stringWithUTF8String:html];
|
||||||
|
NSDictionary *documentAttributes = [NSDictionary dictionaryWithObjectsAndKeys:NSHTMLTextDocumentType, NSDocumentTypeDocumentAttribute, NSCharacterEncodingDocumentAttribute,[NSNumber numberWithInt:NSUTF8StringEncoding], nil];
|
||||||
|
NSAttributedString* atr = [[NSAttributedString alloc] initWithData:[nsHtml dataUsingEncoding:NSUTF8StringEncoding] options:documentAttributes documentAttributes:nil error:nil];
|
||||||
|
|
||||||
|
NSData *rtf = [atr RTFFromRange:NSMakeRange(0, [atr length])
|
||||||
|
documentAttributes:nil];
|
||||||
|
|
||||||
|
[pasteboard setData:rtf forType:NSRTFPboardType];
|
||||||
|
|
||||||
|
NSString *nsText = [NSString stringWithUTF8String:fallback_text];
|
||||||
|
[pasteboard setString:nsText forType:NSPasteboardTypeString];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// CONTEXT MENU
|
// CONTEXT MENU
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,15 @@
|
||||||
|
|
||||||
#define UNICODE
|
#define UNICODE
|
||||||
|
|
||||||
|
#ifdef __MINGW32__
|
||||||
|
# ifndef WINVER
|
||||||
|
# define WINVER 0x0606
|
||||||
|
# endif
|
||||||
|
# define STRSAFE_NO_DEPRECATE
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <winuser.h>
|
||||||
#include <strsafe.h>
|
#include <strsafe.h>
|
||||||
#include <shellapi.h>
|
#include <shellapi.h>
|
||||||
|
|
||||||
|
@ -890,3 +898,37 @@ int32_t set_clipboard_image(wchar_t *path) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inspired by https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
|
||||||
|
int32_t set_clipboard_html(char * html, wchar_t * text_fallback) {
|
||||||
|
// Get clipboard id for HTML format
|
||||||
|
static int cfid = 0;
|
||||||
|
if(!cfid) {
|
||||||
|
cfid = RegisterClipboardFormat(L"HTML Format");
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t result = 0;
|
||||||
|
const size_t html_len = strlen(html) + 1;
|
||||||
|
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, html_len * sizeof(char));
|
||||||
|
memcpy(GlobalLock(hMem), html, html_len * sizeof(char));
|
||||||
|
GlobalUnlock(hMem);
|
||||||
|
|
||||||
|
const size_t fallback_len = wcslen(text_fallback) + 1;
|
||||||
|
HGLOBAL hMemFallback = GlobalAlloc(GMEM_MOVEABLE, fallback_len * sizeof(wchar_t));
|
||||||
|
memcpy(GlobalLock(hMemFallback), text_fallback, fallback_len * sizeof(wchar_t));
|
||||||
|
GlobalUnlock(hMemFallback);
|
||||||
|
|
||||||
|
if (!OpenClipboard(NULL)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
EmptyClipboard();
|
||||||
|
if (!SetClipboardData(cfid, hMem)) {
|
||||||
|
result = -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SetClipboardData(CF_UNICODETEXT, hMemFallback)) {
|
||||||
|
result = -3;
|
||||||
|
}
|
||||||
|
CloseClipboard();
|
||||||
|
GlobalFree(hMem);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
@ -179,6 +179,13 @@ extern "C" int32_t set_clipboard(wchar_t * text);
|
||||||
*/
|
*/
|
||||||
extern "C" int32_t set_clipboard_image(wchar_t * path);
|
extern "C" int32_t set_clipboard_image(wchar_t * path);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set clipboard HTML. Notice how in this case, text is not a wide char but instead
|
||||||
|
* uses the UTF8 encoding.
|
||||||
|
* Also set the text fallback, in case some applications don't support HTML clipboard.
|
||||||
|
*/
|
||||||
|
extern "C" int32_t set_clipboard_html(char * html, wchar_t * text_fallback);
|
||||||
|
|
||||||
// PROCESSES
|
// PROCESSES
|
||||||
|
|
||||||
extern "C" int32_t start_process(wchar_t * cmd);
|
extern "C" int32_t start_process(wchar_t * cmd);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: espanso
|
name: espanso
|
||||||
version: 0.7.2
|
version: 0.7.3
|
||||||
summary: A Cross-platform Text Expander written in Rust
|
summary: A Cross-platform Text Expander written in Rust
|
||||||
description: |
|
description: |
|
||||||
espanso is a Cross-platform, Text Expander written in Rust.
|
espanso is a Cross-platform, Text Expander written in Rust.
|
||||||
|
|
|
@ -51,6 +51,7 @@ extern "C" {
|
||||||
pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32;
|
pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32;
|
||||||
pub fn set_clipboard(text: *const c_char) -> i32;
|
pub fn set_clipboard(text: *const c_char) -> i32;
|
||||||
pub fn set_clipboard_image(path: *const c_char) -> i32;
|
pub fn set_clipboard_image(path: *const c_char) -> i32;
|
||||||
|
pub fn set_clipboard_html(html: *const c_char, text_fallback: *const c_char) -> i32;
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void));
|
pub fn register_icon_click_callback(cb: extern "C" fn(_self: *mut c_void));
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::os::raw::c_void;
|
use std::os::raw::{c_char, c_void};
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct WindowsMenuItem {
|
pub struct WindowsMenuItem {
|
||||||
|
@ -55,6 +55,7 @@ extern "C" {
|
||||||
pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32;
|
pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32;
|
||||||
pub fn set_clipboard(payload: *const u16) -> i32;
|
pub fn set_clipboard(payload: *const u16) -> i32;
|
||||||
pub fn set_clipboard_image(path: *const u16) -> i32;
|
pub fn set_clipboard_image(path: *const u16) -> i32;
|
||||||
|
pub fn set_clipboard_html(html: *const c_char, text_fallback: *const u16) -> i32;
|
||||||
|
|
||||||
// KEYBOARD
|
// KEYBOARD
|
||||||
pub fn register_keypress_callback(
|
pub fn register_keypress_callback(
|
||||||
|
|
|
@ -89,6 +89,31 @@ impl super::ClipboardManager for LinuxClipboardManager {
|
||||||
error!("Could not set image clipboard: {}", e);
|
error!("Could not set image clipboard: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_clipboard_html(&self, html: &str) {
|
||||||
|
let res = Command::new("xclip")
|
||||||
|
.args(&["-sel", "clip", "-t", "text/html"])
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.spawn();
|
||||||
|
|
||||||
|
if let Ok(mut child) = res {
|
||||||
|
let stdin = child.stdin.as_mut();
|
||||||
|
|
||||||
|
if let Some(output) = stdin {
|
||||||
|
let res = output.write_all(html.as_bytes());
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
error!("Could not set clipboard html: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = child.wait();
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
error!("Could not set clipboard html: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LinuxClipboardManager {
|
impl LinuxClipboardManager {
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl super::ClipboardManager for MacClipboardManager {
|
||||||
fn get_clipboard(&self) -> Option<String> {
|
fn get_clipboard(&self) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut buffer: [c_char; 2000] = [0; 2000];
|
let mut buffer: [c_char; 2000] = [0; 2000];
|
||||||
let res = get_clipboard(buffer.as_mut_ptr(), buffer.len() as i32);
|
let res = get_clipboard(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||||
|
|
||||||
if res > 0 {
|
if res > 0 {
|
||||||
let c_string = CStr::from_ptr(buffer.as_ptr());
|
let c_string = CStr::from_ptr(buffer.as_ptr());
|
||||||
|
@ -65,6 +65,18 @@ impl super::ClipboardManager for MacClipboardManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_clipboard_html(&self, html: &str) {
|
||||||
|
// Render the text fallback for those applications that don't support HTML clipboard
|
||||||
|
let decorator = html2text::render::text_renderer::TrivialDecorator::new();
|
||||||
|
let text_fallback =
|
||||||
|
html2text::from_read_with_decorator(html.as_bytes(), 1000000, decorator);
|
||||||
|
unsafe {
|
||||||
|
let payload_c = CString::new(html).expect("unable to create CString for html content");
|
||||||
|
let payload_fallback_c = CString::new(text_fallback).unwrap();
|
||||||
|
set_clipboard_html(payload_c.as_ptr(), payload_fallback_c.as_ptr());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MacClipboardManager {
|
impl MacClipboardManager {
|
||||||
|
|
|
@ -32,6 +32,7 @@ pub trait ClipboardManager {
|
||||||
fn get_clipboard(&self) -> Option<String>;
|
fn get_clipboard(&self) -> Option<String>;
|
||||||
fn set_clipboard(&self, payload: &str);
|
fn set_clipboard(&self, payload: &str);
|
||||||
fn set_clipboard_image(&self, image_path: &Path);
|
fn set_clipboard_image(&self, image_path: &Path);
|
||||||
|
fn set_clipboard_html(&self, html: &str);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LINUX IMPLEMENTATION
|
// LINUX IMPLEMENTATION
|
||||||
|
|
|
@ -17,8 +17,10 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::bridge::windows::{get_clipboard, set_clipboard, set_clipboard_image};
|
use crate::bridge::windows::{
|
||||||
use std::path::Path;
|
get_clipboard, set_clipboard, set_clipboard_html, set_clipboard_image,
|
||||||
|
};
|
||||||
|
use std::{ffi::CString, path::Path};
|
||||||
use widestring::U16CString;
|
use widestring::U16CString;
|
||||||
|
|
||||||
pub struct WindowsClipboardManager {}
|
pub struct WindowsClipboardManager {}
|
||||||
|
@ -33,7 +35,7 @@ impl super::ClipboardManager for WindowsClipboardManager {
|
||||||
fn get_clipboard(&self) -> Option<String> {
|
fn get_clipboard(&self) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut buffer: [u16; 2000] = [0; 2000];
|
let mut buffer: [u16; 2000] = [0; 2000];
|
||||||
let res = get_clipboard(buffer.as_mut_ptr(), buffer.len() as i32);
|
let res = get_clipboard(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||||
|
|
||||||
if res > 0 {
|
if res > 0 {
|
||||||
let c_string = U16CString::from_ptr_str(buffer.as_ptr());
|
let c_string = U16CString::from_ptr_str(buffer.as_ptr());
|
||||||
|
@ -60,4 +62,58 @@ impl super::ClipboardManager for WindowsClipboardManager {
|
||||||
set_clipboard_image(payload_c.as_ptr());
|
set_clipboard_image(payload_c.as_ptr());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_clipboard_html(&self, html: &str) {
|
||||||
|
// In order to set the HTML clipboard, we have to create a prefix with a specific format
|
||||||
|
// For more information, look here:
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
|
||||||
|
// https://docs.microsoft.com/en-za/troubleshoot/cpp/add-html-code-clipboard
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
tokens.push("Version:0.9");
|
||||||
|
tokens.push("StartHTML:<<STR*#>");
|
||||||
|
tokens.push("EndHTML:<<END*#>");
|
||||||
|
tokens.push("StartFragment:<<SFG#*>");
|
||||||
|
tokens.push("EndFragment:<<EFG#*>");
|
||||||
|
tokens.push("<html>");
|
||||||
|
tokens.push("<body>");
|
||||||
|
let content = format!("<!--StartFragment-->{}<!--EndFragment-->", html);
|
||||||
|
tokens.push(&content);
|
||||||
|
tokens.push("</body>");
|
||||||
|
tokens.push("</html>");
|
||||||
|
|
||||||
|
let mut render = tokens.join("\r\n");
|
||||||
|
|
||||||
|
// Now replace the placeholders with the actual positions
|
||||||
|
render = render.replace(
|
||||||
|
"<<STR*#>",
|
||||||
|
&format!("{:0>8}", render.find("<html>").unwrap_or_default()),
|
||||||
|
);
|
||||||
|
render = render.replace("<<END*#>", &format!("{:0>8}", render.len()));
|
||||||
|
render = render.replace(
|
||||||
|
"<<SFG#*>",
|
||||||
|
&format!(
|
||||||
|
"{:0>8}",
|
||||||
|
render.find("<!--StartFragment-->").unwrap_or_default()
|
||||||
|
+ "<!--StartFragment-->".len()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
render = render.replace(
|
||||||
|
"<<EFG#*>",
|
||||||
|
&format!(
|
||||||
|
"{:0>8}",
|
||||||
|
render.find("<!--EndFragment-->").unwrap_or_default()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render the text fallback for those applications that don't support HTML clipboard
|
||||||
|
let decorator = html2text::render::text_renderer::TrivialDecorator::new();
|
||||||
|
let text_fallback =
|
||||||
|
html2text::from_read_with_decorator(html.as_bytes(), 1000000, decorator);
|
||||||
|
unsafe {
|
||||||
|
let payload_c =
|
||||||
|
CString::new(render).expect("unable to create CString for html content");
|
||||||
|
let payload_fallback_c = U16CString::from_str(text_fallback).unwrap();
|
||||||
|
set_clipboard_html(payload_c.as_ptr(), payload_fallback_c.as_ptr());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,6 +159,10 @@ fn default_post_inject_delay() -> u64 {
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_wait_for_modifiers_release() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Configs {
|
pub struct Configs {
|
||||||
#[serde(default = "default_name")]
|
#[serde(default = "default_name")]
|
||||||
|
@ -283,6 +287,9 @@ pub struct Configs {
|
||||||
|
|
||||||
#[serde(default = "default_modulo_path")]
|
#[serde(default = "default_modulo_path")]
|
||||||
pub modulo_path: Option<String>,
|
pub modulo_path: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_wait_for_modifiers_release")]
|
||||||
|
pub wait_for_modifiers_release: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Macro used to validate config fields
|
// Macro used to validate config fields
|
||||||
|
|
|
@ -154,8 +154,14 @@ impl<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inject_text(&self, config: &Configs, target_string: &str, force_clipboard: bool) {
|
fn inject_text(
|
||||||
let backend = if force_clipboard {
|
&self,
|
||||||
|
config: &Configs,
|
||||||
|
target_string: &str,
|
||||||
|
force_clipboard: bool,
|
||||||
|
is_html: bool,
|
||||||
|
) {
|
||||||
|
let backend = if force_clipboard || is_html {
|
||||||
&BackendType::Clipboard
|
&BackendType::Clipboard
|
||||||
} else if config.backend == BackendType::Auto {
|
} else if config.backend == BackendType::Auto {
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(target_os = "linux") {
|
||||||
|
@ -188,7 +194,12 @@ impl<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackendType::Clipboard => {
|
BackendType::Clipboard => {
|
||||||
self.clipboard_manager.set_clipboard(&target_string);
|
if !is_html {
|
||||||
|
self.clipboard_manager.set_clipboard(&target_string);
|
||||||
|
} else {
|
||||||
|
self.clipboard_manager.set_clipboard_html(&target_string);
|
||||||
|
}
|
||||||
|
|
||||||
self.keyboard_manager.trigger_paste(&config);
|
self.keyboard_manager.trigger_paste(&config);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -220,6 +231,13 @@ impl<
|
||||||
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
|
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If configured to do so, wait until the modifier keys are released (or timeout) so
|
||||||
|
// that we avoid unwanted interactions. As an example, see:
|
||||||
|
// https://github.com/federico-terzi/espanso/issues/470
|
||||||
|
if config.wait_for_modifiers_release {
|
||||||
|
crate::keyboard::wait_for_modifiers_release();
|
||||||
|
}
|
||||||
|
|
||||||
if !skip_delete {
|
if !skip_delete {
|
||||||
self.keyboard_manager.delete_string(&config, char_count);
|
self.keyboard_manager.delete_string(&config, char_count);
|
||||||
}
|
}
|
||||||
|
@ -270,10 +288,10 @@ impl<
|
||||||
// clipboard content to restore it later.
|
// clipboard content to restore it later.
|
||||||
previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
|
previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
|
||||||
|
|
||||||
self.inject_text(&config, &target_string, m.force_clipboard);
|
self.inject_text(&config, &target_string, m.force_clipboard, m.is_html);
|
||||||
|
|
||||||
// Disallow undo backspace if cursor positioning is used
|
// Disallow undo backspace if cursor positioning is used or text is HTML
|
||||||
if cursor_rewind.is_none() {
|
if cursor_rewind.is_none() && !m.is_html {
|
||||||
expansion_data = Some((
|
expansion_data = Some((
|
||||||
m.triggers[trigger_offset].clone(),
|
m.triggers[trigger_offset].clone(),
|
||||||
target_string.chars().count() as i32,
|
target_string.chars().count() as i32,
|
||||||
|
@ -350,7 +368,7 @@ impl<
|
||||||
self.keyboard_manager
|
self.keyboard_manager
|
||||||
.delete_string(&config, *injected_text_len - 1);
|
.delete_string(&config, *injected_text_len - 1);
|
||||||
// Restore previous text
|
// Restore previous text
|
||||||
self.inject_text(&config, trigger_string, false);
|
self.inject_text(&config, trigger_string, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ use crate::extension::ExtensionResult;
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::ExtensionOut;
|
||||||
|
|
||||||
pub struct ClipboardExtension {
|
pub struct ClipboardExtension {
|
||||||
clipboard_manager: Box<dyn ClipboardManager>,
|
clipboard_manager: Box<dyn ClipboardManager>,
|
||||||
}
|
}
|
||||||
|
@ -42,11 +44,11 @@ impl super::Extension for ClipboardExtension {
|
||||||
_: &Mapping,
|
_: &Mapping,
|
||||||
_: &Vec<String>,
|
_: &Vec<String>,
|
||||||
_: &HashMap<String, ExtensionResult>,
|
_: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult> {
|
) -> ExtensionOut {
|
||||||
if let Some(clipboard) = self.clipboard_manager.get_clipboard() {
|
if let Some(clipboard) = self.clipboard_manager.get_clipboard() {
|
||||||
Some(ExtensionResult::Single(clipboard))
|
Ok(Some(ExtensionResult::Single(clipboard)))
|
||||||
} else {
|
} else {
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ use chrono::{DateTime, Duration, Local};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::ExtensionOut;
|
||||||
|
|
||||||
pub struct DateExtension {}
|
pub struct DateExtension {}
|
||||||
|
|
||||||
impl DateExtension {
|
impl DateExtension {
|
||||||
|
@ -40,7 +42,7 @@ impl super::Extension for DateExtension {
|
||||||
params: &Mapping,
|
params: &Mapping,
|
||||||
_: &Vec<String>,
|
_: &Vec<String>,
|
||||||
_: &HashMap<String, ExtensionResult>,
|
_: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult> {
|
) -> ExtensionOut {
|
||||||
let mut now: DateTime<Local> = Local::now();
|
let mut now: DateTime<Local> = Local::now();
|
||||||
|
|
||||||
// Compute the given offset
|
// Compute the given offset
|
||||||
|
@ -59,6 +61,6 @@ impl super::Extension for DateExtension {
|
||||||
now.to_rfc2822()
|
now.to_rfc2822()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(ExtensionResult::Single(date))
|
Ok(Some(ExtensionResult::Single(date)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,15 +43,15 @@ impl super::Extension for DummyExtension {
|
||||||
params: &Mapping,
|
params: &Mapping,
|
||||||
_: &Vec<String>,
|
_: &Vec<String>,
|
||||||
_: &HashMap<String, ExtensionResult>,
|
_: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult> {
|
) -> super::ExtensionOut {
|
||||||
let echo = params.get(&Value::from("echo"));
|
let echo = params.get(&Value::from("echo"));
|
||||||
|
|
||||||
if let Some(echo) = echo {
|
if let Some(echo) = echo {
|
||||||
Some(ExtensionResult::Single(
|
Ok(Some(ExtensionResult::Single(
|
||||||
echo.as_str().unwrap_or_default().to_owned(),
|
echo.as_str().unwrap_or_default().to_owned(),
|
||||||
))
|
)))
|
||||||
} else {
|
} else {
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,13 +43,13 @@ impl super::Extension for FormExtension {
|
||||||
params: &Mapping,
|
params: &Mapping,
|
||||||
_: &Vec<String>,
|
_: &Vec<String>,
|
||||||
_: &HashMap<String, ExtensionResult>,
|
_: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult> {
|
) -> super::ExtensionOut {
|
||||||
let layout = params.get(&Value::from("layout"));
|
let layout = params.get(&Value::from("layout"));
|
||||||
let layout = if let Some(value) = layout {
|
let layout = if let Some(value) = layout {
|
||||||
value.as_str().unwrap_or_default().to_string()
|
value.as_str().unwrap_or_default().to_string()
|
||||||
} else {
|
} else {
|
||||||
error!("invoking form extension without specifying a layout");
|
error!("invoking form extension without specifying a layout");
|
||||||
return None;
|
return Err(super::ExtensionError::Internal);
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut form_config = Mapping::new();
|
let mut form_config = Mapping::new();
|
||||||
|
@ -81,16 +81,22 @@ impl super::Extension for FormExtension {
|
||||||
let json: Result<HashMap<String, String>, _> = serde_json::from_str(&output);
|
let json: Result<HashMap<String, String>, _> = serde_json::from_str(&output);
|
||||||
match json {
|
match json {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
return Some(ExtensionResult::Multiple(json));
|
// Check if the JSON is empty. In those cases, it means the user exited
|
||||||
|
// the form before submitting it, therefore the expansion should stop
|
||||||
|
if json.is_empty() {
|
||||||
|
return Err(super::ExtensionError::Aborted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Some(ExtensionResult::Multiple(json)));
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
error!("modulo json parsing error: {}", error);
|
error!("modulo json parsing error: {}", error);
|
||||||
return None;
|
return Err(super::ExtensionError::Internal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("modulo form didn't return any output");
|
error!("modulo form didn't return any output");
|
||||||
return None;
|
return Err(super::ExtensionError::Internal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,17 @@ pub enum ExtensionResult {
|
||||||
Multiple(HashMap<String, String>),
|
Multiple(HashMap<String, String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ExtensionError {
|
||||||
|
// Returned by an extension if an internal process occurred
|
||||||
|
Internal,
|
||||||
|
// Returned by an extension if the user aborted the expansion
|
||||||
|
// for example when pressing ESC inside a FormExtension.
|
||||||
|
Aborted,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ExtensionOut = Result<Option<ExtensionResult>, ExtensionError>;
|
||||||
|
|
||||||
pub trait Extension {
|
pub trait Extension {
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
fn calculate(
|
fn calculate(
|
||||||
|
@ -45,7 +56,7 @@ pub trait Extension {
|
||||||
params: &Mapping,
|
params: &Mapping,
|
||||||
args: &Vec<String>,
|
args: &Vec<String>,
|
||||||
current_vars: &HashMap<String, ExtensionResult>,
|
current_vars: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult>;
|
) -> ExtensionOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_extensions(
|
pub fn get_extensions(
|
||||||
|
|
|
@ -39,7 +39,7 @@ impl super::Extension for MultiEchoExtension {
|
||||||
params: &Mapping,
|
params: &Mapping,
|
||||||
_: &Vec<String>,
|
_: &Vec<String>,
|
||||||
_: &HashMap<String, ExtensionResult>,
|
_: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult> {
|
) -> super::ExtensionOut {
|
||||||
let mut output: HashMap<String, String> = HashMap::new();
|
let mut output: HashMap<String, String> = HashMap::new();
|
||||||
for (key, value) in params.iter() {
|
for (key, value) in params.iter() {
|
||||||
if let Some(key) = key.as_str() {
|
if let Some(key) = key.as_str() {
|
||||||
|
@ -48,6 +48,6 @@ impl super::Extension for MultiEchoExtension {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(ExtensionResult::Multiple(output))
|
Ok(Some(ExtensionResult::Multiple(output)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,11 +41,11 @@ impl super::Extension for RandomExtension {
|
||||||
params: &Mapping,
|
params: &Mapping,
|
||||||
args: &Vec<String>,
|
args: &Vec<String>,
|
||||||
_: &HashMap<String, ExtensionResult>,
|
_: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult> {
|
) -> super::ExtensionOut {
|
||||||
let choices = params.get(&Value::from("choices"));
|
let choices = params.get(&Value::from("choices"));
|
||||||
if choices.is_none() {
|
if choices.is_none() {
|
||||||
warn!("No 'choices' parameter specified for random variable");
|
warn!("No 'choices' parameter specified for random variable");
|
||||||
return None;
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let choices = choices.unwrap().as_sequence();
|
let choices = choices.unwrap().as_sequence();
|
||||||
if let Some(choices) = choices {
|
if let Some(choices) = choices {
|
||||||
|
@ -62,17 +62,17 @@ impl super::Extension for RandomExtension {
|
||||||
// Render arguments
|
// Render arguments
|
||||||
let output = crate::render::utils::render_args(output, args);
|
let output = crate::render::utils::render_args(output, args);
|
||||||
|
|
||||||
return Some(ExtensionResult::Single(output));
|
return Ok(Some(ExtensionResult::Single(output)));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
error!("Could not select a random choice.");
|
error!("Could not select a random choice.");
|
||||||
return None;
|
return Err(super::ExtensionError::Internal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
error!("choices array have an invalid format '{:?}'", choices);
|
error!("choices array have an invalid format '{:?}'", choices);
|
||||||
None
|
Err(super::ExtensionError::Internal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,9 @@ mod tests {
|
||||||
params.insert(Value::from("choices"), Value::from(choices.clone()));
|
params.insert(Value::from("choices"), Value::from(choices.clone()));
|
||||||
|
|
||||||
let extension = RandomExtension::new();
|
let extension = RandomExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec![], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
|
@ -106,7 +108,9 @@ mod tests {
|
||||||
params.insert(Value::from("choices"), Value::from(choices.clone()));
|
params.insert(Value::from("choices"), Value::from(choices.clone()));
|
||||||
|
|
||||||
let extension = RandomExtension::new();
|
let extension = RandomExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec!["test".to_owned()], &HashMap::new());
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec!["test".to_owned()], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
|
|
|
@ -42,11 +42,11 @@ impl super::Extension for ScriptExtension {
|
||||||
params: &Mapping,
|
params: &Mapping,
|
||||||
user_args: &Vec<String>,
|
user_args: &Vec<String>,
|
||||||
vars: &HashMap<String, ExtensionResult>,
|
vars: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult> {
|
) -> super::ExtensionOut {
|
||||||
let args = params.get(&Value::from("args"));
|
let args = params.get(&Value::from("args"));
|
||||||
if args.is_none() {
|
if args.is_none() {
|
||||||
warn!("No 'args' parameter specified for script variable");
|
warn!("No 'args' parameter specified for script variable");
|
||||||
return None;
|
return Err(super::ExtensionError::Internal);
|
||||||
}
|
}
|
||||||
let args = args.unwrap().as_sequence();
|
let args = args.unwrap().as_sequence();
|
||||||
if let Some(args) = args {
|
if let Some(args) = args {
|
||||||
|
@ -145,17 +145,17 @@ impl super::Extension for ScriptExtension {
|
||||||
output_str = output_str.trim().to_owned()
|
output_str = output_str.trim().to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Some(ExtensionResult::Single(output_str));
|
return Ok(Some(ExtensionResult::Single(output_str)));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Could not execute script '{:?}', error: {}", args, e);
|
error!("Could not execute script '{:?}', error: {}", args, e);
|
||||||
return None;
|
return Err(super::ExtensionError::Internal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
error!("Could not execute script with args '{:?}'", args);
|
error!("Could not execute script with args '{:?}'", args);
|
||||||
None
|
Err(super::ExtensionError::Internal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
let extension = ScriptExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new()).unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -194,7 +194,7 @@ mod tests {
|
||||||
params.insert(Value::from("trim"), Value::from(false));
|
params.insert(Value::from("trim"), Value::from(false));
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
let extension = ScriptExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
let output = extension.calculate(¶ms, &vec![], &HashMap::new()).unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -213,7 +213,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
let extension = ScriptExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new());
|
let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()).unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -233,7 +233,7 @@ mod tests {
|
||||||
params.insert(Value::from("inject_args"), Value::from(true));
|
params.insert(Value::from("inject_args"), Value::from(true));
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
let extension = ScriptExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new());
|
let output = extension.calculate(¶ms, &vec!["jon".to_owned()], &HashMap::new()).unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -261,7 +261,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let extension = ScriptExtension::new();
|
let extension = ScriptExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![], &vars);
|
let output = extension.calculate(¶ms, &vec![], &vars).unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -25,11 +25,8 @@ use std::collections::HashMap;
|
||||||
use std::process::{Command, Output};
|
use std::process::{Command, Output};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref POS_ARG_REGEX: Regex = if cfg!(target_os = "windows") {
|
static ref UNIX_POS_ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)").unwrap();
|
||||||
Regex::new("%(?P<pos>\\d+)").unwrap()
|
static ref WIN_POS_ARG_REGEX: Regex = Regex::new("%(?P<pos>\\d+)").unwrap();
|
||||||
} else {
|
|
||||||
Regex::new("\\$(?P<pos>\\d+)").unwrap()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Shell {
|
pub enum Shell {
|
||||||
|
@ -121,6 +118,14 @@ impl Shell {
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_arg_regex(&self) -> &Regex {
|
||||||
|
let regex = match self {
|
||||||
|
Shell::Cmd | Shell::Powershell => &*WIN_POS_ARG_REGEX,
|
||||||
|
_ => &*UNIX_POS_ARG_REGEX,
|
||||||
|
};
|
||||||
|
regex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Shell {
|
impl Default for Shell {
|
||||||
|
@ -155,27 +160,20 @@ impl super::Extension for ShellExtension {
|
||||||
params: &Mapping,
|
params: &Mapping,
|
||||||
args: &Vec<String>,
|
args: &Vec<String>,
|
||||||
vars: &HashMap<String, ExtensionResult>,
|
vars: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult> {
|
) -> super::ExtensionOut {
|
||||||
let cmd = params.get(&Value::from("cmd"));
|
let cmd = params.get(&Value::from("cmd"));
|
||||||
if cmd.is_none() {
|
if cmd.is_none() {
|
||||||
warn!("No 'cmd' parameter specified for shell variable");
|
warn!("No 'cmd' parameter specified for shell variable");
|
||||||
return None;
|
return Err(super::ExtensionError::Internal);
|
||||||
}
|
}
|
||||||
|
|
||||||
let original_cmd = cmd.unwrap().as_str().unwrap();
|
let inject_args = params
|
||||||
|
.get(&Value::from("inject_args"))
|
||||||
|
.unwrap_or(&Value::from(false))
|
||||||
|
.as_bool()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Render positional parameters in args
|
let original_cmd = cmd.unwrap().as_str().unwrap();
|
||||||
let cmd = POS_ARG_REGEX
|
|
||||||
.replace_all(&original_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 shell_param = params.get(&Value::from("shell"));
|
let shell_param = params.get(&Value::from("shell"));
|
||||||
let shell = if let Some(shell_param) = shell_param {
|
let shell = if let Some(shell_param) = shell_param {
|
||||||
|
@ -184,7 +182,7 @@ impl super::Extension for ShellExtension {
|
||||||
|
|
||||||
if shell.is_none() {
|
if shell.is_none() {
|
||||||
error!("Invalid shell parameter, please select a valid one.");
|
error!("Invalid shell parameter, please select a valid one.");
|
||||||
return None;
|
return Err(super::ExtensionError::Internal);
|
||||||
}
|
}
|
||||||
|
|
||||||
shell.unwrap()
|
shell.unwrap()
|
||||||
|
@ -192,6 +190,24 @@ impl super::Extension for ShellExtension {
|
||||||
Shell::default()
|
Shell::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Render positional parameters in args
|
||||||
|
let cmd = if inject_args {
|
||||||
|
shell
|
||||||
|
.get_arg_regex()
|
||||||
|
.replace_all(&original_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()
|
||||||
|
} else {
|
||||||
|
original_cmd.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
let env_variables = super::utils::convert_to_env_variables(&vars);
|
let env_variables = super::utils::convert_to_env_variables(&vars);
|
||||||
|
|
||||||
let output = shell.execute_cmd(&cmd, &env_variables);
|
let output = shell.execute_cmd(&cmd, &env_variables);
|
||||||
|
@ -238,11 +254,11 @@ impl super::Extension for ShellExtension {
|
||||||
output_str = output_str.trim().to_owned()
|
output_str = output_str.trim().to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(ExtensionResult::Single(output_str))
|
Ok(Some(ExtensionResult::Single(output_str)))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Could not execute cmd '{}', error: {}", cmd, e);
|
error!("Could not execute cmd '{}', error: {}", cmd, e);
|
||||||
None
|
Err(super::ExtensionError::Internal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,7 +276,9 @@ mod tests {
|
||||||
params.insert(Value::from("trim"), Value::from(false));
|
params.insert(Value::from("trim"), Value::from(false));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec![], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
|
@ -283,7 +301,9 @@ mod tests {
|
||||||
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec![], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -301,7 +321,9 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec![], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -317,7 +339,9 @@ mod tests {
|
||||||
params.insert(Value::from("trim"), Value::from("error"));
|
params.insert(Value::from("trim"), Value::from("error"));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec![], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -334,7 +358,9 @@ mod tests {
|
||||||
params.insert(Value::from("trim"), Value::from(true));
|
params.insert(Value::from("trim"), Value::from(true));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec![], &HashMap::new());
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec![], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -348,23 +374,51 @@ mod tests {
|
||||||
fn test_shell_args_unix() {
|
fn test_shell_args_unix() {
|
||||||
let mut params = Mapping::new();
|
let mut params = Mapping::new();
|
||||||
params.insert(Value::from("cmd"), Value::from("echo $0"));
|
params.insert(Value::from("cmd"), Value::from("echo $0"));
|
||||||
|
params.insert(Value::from("inject_args"), Value::from(true));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new());
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn test_shell_no_default_inject_args_unix() {
|
||||||
|
let mut params = Mapping::new();
|
||||||
|
params.insert(
|
||||||
|
Value::from("cmd"),
|
||||||
|
Value::from("echo 'hey friend' | awk '{ print $2 }'"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let extension = ShellExtension::new();
|
||||||
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(output.is_some());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output.unwrap(),
|
||||||
|
ExtensionResult::Single("friend".to_owned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn test_shell_args_windows() {
|
fn test_shell_args_windows() {
|
||||||
let mut params = Mapping::new();
|
let mut params = Mapping::new();
|
||||||
params.insert(Value::from("cmd"), Value::from("echo %0"));
|
params.insert(Value::from("cmd"), Value::from("echo %0"));
|
||||||
|
params.insert(Value::from("inject_args"), Value::from(true));
|
||||||
|
|
||||||
let extension = ShellExtension::new();
|
let extension = ShellExtension::new();
|
||||||
let output = extension.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new());
|
let output = extension
|
||||||
|
.calculate(¶ms, &vec!["hello".to_owned()], &HashMap::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
|
|
||||||
|
@ -387,7 +441,7 @@ mod tests {
|
||||||
"var1".to_owned(),
|
"var1".to_owned(),
|
||||||
ExtensionResult::Single("hello".to_owned()),
|
ExtensionResult::Single("hello".to_owned()),
|
||||||
);
|
);
|
||||||
let output = extension.calculate(¶ms, &vec![], &vars);
|
let output = extension.calculate(¶ms, &vec![], &vars).unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
assert_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
||||||
|
@ -408,7 +462,7 @@ mod tests {
|
||||||
let mut subvars = HashMap::new();
|
let mut subvars = HashMap::new();
|
||||||
subvars.insert("name".to_owned(), "John".to_owned());
|
subvars.insert("name".to_owned(), "John".to_owned());
|
||||||
vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
|
vars.insert("form1".to_owned(), ExtensionResult::Multiple(subvars));
|
||||||
let output = extension.calculate(¶ms, &vec![], &vars);
|
let output = extension.calculate(¶ms, &vec![], &vars).unwrap();
|
||||||
|
|
||||||
assert!(output.is_some());
|
assert!(output.is_some());
|
||||||
assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned()));
|
assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned()));
|
||||||
|
|
|
@ -39,14 +39,14 @@ impl super::Extension for VarDummyExtension {
|
||||||
params: &Mapping,
|
params: &Mapping,
|
||||||
_: &Vec<String>,
|
_: &Vec<String>,
|
||||||
vars: &HashMap<String, ExtensionResult>,
|
vars: &HashMap<String, ExtensionResult>,
|
||||||
) -> Option<ExtensionResult> {
|
) -> super::ExtensionOut {
|
||||||
let target = params.get(&Value::from("target"));
|
let target = params.get(&Value::from("target"));
|
||||||
|
|
||||||
if let Some(target) = target {
|
if let Some(target) = target {
|
||||||
let value = vars.get(target.as_str().unwrap_or_default());
|
let value = vars.get(target.as_str().unwrap_or_default());
|
||||||
Some(value.unwrap().clone())
|
Ok(Some(value.unwrap().clone()))
|
||||||
} else {
|
} else {
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
src/guard.rs
19
src/guard.rs
|
@ -1,3 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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::Configs;
|
use crate::config::Configs;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::sync::atomic::Ordering::Release;
|
use std::sync::atomic::Ordering::Release;
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::config::Configs;
|
use crate::config::Configs;
|
||||||
|
use log::warn;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
@ -71,3 +72,19 @@ pub fn get_manager() -> impl KeyboardManager {
|
||||||
pub fn get_manager() -> impl KeyboardManager {
|
pub fn get_manager() -> impl KeyboardManager {
|
||||||
macos::MacKeyboardManager {}
|
macos::MacKeyboardManager {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These methods are used to wait until all modifiers are released (or timeout occurs)
|
||||||
|
pub fn wait_for_modifiers_release() {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let released = crate::keyboard::windows::wait_for_modifiers_release();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let released = crate::keyboard::macos::wait_for_modifiers_release();
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let released = true; // NOOP on linux (at least for now)
|
||||||
|
|
||||||
|
if !released {
|
||||||
|
warn!("Wait for modifiers release timed out! Please release your modifiers keys (CTRL, CMD, ALT, SHIFT) after typing the trigger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,9 +22,9 @@ use crate::event::{KeyEvent, KeyModifier};
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_yaml::{Mapping, Value};
|
use serde_yaml::{Mapping, Value};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
|
|
||||||
pub(crate) mod scrolling;
|
pub(crate) mod scrolling;
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ pub struct Match {
|
||||||
pub passive_only: bool,
|
pub passive_only: bool,
|
||||||
pub propagate_case: bool,
|
pub propagate_case: bool,
|
||||||
pub force_clipboard: bool,
|
pub force_clipboard: bool,
|
||||||
|
pub is_html: bool,
|
||||||
|
|
||||||
// Automatically calculated from the triggers, used by the matcher to check for correspondences.
|
// Automatically calculated from the triggers, used by the matcher to check for correspondences.
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
|
@ -132,15 +133,36 @@ impl<'a> From<&'a AutoMatch> for Match {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let content = if let Some(replace) = &other.replace {
|
let (text_content, is_html) = if let Some(replace) = &other.replace {
|
||||||
// Text match
|
(Some(Cow::from(replace)), false)
|
||||||
let new_replace = replace.clone();
|
} else if let Some(markdown_str) = &other.markdown {
|
||||||
|
// Render the markdown into HTML
|
||||||
|
let mut html = markdown::to_html(markdown_str);
|
||||||
|
html = html.trim().to_owned();
|
||||||
|
|
||||||
|
if !other.paragraph {
|
||||||
|
// Remove the surrounding paragraph
|
||||||
|
if html.starts_with("<p>") {
|
||||||
|
html = html.trim_start_matches("<p>").to_owned();
|
||||||
|
}
|
||||||
|
if html.ends_with("</p>") {
|
||||||
|
html = html.trim_end_matches("</p>").to_owned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(Some(Cow::from(html)), true)
|
||||||
|
} else if let Some(html) = &other.html {
|
||||||
|
(Some(Cow::from(html)), true)
|
||||||
|
} else {
|
||||||
|
(None, false)
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = if let Some(content) = text_content {
|
||||||
// Check if the match contains variables
|
// Check if the match contains variables
|
||||||
let has_vars = VAR_REGEX.is_match(replace);
|
let has_vars = VAR_REGEX.is_match(&content);
|
||||||
|
|
||||||
let content = TextContent {
|
let content = TextContent {
|
||||||
replace: new_replace,
|
replace: content.to_string(),
|
||||||
vars: other.vars.clone(),
|
vars: other.vars.clone(),
|
||||||
_has_vars: has_vars,
|
_has_vars: has_vars,
|
||||||
};
|
};
|
||||||
|
@ -155,6 +177,9 @@ impl<'a> From<&'a AutoMatch> for Match {
|
||||||
});
|
});
|
||||||
let new_replace = new_replace.to_string();
|
let new_replace = new_replace.to_string();
|
||||||
|
|
||||||
|
// Convert escaped brakets in forms
|
||||||
|
let form = form.replace("\\{", "{ ").replace("\\}", " }");
|
||||||
|
|
||||||
// Convert the form data to valid variables
|
// Convert the form data to valid variables
|
||||||
let mut params = Mapping::new();
|
let mut params = Mapping::new();
|
||||||
if let Some(fields) = &other.form_fields {
|
if let Some(fields) = &other.form_fields {
|
||||||
|
@ -164,7 +189,7 @@ impl<'a> From<&'a AutoMatch> for Match {
|
||||||
});
|
});
|
||||||
params.insert(Value::from("fields"), Value::from(mapping_fields));
|
params.insert(Value::from("fields"), Value::from(mapping_fields));
|
||||||
}
|
}
|
||||||
params.insert(Value::from("layout"), Value::from(form.to_owned()));
|
params.insert(Value::from("layout"), Value::from(form));
|
||||||
|
|
||||||
let vars = vec![MatchVariable {
|
let vars = vec![MatchVariable {
|
||||||
name: "form1".to_owned(),
|
name: "form1".to_owned(),
|
||||||
|
@ -208,7 +233,7 @@ impl<'a> From<&'a AutoMatch> for Match {
|
||||||
|
|
||||||
MatchContentType::Image(content)
|
MatchContentType::Image(content)
|
||||||
} else {
|
} else {
|
||||||
eprintln!("ERROR: no action specified for match {}, please specify either 'replace', 'image_path' or 'form'", other.trigger);
|
eprintln!("ERROR: no action specified for match {}, please specify either 'replace', 'markdown', 'html', image_path' or 'form'", other.trigger);
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -220,6 +245,7 @@ impl<'a> From<&'a AutoMatch> for Match {
|
||||||
_trigger_sequences: trigger_sequences,
|
_trigger_sequences: trigger_sequences,
|
||||||
propagate_case: other.propagate_case,
|
propagate_case: other.propagate_case,
|
||||||
force_clipboard: other.force_clipboard,
|
force_clipboard: other.force_clipboard,
|
||||||
|
is_html,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,6 +285,15 @@ struct AutoMatch {
|
||||||
|
|
||||||
#[serde(default = "default_force_clipboard")]
|
#[serde(default = "default_force_clipboard")]
|
||||||
pub force_clipboard: bool,
|
pub force_clipboard: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_markdown")]
|
||||||
|
pub markdown: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_paragraph")]
|
||||||
|
pub paragraph: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_html")]
|
||||||
|
pub html: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_trigger() -> String {
|
fn default_trigger() -> String {
|
||||||
|
@ -294,6 +329,15 @@ fn default_propagate_case() -> bool {
|
||||||
fn default_force_clipboard() -> bool {
|
fn default_force_clipboard() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
fn default_markdown() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn default_paragraph() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn default_html() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub struct MatchVariable {
|
pub struct MatchVariable {
|
||||||
|
@ -663,4 +707,95 @@ mod tests {
|
||||||
_ => panic!("wrong content"),
|
_ => panic!("wrong content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_match_markdown_loaded_correctly() {
|
||||||
|
let match_str = r###"
|
||||||
|
trigger: ":test"
|
||||||
|
markdown: "This *text* is **very bold**"
|
||||||
|
"###;
|
||||||
|
|
||||||
|
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
||||||
|
|
||||||
|
match _match.content {
|
||||||
|
MatchContentType::Text(content) => {
|
||||||
|
assert_eq!(
|
||||||
|
content.replace,
|
||||||
|
"This <em>text</em> is <strong>very bold</strong>"
|
||||||
|
);
|
||||||
|
assert_eq!(_match.is_html, true);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
assert!(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_match_markdown_keep_vars() {
|
||||||
|
let match_str = r###"
|
||||||
|
trigger: ":test"
|
||||||
|
markdown: "This *text* is {{variable}} **very bold**"
|
||||||
|
"###;
|
||||||
|
|
||||||
|
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
||||||
|
|
||||||
|
match _match.content {
|
||||||
|
MatchContentType::Text(content) => {
|
||||||
|
assert_eq!(
|
||||||
|
content.replace,
|
||||||
|
"This <em>text</em> is {{variable}} <strong>very bold</strong>"
|
||||||
|
);
|
||||||
|
assert_eq!(_match.is_html, true);
|
||||||
|
assert_eq!(content._has_vars, true);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
assert!(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_match_html_loaded_correctly() {
|
||||||
|
let match_str = r###"
|
||||||
|
trigger: ":test"
|
||||||
|
html: "This <i>text<i> is <b>very bold</b>"
|
||||||
|
"###;
|
||||||
|
|
||||||
|
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
||||||
|
|
||||||
|
match _match.content {
|
||||||
|
MatchContentType::Text(content) => {
|
||||||
|
assert_eq!(content.replace, "This <i>text<i> is <b>very bold</b>");
|
||||||
|
assert_eq!(_match.is_html, true);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
assert!(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_match_html_keep_vars() {
|
||||||
|
let match_str = r###"
|
||||||
|
trigger: ":test"
|
||||||
|
html: "This <i>text<i> is {{var}} <b>very bold</b>"
|
||||||
|
"###;
|
||||||
|
|
||||||
|
let _match: Match = serde_yaml::from_str(match_str).unwrap();
|
||||||
|
|
||||||
|
match _match.content {
|
||||||
|
MatchContentType::Text(content) => {
|
||||||
|
assert_eq!(
|
||||||
|
content.replace,
|
||||||
|
"This <i>text<i> is {{var}} <b>very bold</b>"
|
||||||
|
);
|
||||||
|
assert_eq!(_match.is_html, true);
|
||||||
|
assert_eq!(content._has_vars, true);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
assert!(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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 log::debug;
|
use log::debug;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::{copy, Cursor};
|
use std::io::{copy, Cursor};
|
||||||
|
|
|
@ -182,19 +182,24 @@ impl super::Renderer for DefaultRenderer {
|
||||||
// Normal extension variables
|
// Normal extension variables
|
||||||
let extension = self.extension_map.get(&variable.var_type);
|
let extension = self.extension_map.get(&variable.var_type);
|
||||||
if let Some(extension) = extension {
|
if let Some(extension) = extension {
|
||||||
let ext_out =
|
let ext_res =
|
||||||
extension.calculate(&variable.params, &args, &output_map);
|
extension.calculate(&variable.params, &args, &output_map);
|
||||||
if let Some(output) = ext_out {
|
match ext_res {
|
||||||
output_map.insert(variable.name.clone(), output);
|
Ok(ext_out) => {
|
||||||
} else {
|
if let Some(output) = ext_out {
|
||||||
output_map.insert(
|
output_map.insert(variable.name.clone(), output);
|
||||||
variable.name.clone(),
|
} else {
|
||||||
ExtensionResult::Single("".to_owned()),
|
output_map.insert(
|
||||||
);
|
variable.name.clone(),
|
||||||
warn!(
|
ExtensionResult::Single("".to_owned()),
|
||||||
"Could not generate output for variable: {}",
|
);
|
||||||
variable.name
|
warn!(
|
||||||
);
|
"Could not generate output for variable: {}",
|
||||||
|
variable.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => return RenderResult::Error,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!(
|
||||||
|
@ -238,13 +243,15 @@ impl super::Renderer for DefaultRenderer {
|
||||||
|
|
||||||
// Unescape any brackets (needed to be able to insert double brackets in replacement
|
// Unescape any brackets (needed to be able to insert double brackets in replacement
|
||||||
// text, without triggering the variable system). See issue #187
|
// text, without triggering the variable system). See issue #187
|
||||||
let target_string = target_string.replace("\\{", "{").replace("\\}", "}");
|
let mut target_string = target_string.replace("\\{", "{").replace("\\}", "}");
|
||||||
|
|
||||||
// Render any argument that may be present
|
// Render any argument that may be present
|
||||||
let target_string = utils::render_args(&target_string, &args);
|
if !args.is_empty() {
|
||||||
|
target_string = utils::render_args(&target_string, &args);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle case propagation
|
// Handle case propagation
|
||||||
let target_string = if m.propagate_case {
|
target_string = if m.propagate_case {
|
||||||
let trigger = &m.triggers[trigger_offset];
|
let trigger = &m.triggers[trigger_offset];
|
||||||
|
|
||||||
// The check should be carried out from the position of the first
|
// The check should be carried out from the position of the first
|
||||||
|
@ -518,6 +525,25 @@ mod tests {
|
||||||
verify_render(rendered, "Hi Jon");
|
verify_render(rendered, "Hi Jon");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_render_passive_simple_match_no_args_should_not_replace_args_syntax() {
|
||||||
|
let text = ":greet";
|
||||||
|
|
||||||
|
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 $0$");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_passive_simple_match_with_multiple_args() {
|
fn test_render_passive_simple_match_with_multiple_args() {
|
||||||
let text = ":greet/Jon/Snow/";
|
let text = ":greet/Jon/Snow/";
|
||||||
|
|
|
@ -29,8 +29,8 @@ pub struct LinuxSystemManager {}
|
||||||
impl super::SystemManager for LinuxSystemManager {
|
impl super::SystemManager for LinuxSystemManager {
|
||||||
fn get_current_window_title(&self) -> Option<String> {
|
fn get_current_window_title(&self) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut buffer: [c_char; 100] = [0; 100];
|
let mut buffer: [c_char; 256] = [0; 256];
|
||||||
let res = get_active_window_name(buffer.as_mut_ptr(), buffer.len() as i32);
|
let res = get_active_window_name(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||||
|
|
||||||
if res > 0 {
|
if res > 0 {
|
||||||
let c_string = CStr::from_ptr(buffer.as_ptr());
|
let c_string = CStr::from_ptr(buffer.as_ptr());
|
||||||
|
@ -47,8 +47,8 @@ impl super::SystemManager for LinuxSystemManager {
|
||||||
|
|
||||||
fn get_current_window_class(&self) -> Option<String> {
|
fn get_current_window_class(&self) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut buffer: [c_char; 100] = [0; 100];
|
let mut buffer: [c_char; 256] = [0; 256];
|
||||||
let res = get_active_window_class(buffer.as_mut_ptr(), buffer.len() as i32);
|
let res = get_active_window_class(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||||
|
|
||||||
if res > 0 {
|
if res > 0 {
|
||||||
let c_string = CStr::from_ptr(buffer.as_ptr());
|
let c_string = CStr::from_ptr(buffer.as_ptr());
|
||||||
|
@ -65,8 +65,8 @@ impl super::SystemManager for LinuxSystemManager {
|
||||||
|
|
||||||
fn get_current_window_executable(&self) -> Option<String> {
|
fn get_current_window_executable(&self) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut buffer: [c_char; 100] = [0; 100];
|
let mut buffer: [c_char; 256] = [0; 256];
|
||||||
let res = get_active_window_executable(buffer.as_mut_ptr(), buffer.len() as i32);
|
let res = get_active_window_executable(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||||
|
|
||||||
if res > 0 {
|
if res > 0 {
|
||||||
let c_string = CStr::from_ptr(buffer.as_ptr());
|
let c_string = CStr::from_ptr(buffer.as_ptr());
|
||||||
|
|
|
@ -33,8 +33,8 @@ impl super::SystemManager for MacSystemManager {
|
||||||
|
|
||||||
fn get_current_window_class(&self) -> Option<String> {
|
fn get_current_window_class(&self) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut buffer: [c_char; 250] = [0; 250];
|
let mut buffer: [c_char; 256] = [0; 256];
|
||||||
let res = get_active_app_identifier(buffer.as_mut_ptr(), buffer.len() as i32);
|
let res = get_active_app_identifier(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||||
|
|
||||||
if res > 0 {
|
if res > 0 {
|
||||||
let c_string = CStr::from_ptr(buffer.as_ptr());
|
let c_string = CStr::from_ptr(buffer.as_ptr());
|
||||||
|
@ -51,8 +51,8 @@ impl super::SystemManager for MacSystemManager {
|
||||||
|
|
||||||
fn get_current_window_executable(&self) -> Option<String> {
|
fn get_current_window_executable(&self) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut buffer: [c_char; 250] = [0; 250];
|
let mut buffer: [c_char; 256] = [0; 256];
|
||||||
let res = get_active_app_bundle(buffer.as_mut_ptr(), buffer.len() as i32);
|
let res = get_active_app_bundle(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||||
|
|
||||||
if res > 0 {
|
if res > 0 {
|
||||||
let c_string = CStr::from_ptr(buffer.as_ptr());
|
let c_string = CStr::from_ptr(buffer.as_ptr());
|
||||||
|
|
|
@ -31,8 +31,8 @@ impl WindowsSystemManager {
|
||||||
impl super::SystemManager for WindowsSystemManager {
|
impl super::SystemManager for WindowsSystemManager {
|
||||||
fn get_current_window_title(&self) -> Option<String> {
|
fn get_current_window_title(&self) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut buffer: [u16; 100] = [0; 100];
|
let mut buffer: [u16; 256] = [0; 256];
|
||||||
let res = get_active_window_name(buffer.as_mut_ptr(), buffer.len() as i32);
|
let res = get_active_window_name(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||||
|
|
||||||
if res > 0 {
|
if res > 0 {
|
||||||
let c_string = U16CString::from_ptr_str(buffer.as_ptr());
|
let c_string = U16CString::from_ptr_str(buffer.as_ptr());
|
||||||
|
@ -51,8 +51,8 @@ impl super::SystemManager for WindowsSystemManager {
|
||||||
|
|
||||||
fn get_current_window_executable(&self) -> Option<String> {
|
fn get_current_window_executable(&self) -> Option<String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut buffer: [u16; 250] = [0; 250];
|
let mut buffer: [u16; 256] = [0; 256];
|
||||||
let res = get_active_window_executable(buffer.as_mut_ptr(), buffer.len() as i32);
|
let res = get_active_window_executable(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||||
|
|
||||||
if res > 0 {
|
if res > 0 {
|
||||||
let c_string = U16CString::from_ptr_str(buffer.as_ptr());
|
let c_string = U16CString::from_ptr_str(buffer.as_ptr());
|
||||||
|
|
|
@ -1,3 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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 log::info;
|
use log::info;
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
|
@ -1,3 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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::Configs;
|
use crate::config::Configs;
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use std::io::{Error, Write};
|
use std::io::{Error, Write};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user