Merge pull request #535 from federico-terzi/dev

Version 0.7.3
This commit is contained in:
Federico Terzi 2020-12-03 20:47:59 +01:00 committed by GitHub
commit 386a351df7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1574 additions and 839 deletions

1631
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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

View File

@ -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")]

View File

@ -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];

View File

@ -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.
*/

View File

@ -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

View File

@ -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;
}

View File

@ -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);

View File

@ -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.

View File

@ -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));

View File

@ -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(

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

@ -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());
}
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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)
}
}
}

View File

@ -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)))
}
}

View File

@ -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)
}
}
}

View File

@ -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);
}
}
}

View File

@ -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(

View File

@ -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)))
}
}

View File

@ -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(&params, &vec![], &HashMap::new());
let output = extension
.calculate(&params, &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(&params, &vec!["test".to_owned()], &HashMap::new());
let output = extension
.calculate(&params, &vec!["test".to_owned()], &HashMap::new())
.unwrap();
assert!(output.is_some());

View File

@ -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(&params, &vec![], &HashMap::new());
let output = extension.calculate(&params, &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(&params, &vec![], &HashMap::new());
let output = extension.calculate(&params, &vec![], &HashMap::new()).unwrap();
assert!(output.is_some());
assert_eq!(
@ -213,7 +213,7 @@ mod tests {
);
let extension = ScriptExtension::new();
let output = extension.calculate(&params, &vec!["jon".to_owned()], &HashMap::new());
let output = extension.calculate(&params, &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(&params, &vec!["jon".to_owned()], &HashMap::new());
let output = extension.calculate(&params, &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(&params, &vec![], &vars);
let output = extension.calculate(&params, &vec![], &vars).unwrap();
assert!(output.is_some());
assert_eq!(

View File

@ -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(&params, &vec![], &HashMap::new());
let output = extension
.calculate(&params, &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(&params, &vec![], &HashMap::new());
let output = extension
.calculate(&params, &vec![], &HashMap::new())
.unwrap();
assert!(output.is_some());
assert_eq!(
@ -301,7 +321,9 @@ mod tests {
);
let extension = ShellExtension::new();
let output = extension.calculate(&params, &vec![], &HashMap::new());
let output = extension
.calculate(&params, &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(&params, &vec![], &HashMap::new());
let output = extension
.calculate(&params, &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(&params, &vec![], &HashMap::new());
let output = extension
.calculate(&params, &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(&params, &vec!["hello".to_owned()], &HashMap::new());
let output = extension
.calculate(&params, &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(&params, &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(&params, &vec!["hello".to_owned()], &HashMap::new());
let output = extension
.calculate(&params, &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(&params, &vec![], &vars);
let output = extension.calculate(&params, &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(&params, &vec![], &vars);
let output = extension.calculate(&params, &vec![], &vars).unwrap();
assert!(output.is_some());
assert_eq!(output.unwrap(), ExtensionResult::Single("John".to_owned()));

View File

@ -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)
}
}
}

View File

@ -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;

View File

@ -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");
}
}

View File

@ -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);
}
}
}
}

View File

@ -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};

View File

@ -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/";

View File

@ -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());

View File

@ -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());

View File

@ -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());

View File

@ -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;

View File

@ -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};