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]
|
||||
name = "espanso"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||
license = "GPL-3.0"
|
||||
description = "Cross-platform Text Expander written in Rust"
|
||||
|
@ -14,7 +14,7 @@ version = "0.1.1"
|
|||
|
||||
[dependencies]
|
||||
widestring = "0.4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde = { version = "1.0.117", features = ["derive"] }
|
||||
serde_yaml = "0.8"
|
||||
dirs = "2.0.2"
|
||||
clap = "2.33.0"
|
||||
|
@ -22,7 +22,7 @@ regex = "1.3.1"
|
|||
log = "0.4.8"
|
||||
simplelog = "0.7.1"
|
||||
fs2 = "0.4.3"
|
||||
serde_json = "1.0.40"
|
||||
serde_json = "1.0.60"
|
||||
log-panics = {version = "2.0.0", features = ["with-backtrace"]}
|
||||
backtrace = "0.3.37"
|
||||
chrono = "0.4.9"
|
||||
|
@ -34,6 +34,8 @@ dialoguer = "0.4.0"
|
|||
rand = "0.7.2"
|
||||
zip = "0.5.3"
|
||||
notify = "4.0.13"
|
||||
markdown = "0.3.0"
|
||||
html2text = "0.2.1"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.62"
|
||||
|
|
|
@ -34,6 +34,7 @@ ___
|
|||
* **Custom scripts** support
|
||||
* **Shell commands** support
|
||||
* **App-specific** configurations
|
||||
* Support [Forms](https://espanso.org/docs/forms/)
|
||||
* Expandable with **packages**
|
||||
* Built-in **package manager** for [espanso hub](https://hub.espanso.org/)
|
||||
* File based configuration
|
||||
|
|
4
build.rs
4
build.rs
|
@ -28,6 +28,10 @@ fn get_config() -> PathBuf {
|
|||
fn print_config() {
|
||||
println!("cargo:rustc-link-lib=static=winbridge");
|
||||
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")]
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
// Setup status icon
|
||||
if (show_icon) {
|
||||
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
||||
|
||||
[self setIcon: icon_path];
|
||||
}
|
||||
|
||||
|
@ -61,16 +60,12 @@
|
|||
|
||||
- (void) updateIcon: (char *)iconPath {
|
||||
if (show_icon) {
|
||||
[myStatusItem release];
|
||||
|
||||
[self setIcon: iconPath];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setIcon: (char *)iconPath {
|
||||
if (show_icon) {
|
||||
myStatusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
||||
|
||||
NSString *nsIconPath = [NSString stringWithUTF8String:iconPath];
|
||||
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
|
||||
[statusImage setTemplate:YES];
|
||||
|
|
|
@ -170,6 +170,11 @@ int32_t set_clipboard(char * text);
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
|
@ -324,6 +324,24 @@ int32_t set_clipboard_image(char *path) {
|
|||
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
|
||||
|
||||
|
|
|
@ -27,7 +27,15 @@
|
|||
|
||||
#define UNICODE
|
||||
|
||||
#ifdef __MINGW32__
|
||||
# ifndef WINVER
|
||||
# define WINVER 0x0606
|
||||
# endif
|
||||
# define STRSAFE_NO_DEPRECATE
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
#include <winuser.h>
|
||||
#include <strsafe.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
|
@ -890,3 +898,37 @@ int32_t set_clipboard_image(wchar_t *path) {
|
|||
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);
|
||||
|
||||
/*
|
||||
* 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
|
||||
|
||||
extern "C" int32_t start_process(wchar_t * cmd);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: espanso
|
||||
version: 0.7.2
|
||||
version: 0.7.3
|
||||
summary: A Cross-platform Text Expander written in Rust
|
||||
description: |
|
||||
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 set_clipboard(text: *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
|
||||
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/>.
|
||||
*/
|
||||
|
||||
use std::os::raw::c_void;
|
||||
use std::os::raw::{c_char, c_void};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct WindowsMenuItem {
|
||||
|
@ -55,6 +55,7 @@ extern "C" {
|
|||
pub fn get_clipboard(buffer: *mut u16, size: i32) -> i32;
|
||||
pub fn set_clipboard(payload: *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
|
||||
pub fn register_keypress_callback(
|
||||
|
|
|
@ -89,6 +89,31 @@ impl super::ClipboardManager for LinuxClipboardManager {
|
|||
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 {
|
||||
|
|
|
@ -29,7 +29,7 @@ impl super::ClipboardManager for MacClipboardManager {
|
|||
fn get_clipboard(&self) -> Option<String> {
|
||||
unsafe {
|
||||
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 {
|
||||
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 {
|
||||
|
|
|
@ -32,6 +32,7 @@ pub trait ClipboardManager {
|
|||
fn get_clipboard(&self) -> Option<String>;
|
||||
fn set_clipboard(&self, payload: &str);
|
||||
fn set_clipboard_image(&self, image_path: &Path);
|
||||
fn set_clipboard_html(&self, html: &str);
|
||||
}
|
||||
|
||||
// LINUX IMPLEMENTATION
|
||||
|
|
|
@ -17,8 +17,10 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::bridge::windows::{get_clipboard, set_clipboard, set_clipboard_image};
|
||||
use std::path::Path;
|
||||
use crate::bridge::windows::{
|
||||
get_clipboard, set_clipboard, set_clipboard_html, set_clipboard_image,
|
||||
};
|
||||
use std::{ffi::CString, path::Path};
|
||||
use widestring::U16CString;
|
||||
|
||||
pub struct WindowsClipboardManager {}
|
||||
|
@ -33,7 +35,7 @@ impl super::ClipboardManager for WindowsClipboardManager {
|
|||
fn get_clipboard(&self) -> Option<String> {
|
||||
unsafe {
|
||||
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 {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn default_wait_for_modifiers_release() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Configs {
|
||||
#[serde(default = "default_name")]
|
||||
|
@ -283,6 +287,9 @@ pub struct Configs {
|
|||
|
||||
#[serde(default = "default_modulo_path")]
|
||||
pub modulo_path: Option<String>,
|
||||
|
||||
#[serde(default = "default_wait_for_modifiers_release")]
|
||||
pub wait_for_modifiers_release: bool,
|
||||
}
|
||||
|
||||
// Macro used to validate config fields
|
||||
|
|
|
@ -154,8 +154,14 @@ impl<
|
|||
}
|
||||
}
|
||||
|
||||
fn inject_text(&self, config: &Configs, target_string: &str, force_clipboard: bool) {
|
||||
let backend = if force_clipboard {
|
||||
fn inject_text(
|
||||
&self,
|
||||
config: &Configs,
|
||||
target_string: &str,
|
||||
force_clipboard: bool,
|
||||
is_html: bool,
|
||||
) {
|
||||
let backend = if force_clipboard || is_html {
|
||||
&BackendType::Clipboard
|
||||
} else if config.backend == BackendType::Auto {
|
||||
if cfg!(target_os = "linux") {
|
||||
|
@ -188,7 +194,12 @@ impl<
|
|||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
_ => {
|
||||
|
@ -220,6 +231,13 @@ impl<
|
|||
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 {
|
||||
self.keyboard_manager.delete_string(&config, char_count);
|
||||
}
|
||||
|
@ -270,10 +288,10 @@ impl<
|
|||
// clipboard content to restore it later.
|
||||
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
|
||||
if cursor_rewind.is_none() {
|
||||
// Disallow undo backspace if cursor positioning is used or text is HTML
|
||||
if cursor_rewind.is_none() && !m.is_html {
|
||||
expansion_data = Some((
|
||||
m.triggers[trigger_offset].clone(),
|
||||
target_string.chars().count() as i32,
|
||||
|
@ -350,7 +368,7 @@ impl<
|
|||
self.keyboard_manager
|
||||
.delete_string(&config, *injected_text_len - 1);
|
||||
// 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 std::collections::HashMap;
|
||||
|
||||
use super::ExtensionOut;
|
||||
|
||||
pub struct ClipboardExtension {
|
||||
clipboard_manager: Box<dyn ClipboardManager>,
|
||||
}
|
||||
|
@ -42,11 +44,11 @@ impl super::Extension for ClipboardExtension {
|
|||
_: &Mapping,
|
||||
_: &Vec<String>,
|
||||
_: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult> {
|
||||
) -> ExtensionOut {
|
||||
if let Some(clipboard) = self.clipboard_manager.get_clipboard() {
|
||||
Some(ExtensionResult::Single(clipboard))
|
||||
Ok(Some(ExtensionResult::Single(clipboard)))
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ use chrono::{DateTime, Duration, Local};
|
|||
use serde_yaml::{Mapping, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ExtensionOut;
|
||||
|
||||
pub struct DateExtension {}
|
||||
|
||||
impl DateExtension {
|
||||
|
@ -40,7 +42,7 @@ impl super::Extension for DateExtension {
|
|||
params: &Mapping,
|
||||
_: &Vec<String>,
|
||||
_: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult> {
|
||||
) -> ExtensionOut {
|
||||
let mut now: DateTime<Local> = Local::now();
|
||||
|
||||
// Compute the given offset
|
||||
|
@ -59,6 +61,6 @@ impl super::Extension for DateExtension {
|
|||
now.to_rfc2822()
|
||||
};
|
||||
|
||||
Some(ExtensionResult::Single(date))
|
||||
Ok(Some(ExtensionResult::Single(date)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,15 +43,15 @@ impl super::Extension for DummyExtension {
|
|||
params: &Mapping,
|
||||
_: &Vec<String>,
|
||||
_: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult> {
|
||||
) -> super::ExtensionOut {
|
||||
let echo = params.get(&Value::from("echo"));
|
||||
|
||||
if let Some(echo) = echo {
|
||||
Some(ExtensionResult::Single(
|
||||
Ok(Some(ExtensionResult::Single(
|
||||
echo.as_str().unwrap_or_default().to_owned(),
|
||||
))
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,13 +43,13 @@ impl super::Extension for FormExtension {
|
|||
params: &Mapping,
|
||||
_: &Vec<String>,
|
||||
_: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult> {
|
||||
) -> super::ExtensionOut {
|
||||
let layout = params.get(&Value::from("layout"));
|
||||
let layout = if let Some(value) = layout {
|
||||
value.as_str().unwrap_or_default().to_string()
|
||||
} else {
|
||||
error!("invoking form extension without specifying a layout");
|
||||
return None;
|
||||
return Err(super::ExtensionError::Internal);
|
||||
};
|
||||
|
||||
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);
|
||||
match 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) => {
|
||||
error!("modulo json parsing error: {}", error);
|
||||
return None;
|
||||
return Err(super::ExtensionError::Internal);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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>),
|
||||
}
|
||||
|
||||
#[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 {
|
||||
fn name(&self) -> String;
|
||||
fn calculate(
|
||||
|
@ -45,7 +56,7 @@ pub trait Extension {
|
|||
params: &Mapping,
|
||||
args: &Vec<String>,
|
||||
current_vars: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult>;
|
||||
) -> ExtensionOut;
|
||||
}
|
||||
|
||||
pub fn get_extensions(
|
||||
|
|
|
@ -39,7 +39,7 @@ impl super::Extension for MultiEchoExtension {
|
|||
params: &Mapping,
|
||||
_: &Vec<String>,
|
||||
_: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult> {
|
||||
) -> super::ExtensionOut {
|
||||
let mut output: HashMap<String, String> = HashMap::new();
|
||||
for (key, value) in params.iter() {
|
||||
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,
|
||||
args: &Vec<String>,
|
||||
_: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult> {
|
||||
) -> super::ExtensionOut {
|
||||
let choices = params.get(&Value::from("choices"));
|
||||
if choices.is_none() {
|
||||
warn!("No 'choices' parameter specified for random variable");
|
||||
return None;
|
||||
return Ok(None);
|
||||
}
|
||||
let choices = choices.unwrap().as_sequence();
|
||||
if let Some(choices) = choices {
|
||||
|
@ -62,17 +62,17 @@ impl super::Extension for RandomExtension {
|
|||
// Render arguments
|
||||
let output = crate::render::utils::render_args(output, args);
|
||||
|
||||
return Some(ExtensionResult::Single(output));
|
||||
return Ok(Some(ExtensionResult::Single(output)));
|
||||
}
|
||||
None => {
|
||||
error!("Could not select a random choice.");
|
||||
return None;
|
||||
return Err(super::ExtensionError::Internal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
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());
|
||||
|
||||
|
@ -106,7 +108,9 @@ mod tests {
|
|||
params.insert(Value::from("choices"), Value::from(choices.clone()));
|
||||
|
||||
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());
|
||||
|
||||
|
|
|
@ -42,11 +42,11 @@ impl super::Extension for ScriptExtension {
|
|||
params: &Mapping,
|
||||
user_args: &Vec<String>,
|
||||
vars: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult> {
|
||||
) -> super::ExtensionOut {
|
||||
let args = params.get(&Value::from("args"));
|
||||
if args.is_none() {
|
||||
warn!("No 'args' parameter specified for script variable");
|
||||
return None;
|
||||
return Err(super::ExtensionError::Internal);
|
||||
}
|
||||
let args = args.unwrap().as_sequence();
|
||||
if let Some(args) = args {
|
||||
|
@ -145,17 +145,17 @@ impl super::Extension for ScriptExtension {
|
|||
output_str = output_str.trim().to_owned()
|
||||
}
|
||||
|
||||
return Some(ExtensionResult::Single(output_str));
|
||||
return Ok(Some(ExtensionResult::Single(output_str)));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Could not execute script '{:?}', error: {}", args, e);
|
||||
return None;
|
||||
return Err(super::ExtensionError::Internal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error!("Could not execute script with args '{:?}'", args);
|
||||
None
|
||||
Err(super::ExtensionError::Internal)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ mod tests {
|
|||
);
|
||||
|
||||
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_eq!(
|
||||
|
@ -194,7 +194,7 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from(false));
|
||||
|
||||
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_eq!(
|
||||
|
@ -213,7 +213,7 @@ mod tests {
|
|||
);
|
||||
|
||||
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_eq!(
|
||||
|
@ -233,7 +233,7 @@ mod tests {
|
|||
params.insert(Value::from("inject_args"), Value::from(true));
|
||||
|
||||
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_eq!(
|
||||
|
@ -261,7 +261,7 @@ mod tests {
|
|||
);
|
||||
|
||||
let extension = ScriptExtension::new();
|
||||
let output = extension.calculate(¶ms, &vec![], &vars);
|
||||
let output = extension.calculate(¶ms, &vec![], &vars).unwrap();
|
||||
|
||||
assert!(output.is_some());
|
||||
assert_eq!(
|
||||
|
|
|
@ -25,11 +25,8 @@ use std::collections::HashMap;
|
|||
use std::process::{Command, Output};
|
||||
|
||||
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()
|
||||
};
|
||||
static ref UNIX_POS_ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)").unwrap();
|
||||
static ref WIN_POS_ARG_REGEX: Regex = Regex::new("%(?P<pos>\\d+)").unwrap();
|
||||
}
|
||||
|
||||
pub enum Shell {
|
||||
|
@ -121,6 +118,14 @@ impl Shell {
|
|||
_ => 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 {
|
||||
|
@ -155,27 +160,20 @@ impl super::Extension for ShellExtension {
|
|||
params: &Mapping,
|
||||
args: &Vec<String>,
|
||||
vars: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult> {
|
||||
) -> super::ExtensionOut {
|
||||
let cmd = params.get(&Value::from("cmd"));
|
||||
if cmd.is_none() {
|
||||
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 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 original_cmd = cmd.unwrap().as_str().unwrap();
|
||||
|
||||
let shell_param = params.get(&Value::from("shell"));
|
||||
let shell = if let Some(shell_param) = shell_param {
|
||||
|
@ -184,7 +182,7 @@ impl super::Extension for ShellExtension {
|
|||
|
||||
if shell.is_none() {
|
||||
error!("Invalid shell parameter, please select a valid one.");
|
||||
return None;
|
||||
return Err(super::ExtensionError::Internal);
|
||||
}
|
||||
|
||||
shell.unwrap()
|
||||
|
@ -192,6 +190,24 @@ impl super::Extension for ShellExtension {
|
|||
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 output = shell.execute_cmd(&cmd, &env_variables);
|
||||
|
@ -238,11 +254,11 @@ impl super::Extension for ShellExtension {
|
|||
output_str = output_str.trim().to_owned()
|
||||
}
|
||||
|
||||
Some(ExtensionResult::Single(output_str))
|
||||
Ok(Some(ExtensionResult::Single(output_str)))
|
||||
}
|
||||
Err(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));
|
||||
|
||||
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());
|
||||
|
||||
|
@ -283,7 +301,9 @@ mod tests {
|
|||
params.insert(Value::from("cmd"), Value::from("echo \"hello world\""));
|
||||
|
||||
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_eq!(
|
||||
|
@ -301,7 +321,9 @@ mod tests {
|
|||
);
|
||||
|
||||
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_eq!(
|
||||
|
@ -317,7 +339,9 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from("error"));
|
||||
|
||||
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_eq!(
|
||||
|
@ -334,7 +358,9 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from(true));
|
||||
|
||||
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_eq!(
|
||||
|
@ -348,23 +374,51 @@ mod tests {
|
|||
fn test_shell_args_unix() {
|
||||
let mut params = Mapping::new();
|
||||
params.insert(Value::from("cmd"), Value::from("echo $0"));
|
||||
params.insert(Value::from("inject_args"), Value::from(true));
|
||||
|
||||
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_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]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn test_shell_args_windows() {
|
||||
let mut params = Mapping::new();
|
||||
params.insert(Value::from("cmd"), Value::from("echo %0"));
|
||||
params.insert(Value::from("inject_args"), Value::from(true));
|
||||
|
||||
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());
|
||||
|
||||
|
@ -387,7 +441,7 @@ mod tests {
|
|||
"var1".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_eq!(output.unwrap(), ExtensionResult::Single("hello".to_owned()));
|
||||
|
@ -408,7 +462,7 @@ mod tests {
|
|||
let mut subvars = HashMap::new();
|
||||
subvars.insert("name".to_owned(), "John".to_owned());
|
||||
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_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned()));
|
||||
|
|
|
@ -39,14 +39,14 @@ impl super::Extension for VarDummyExtension {
|
|||
params: &Mapping,
|
||||
_: &Vec<String>,
|
||||
vars: &HashMap<String, ExtensionResult>,
|
||||
) -> Option<ExtensionResult> {
|
||||
) -> super::ExtensionOut {
|
||||
let target = params.get(&Value::from("target"));
|
||||
|
||||
if let Some(target) = target {
|
||||
let value = vars.get(target.as_str().unwrap_or_default());
|
||||
Some(value.unwrap().clone())
|
||||
Ok(Some(value.unwrap().clone()))
|
||||
} 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 log::debug;
|
||||
use std::sync::atomic::Ordering::Release;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
use crate::config::Configs;
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
@ -71,3 +72,19 @@ pub fn get_manager() -> impl KeyboardManager {
|
|||
pub fn get_manager() -> impl KeyboardManager {
|
||||
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 serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
pub(crate) mod scrolling;
|
||||
|
||||
|
@ -36,6 +36,7 @@ pub struct Match {
|
|||
pub passive_only: bool,
|
||||
pub propagate_case: bool,
|
||||
pub force_clipboard: bool,
|
||||
pub is_html: bool,
|
||||
|
||||
// Automatically calculated from the triggers, used by the matcher to check for correspondences.
|
||||
#[serde(skip_serializing)]
|
||||
|
@ -132,15 +133,36 @@ impl<'a> From<&'a AutoMatch> for Match {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let content = if let Some(replace) = &other.replace {
|
||||
// Text match
|
||||
let new_replace = replace.clone();
|
||||
let (text_content, is_html) = if let Some(replace) = &other.replace {
|
||||
(Some(Cow::from(replace)), false)
|
||||
} 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
|
||||
let has_vars = VAR_REGEX.is_match(replace);
|
||||
let has_vars = VAR_REGEX.is_match(&content);
|
||||
|
||||
let content = TextContent {
|
||||
replace: new_replace,
|
||||
replace: content.to_string(),
|
||||
vars: other.vars.clone(),
|
||||
_has_vars: has_vars,
|
||||
};
|
||||
|
@ -155,6 +177,9 @@ impl<'a> From<&'a AutoMatch> for Match {
|
|||
});
|
||||
let new_replace = new_replace.to_string();
|
||||
|
||||
// Convert escaped brakets in forms
|
||||
let form = form.replace("\\{", "{ ").replace("\\}", " }");
|
||||
|
||||
// Convert the form data to valid variables
|
||||
let mut params = Mapping::new();
|
||||
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("layout"), Value::from(form.to_owned()));
|
||||
params.insert(Value::from("layout"), Value::from(form));
|
||||
|
||||
let vars = vec![MatchVariable {
|
||||
name: "form1".to_owned(),
|
||||
|
@ -208,7 +233,7 @@ impl<'a> From<&'a AutoMatch> for Match {
|
|||
|
||||
MatchContentType::Image(content)
|
||||
} 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);
|
||||
};
|
||||
|
||||
|
@ -220,6 +245,7 @@ impl<'a> From<&'a AutoMatch> for Match {
|
|||
_trigger_sequences: trigger_sequences,
|
||||
propagate_case: other.propagate_case,
|
||||
force_clipboard: other.force_clipboard,
|
||||
is_html,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -259,6 +285,15 @@ struct AutoMatch {
|
|||
|
||||
#[serde(default = "default_force_clipboard")]
|
||||
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 {
|
||||
|
@ -294,6 +329,15 @@ fn default_propagate_case() -> bool {
|
|||
fn default_force_clipboard() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_markdown() -> Option<String> {
|
||||
None
|
||||
}
|
||||
fn default_paragraph() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_html() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct MatchVariable {
|
||||
|
@ -663,4 +707,95 @@ mod tests {
|
|||
_ => 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 std::error::Error;
|
||||
use std::io::{copy, Cursor};
|
||||
|
|
|
@ -182,19 +182,24 @@ impl super::Renderer for DefaultRenderer {
|
|||
// Normal extension variables
|
||||
let extension = self.extension_map.get(&variable.var_type);
|
||||
if let Some(extension) = extension {
|
||||
let ext_out =
|
||||
let ext_res =
|
||||
extension.calculate(&variable.params, &args, &output_map);
|
||||
if let Some(output) = ext_out {
|
||||
output_map.insert(variable.name.clone(), output);
|
||||
} else {
|
||||
output_map.insert(
|
||||
variable.name.clone(),
|
||||
ExtensionResult::Single("".to_owned()),
|
||||
);
|
||||
warn!(
|
||||
"Could not generate output for variable: {}",
|
||||
variable.name
|
||||
);
|
||||
match ext_res {
|
||||
Ok(ext_out) => {
|
||||
if let Some(output) = ext_out {
|
||||
output_map.insert(variable.name.clone(), output);
|
||||
} else {
|
||||
output_map.insert(
|
||||
variable.name.clone(),
|
||||
ExtensionResult::Single("".to_owned()),
|
||||
);
|
||||
warn!(
|
||||
"Could not generate output for variable: {}",
|
||||
variable.name
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_) => return RenderResult::Error,
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
|
@ -238,13 +243,15 @@ impl super::Renderer for DefaultRenderer {
|
|||
|
||||
// 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("\\}", "}");
|
||||
let mut target_string = target_string.replace("\\{", "{").replace("\\}", "}");
|
||||
|
||||
// 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
|
||||
let target_string = if m.propagate_case {
|
||||
target_string = if m.propagate_case {
|
||||
let trigger = &m.triggers[trigger_offset];
|
||||
|
||||
// The check should be carried out from the position of the first
|
||||
|
@ -518,6 +525,25 @@ mod tests {
|
|||
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]
|
||||
fn test_render_passive_simple_match_with_multiple_args() {
|
||||
let text = ":greet/Jon/Snow/";
|
||||
|
|
|
@ -29,8 +29,8 @@ pub struct LinuxSystemManager {}
|
|||
impl super::SystemManager for LinuxSystemManager {
|
||||
fn get_current_window_title(&self) -> Option<String> {
|
||||
unsafe {
|
||||
let mut buffer: [c_char; 100] = [0; 100];
|
||||
let res = get_active_window_name(buffer.as_mut_ptr(), buffer.len() as i32);
|
||||
let mut buffer: [c_char; 256] = [0; 256];
|
||||
let res = get_active_window_name(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||
|
||||
if res > 0 {
|
||||
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> {
|
||||
unsafe {
|
||||
let mut buffer: [c_char; 100] = [0; 100];
|
||||
let res = get_active_window_class(buffer.as_mut_ptr(), buffer.len() as i32);
|
||||
let mut buffer: [c_char; 256] = [0; 256];
|
||||
let res = get_active_window_class(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||
|
||||
if res > 0 {
|
||||
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> {
|
||||
unsafe {
|
||||
let mut buffer: [c_char; 100] = [0; 100];
|
||||
let res = get_active_window_executable(buffer.as_mut_ptr(), buffer.len() as i32);
|
||||
let mut buffer: [c_char; 256] = [0; 256];
|
||||
let res = get_active_window_executable(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||
|
||||
if res > 0 {
|
||||
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> {
|
||||
unsafe {
|
||||
let mut buffer: [c_char; 250] = [0; 250];
|
||||
let res = get_active_app_identifier(buffer.as_mut_ptr(), buffer.len() as i32);
|
||||
let mut buffer: [c_char; 256] = [0; 256];
|
||||
let res = get_active_app_identifier(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||
|
||||
if res > 0 {
|
||||
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> {
|
||||
unsafe {
|
||||
let mut buffer: [c_char; 250] = [0; 250];
|
||||
let res = get_active_app_bundle(buffer.as_mut_ptr(), buffer.len() as i32);
|
||||
let mut buffer: [c_char; 256] = [0; 256];
|
||||
let res = get_active_app_bundle(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||
|
||||
if res > 0 {
|
||||
let c_string = CStr::from_ptr(buffer.as_ptr());
|
||||
|
|
|
@ -31,8 +31,8 @@ impl WindowsSystemManager {
|
|||
impl super::SystemManager for WindowsSystemManager {
|
||||
fn get_current_window_title(&self) -> Option<String> {
|
||||
unsafe {
|
||||
let mut buffer: [u16; 100] = [0; 100];
|
||||
let res = get_active_window_name(buffer.as_mut_ptr(), buffer.len() as i32);
|
||||
let mut buffer: [u16; 256] = [0; 256];
|
||||
let res = get_active_window_name(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||
|
||||
if res > 0 {
|
||||
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> {
|
||||
unsafe {
|
||||
let mut buffer: [u16; 250] = [0; 250];
|
||||
let res = get_active_window_executable(buffer.as_mut_ptr(), buffer.len() as i32);
|
||||
let mut buffer: [u16; 256] = [0; 256];
|
||||
let res = get_active_window_executable(buffer.as_mut_ptr(), (buffer.len() - 1) as i32);
|
||||
|
||||
if res > 0 {
|
||||
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 std::os::unix::fs::symlink;
|
||||
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 log::{error, info};
|
||||
use std::io::{Error, Write};
|
||||
|
|
Loading…
Reference in New Issue
Block a user