commit
7e2c6ac7ff
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -370,7 +370,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "espanso"
|
||||
version = "0.3.5"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "espanso"
|
||||
version = "0.3.5"
|
||||
version = "0.4.0"
|
||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||
license = "GPL-3.0"
|
||||
description = "Cross-platform Text Expander written in Rust"
|
||||
|
|
|
@ -41,6 +41,13 @@ ___
|
|||
|
||||
Visit the [official documentation](https://espanso.org/docs/).
|
||||
|
||||
## Support
|
||||
|
||||
If you need some help to setup espanso, want to ask a question or simply get involved
|
||||
in the community, [Join the official Subreddit](https://www.reddit.com/r/espanso/)! :)
|
||||
|
||||
|
||||
|
||||
## Donations
|
||||
|
||||
espanso is a free, open source software developed in my (little) spare time.
|
||||
|
@ -57,6 +64,7 @@ Many people helped the project along the way, thanks to all of you. In particula
|
|||
* [Luca Antognetti](https://github.com/luca-ant) - Linux and Windows Tester
|
||||
* [Matteo Pellegrino](https://www.matteopellegrino.me/) - MacOS Tester
|
||||
* [Timo Runge](http://timorunge.com/) - MacOS contributor
|
||||
* [NickSeagull](http://nickseagull.github.io/) - Contributor
|
||||
|
||||
## Remarks
|
||||
|
||||
|
|
|
@ -299,6 +299,10 @@ void trigger_terminal_paste() {
|
|||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Control_L+Shift+v", 8000);
|
||||
}
|
||||
|
||||
void trigger_shift_ins_paste() {
|
||||
xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Shift+Insert", 8000);
|
||||
}
|
||||
|
||||
// SYSTEM MODULE
|
||||
|
||||
// Function taken from the wmlib tool source code
|
||||
|
@ -470,3 +474,5 @@ int32_t is_current_window_terminal() {
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -82,6 +82,11 @@ extern "C" void trigger_paste();
|
|||
*/
|
||||
extern "C" void trigger_terminal_paste();
|
||||
|
||||
/*
|
||||
* Trigger shift ins pasting( Pressing SHIFT+INS )
|
||||
*/
|
||||
extern "C" void trigger_shift_ins_paste();
|
||||
|
||||
|
||||
// SYSTEM MODULE
|
||||
|
||||
|
|
|
@ -148,5 +148,11 @@ int32_t get_clipboard(char * buffer, int32_t size);
|
|||
*/
|
||||
int32_t set_clipboard(char * text);
|
||||
|
||||
/*
|
||||
* Set the clipboard image to the given file
|
||||
*/
|
||||
int32_t set_clipboard_image(char * path);
|
||||
|
||||
|
||||
};
|
||||
#endif //ESPANSO_BRIDGE_H
|
||||
|
|
|
@ -230,6 +230,24 @@ int32_t set_clipboard(char * text) {
|
|||
[pasteboard setString:nsText forType:NSPasteboardTypeString];
|
||||
}
|
||||
|
||||
int32_t set_clipboard_image(char *path) {
|
||||
NSString *pathString = [NSString stringWithUTF8String:path];
|
||||
NSImage *image = [[NSImage alloc] initWithContentsOfFile:pathString];
|
||||
int result = 0;
|
||||
|
||||
if (image != nil) {
|
||||
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
|
||||
[pasteboard clearContents];
|
||||
NSArray *copiedObjects = [NSArray arrayWithObject:image];
|
||||
[pasteboard writeObjects:copiedObjects];
|
||||
result = 1;
|
||||
}
|
||||
[image release];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// CONTEXT MENU
|
||||
|
||||
int32_t show_context_menu(MenuItem * items, int32_t count) {
|
||||
|
@ -273,4 +291,4 @@ int32_t prompt_accessibility() {
|
|||
void open_settings_panel() {
|
||||
NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]];
|
||||
}
|
||||
}
|
|
@ -31,6 +31,9 @@
|
|||
#include <strsafe.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#pragma comment( lib, "gdiplus.lib" )
|
||||
#include <gdiplus.h>
|
||||
|
||||
// How many milliseconds must pass between keystrokes to refresh the keyboard layout
|
||||
const long refreshKeyboardLayoutInterval = 2000;
|
||||
|
||||
|
@ -656,3 +659,43 @@ int32_t get_clipboard(wchar_t *buffer, int32_t size) {
|
|||
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
int32_t set_clipboard_image(wchar_t *path) {
|
||||
bool result = false;
|
||||
|
||||
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
|
||||
ULONG_PTR gdiplusToken;
|
||||
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
|
||||
|
||||
Gdiplus::Bitmap *gdibmp = Gdiplus::Bitmap::FromFile(path);
|
||||
if (gdibmp)
|
||||
{
|
||||
HBITMAP hbitmap;
|
||||
gdibmp->GetHBITMAP(0, &hbitmap);
|
||||
if (OpenClipboard(NULL))
|
||||
{
|
||||
EmptyClipboard();
|
||||
DIBSECTION ds;
|
||||
if (GetObject(hbitmap, sizeof(DIBSECTION), &ds))
|
||||
{
|
||||
HDC hdc = GetDC(HWND_DESKTOP);
|
||||
//create compatible bitmap (get DDB from DIB)
|
||||
HBITMAP hbitmap_ddb = CreateDIBitmap(hdc, &ds.dsBmih, CBM_INIT,
|
||||
ds.dsBm.bmBits, (BITMAPINFO*)&ds.dsBmih, DIB_RGB_COLORS);
|
||||
ReleaseDC(HWND_DESKTOP, hdc);
|
||||
SetClipboardData(CF_BITMAP, hbitmap_ddb);
|
||||
DeleteObject(hbitmap_ddb);
|
||||
result = true;
|
||||
}
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
//cleanup:
|
||||
DeleteObject(hbitmap);
|
||||
delete gdibmp;
|
||||
}
|
||||
|
||||
Gdiplus::GdiplusShutdown(gdiplusToken);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -146,4 +146,9 @@ extern "C" int32_t get_clipboard(wchar_t * buffer, int32_t size);
|
|||
*/
|
||||
extern "C" int32_t set_clipboard(wchar_t * text);
|
||||
|
||||
/*
|
||||
* Set the clipboard image to the given path
|
||||
*/
|
||||
extern "C" int32_t set_clipboard_image(wchar_t * path);
|
||||
|
||||
#endif //ESPANSO_BRIDGE_H
|
|
@ -42,4 +42,5 @@ extern {
|
|||
pub fn left_arrow(count: i32);
|
||||
pub fn trigger_paste();
|
||||
pub fn trigger_terminal_paste();
|
||||
pub fn trigger_shift_ins_paste();
|
||||
}
|
|
@ -43,6 +43,7 @@ extern {
|
|||
// Clipboard
|
||||
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;
|
||||
|
||||
// UI
|
||||
pub fn register_icon_click_callback(cb: extern fn(_self: *mut c_void));
|
||||
|
|
|
@ -47,6 +47,7 @@ extern {
|
|||
// CLIPBOARD
|
||||
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;
|
||||
|
||||
// KEYBOARD
|
||||
pub fn register_keypress_callback(cb: extern fn(_self: *mut c_void, *const u16,
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
|
||||
use std::process::{Command, Stdio};
|
||||
use std::io::{Write};
|
||||
use log::error;
|
||||
use log::{error, warn};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct LinuxClipboardManager {}
|
||||
|
||||
|
@ -63,6 +64,29 @@ impl super::ClipboardManager for LinuxClipboardManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_clipboard_image(&self, image_path: &Path) {
|
||||
let extension = image_path.extension();
|
||||
let mime = match extension {
|
||||
Some(ext) => {
|
||||
let ext = ext.to_string_lossy().to_lowercase();
|
||||
match ext.as_ref() {
|
||||
"png" => {"image/png"},
|
||||
"jpg" | "jpeg" => {"image/jpeg"},
|
||||
"gif" => {"image/gif"},
|
||||
"svg" => {"image/svg"},
|
||||
_ => {"image/png"},
|
||||
}
|
||||
},
|
||||
None => {"image/png"},
|
||||
};
|
||||
|
||||
let image_path = image_path.to_string_lossy().into_owned();
|
||||
|
||||
let res = Command::new("xclip")
|
||||
.args(&["-selection", "clipboard", "-t", mime, "-i", &image_path])
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
|
||||
impl LinuxClipboardManager {
|
||||
|
|
|
@ -18,8 +18,10 @@
|
|||
*/
|
||||
|
||||
use std::os::raw::c_char;
|
||||
use crate::bridge::macos::{get_clipboard, set_clipboard};
|
||||
use crate::bridge::macos::*;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::path::Path;
|
||||
use log::{error, warn};
|
||||
|
||||
pub struct MacClipboardManager {
|
||||
|
||||
|
@ -52,6 +54,19 @@ impl super::ClipboardManager for MacClipboardManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_clipboard_image(&self, image_path: &Path) {
|
||||
let path_string = image_path.to_string_lossy().into_owned();
|
||||
let res = CString::new(path_string);
|
||||
if let Ok(path) = res {
|
||||
unsafe {
|
||||
let result = set_clipboard_image(path.as_ptr());
|
||||
if result != 1 {
|
||||
warn!("Couldn't set clipboard for image: {:?}", image_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MacClipboardManager {
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
|
@ -29,6 +31,7 @@ mod macos;
|
|||
pub trait ClipboardManager {
|
||||
fn get_clipboard(&self) -> Option<String>;
|
||||
fn set_clipboard(&self, payload: &str);
|
||||
fn set_clipboard_image(&self, image_path: &Path);
|
||||
}
|
||||
|
||||
// LINUX IMPLEMENTATION
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
*/
|
||||
|
||||
use widestring::U16CString;
|
||||
use crate::bridge::windows::{set_clipboard, get_clipboard};
|
||||
use crate::bridge::windows::{set_clipboard, get_clipboard, set_clipboard_image};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct WindowsClipboardManager {
|
||||
|
||||
|
@ -53,4 +54,12 @@ impl super::ClipboardManager for WindowsClipboardManager {
|
|||
set_clipboard(payload_c.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
fn set_clipboard_image(&self, image_path: &Path) {
|
||||
let path_string = image_path.to_string_lossy().into_owned();
|
||||
unsafe {
|
||||
let payload_c = U16CString::from_str(path_string).unwrap();
|
||||
set_clipboard_image(payload_c.as_ptr());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ use std::fs::{File, create_dir_all};
|
|||
use std::io::Read;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::event::KeyModifier;
|
||||
use crate::keyboard::PasteShortcut;
|
||||
use std::collections::{HashSet, HashMap};
|
||||
use log::{error};
|
||||
use std::fmt;
|
||||
|
@ -97,6 +98,9 @@ pub struct Configs {
|
|||
#[serde(default = "default_toggle_interval")]
|
||||
pub toggle_interval: u32,
|
||||
|
||||
#[serde(default)]
|
||||
pub paste_shortcut: PasteShortcut,
|
||||
|
||||
#[serde(default = "default_backspace_limit")]
|
||||
pub backspace_limit: i32,
|
||||
|
||||
|
@ -426,6 +430,7 @@ mod tests {
|
|||
use std::io::Write;
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
use std::any::Any;
|
||||
use crate::matcher::{TextContent, MatchContentType};
|
||||
|
||||
const TEST_WORKING_CONFIG_FILE : &str = include_str!("../res/test/working_config.yml");
|
||||
const TEST_CONFIG_FILE_WITH_BAD_YAML : &str = include_str!("../res/test/config_with_bad_yaml.yml");
|
||||
|
@ -727,7 +732,13 @@ mod tests {
|
|||
assert_eq!(config_set.default.matches.len(), 2);
|
||||
assert_eq!(config_set.specific[0].matches.len(), 2);
|
||||
|
||||
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":lol" && x.replace == "newstring").is_some());
|
||||
assert!(config_set.specific[0].matches.iter().find(|x| {
|
||||
if let MatchContentType::Text(content) = &x.content {
|
||||
x.trigger == ":lol" && content.replace == "newstring"
|
||||
}else{
|
||||
false
|
||||
}
|
||||
}).is_some());
|
||||
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":yess").is_some());
|
||||
}
|
||||
|
||||
|
@ -755,7 +766,13 @@ mod tests {
|
|||
assert_eq!(config_set.default.matches.len(), 2);
|
||||
assert_eq!(config_set.specific[0].matches.len(), 1);
|
||||
|
||||
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == "hello" && x.replace == "newstring").is_some());
|
||||
assert!(config_set.specific[0].matches.iter().find(|x| {
|
||||
if let MatchContentType::Text(content) = &x.content {
|
||||
x.trigger == "hello" && content.replace == "newstring"
|
||||
}else{
|
||||
false
|
||||
}
|
||||
}).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -897,7 +914,13 @@ mod tests {
|
|||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.len(), 0);
|
||||
assert_eq!(config_set.default.matches.len(), 1);
|
||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta" && m.replace == "world"));
|
||||
assert!(config_set.default.matches.iter().any(|m| {
|
||||
if let MatchContentType::Text(content) = &m.content {
|
||||
m.trigger == "hasta" && content.replace == "world"
|
||||
}else{
|
||||
false
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
188
src/engine.rs
188
src/engine.rs
|
@ -17,7 +17,7 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::matcher::{Match, MatchReceiver};
|
||||
use crate::matcher::{Match, MatchReceiver, MatchContentType};
|
||||
use crate::keyboard::KeyboardManager;
|
||||
use crate::config::ConfigManager;
|
||||
use crate::config::BackendType;
|
||||
|
@ -29,6 +29,7 @@ use crate::extension::Extension;
|
|||
use std::cell::RefCell;
|
||||
use std::process::exit;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use regex::{Regex, Captures};
|
||||
|
||||
pub struct Engine<'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>,
|
||||
|
@ -118,96 +119,113 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
|||
|
||||
self.keyboard_manager.delete_string(char_count);
|
||||
|
||||
let mut target_string = if m._has_vars {
|
||||
let mut output_map = HashMap::new();
|
||||
// Manage the different types of matches
|
||||
match &m.content {
|
||||
// Text Match
|
||||
MatchContentType::Text(content) => {
|
||||
let mut target_string = if content._has_vars {
|
||||
let mut output_map = HashMap::new();
|
||||
|
||||
for variable in m.vars.iter() {
|
||||
let extension = self.extension_map.get(&variable.var_type);
|
||||
if let Some(extension) = extension {
|
||||
let ext_out = extension.calculate(&variable.params);
|
||||
if let Some(output) = ext_out {
|
||||
output_map.insert(variable.name.clone(), output);
|
||||
}else{
|
||||
output_map.insert(variable.name.clone(), "".to_owned());
|
||||
warn!("Could not generate output for variable: {}", variable.name);
|
||||
}
|
||||
}else{
|
||||
error!("No extension found for variable type: {}", variable.var_type);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the variables
|
||||
let result = VAR_REGEX.replace_all(&m.replace, |caps: &Captures| {
|
||||
let var_name = caps.name("name").unwrap().as_str();
|
||||
let output = output_map.get(var_name);
|
||||
output.unwrap()
|
||||
});
|
||||
|
||||
result.to_string()
|
||||
}else{ // No variables, simple text substitution
|
||||
m.replace.clone()
|
||||
};
|
||||
|
||||
// If a trailing separator was counted in the match, add it back to the target string
|
||||
if let Some(trailing_separator) = trailing_separator {
|
||||
if trailing_separator == '\r' { // If the trailing separator is a carriage return,
|
||||
target_string.push('\n'); // convert it to new line
|
||||
}else{
|
||||
target_string.push(trailing_separator);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Windows style newlines into unix styles
|
||||
target_string = target_string.replace("\r\n", "\n");
|
||||
|
||||
// Calculate cursor rewind moves if a Cursor Hint is present
|
||||
let index = target_string.find("$|$");
|
||||
let cursor_rewind = if let Some(index) = index {
|
||||
// Convert the byte index to a char index
|
||||
let char_str = &target_string[0..index];
|
||||
let char_index = char_str.chars().count();
|
||||
let total_size = target_string.chars().count();
|
||||
|
||||
// Remove the $|$ placeholder
|
||||
target_string = target_string.replace("$|$", "");
|
||||
|
||||
// Calculate the amount of rewind moves needed (LEFT ARROW).
|
||||
// Subtract also 3, equal to the number of chars of the placeholder "$|$"
|
||||
let moves = (total_size - char_index - 3) as i32;
|
||||
Some(moves)
|
||||
}else{
|
||||
None
|
||||
};
|
||||
|
||||
match config.backend {
|
||||
BackendType::Inject => {
|
||||
// Send the expected string. On linux, newlines are managed automatically
|
||||
// while on windows and macos, we need to emulate a Enter key press.
|
||||
|
||||
if cfg!(target_os = "linux") {
|
||||
self.keyboard_manager.send_string(&target_string);
|
||||
}else{
|
||||
// To handle newlines, substitute each "\n" char with an Enter key press.
|
||||
let splits = target_string.split('\n');
|
||||
|
||||
for (i, split) in splits.enumerate() {
|
||||
if i > 0 {
|
||||
self.keyboard_manager.send_enter();
|
||||
for variable in content.vars.iter() {
|
||||
let extension = self.extension_map.get(&variable.var_type);
|
||||
if let Some(extension) = extension {
|
||||
let ext_out = extension.calculate(&variable.params);
|
||||
if let Some(output) = ext_out {
|
||||
output_map.insert(variable.name.clone(), output);
|
||||
}else{
|
||||
output_map.insert(variable.name.clone(), "".to_owned());
|
||||
warn!("Could not generate output for variable: {}", variable.name);
|
||||
}
|
||||
}else{
|
||||
error!("No extension found for variable type: {}", variable.var_type);
|
||||
}
|
||||
}
|
||||
|
||||
self.keyboard_manager.send_string(split);
|
||||
// Replace the variables
|
||||
let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
|
||||
let var_name = caps.name("name").unwrap().as_str();
|
||||
let output = output_map.get(var_name);
|
||||
output.unwrap()
|
||||
});
|
||||
|
||||
result.to_string()
|
||||
}else{ // No variables, simple text substitution
|
||||
content.replace.clone()
|
||||
};
|
||||
|
||||
// If a trailing separator was counted in the match, add it back to the target string
|
||||
if let Some(trailing_separator) = trailing_separator {
|
||||
if trailing_separator == '\r' { // If the trailing separator is a carriage return,
|
||||
target_string.push('\n'); // convert it to new line
|
||||
}else{
|
||||
target_string.push(trailing_separator);
|
||||
}
|
||||
}
|
||||
},
|
||||
BackendType::Clipboard => {
|
||||
self.clipboard_manager.set_clipboard(&target_string);
|
||||
self.keyboard_manager.trigger_paste();
|
||||
},
|
||||
}
|
||||
|
||||
if let Some(moves) = cursor_rewind {
|
||||
// Simulate left arrow key presses to bring the cursor into the desired position
|
||||
self.keyboard_manager.move_cursor_left(moves);
|
||||
// Convert Windows style newlines into unix styles
|
||||
target_string = target_string.replace("\r\n", "\n");
|
||||
|
||||
// Calculate cursor rewind moves if a Cursor Hint is present
|
||||
let index = target_string.find("$|$");
|
||||
let cursor_rewind = if let Some(index) = index {
|
||||
// Convert the byte index to a char index
|
||||
let char_str = &target_string[0..index];
|
||||
let char_index = char_str.chars().count();
|
||||
let total_size = target_string.chars().count();
|
||||
|
||||
// Remove the $|$ placeholder
|
||||
target_string = target_string.replace("$|$", "");
|
||||
|
||||
// Calculate the amount of rewind moves needed (LEFT ARROW).
|
||||
// Subtract also 3, equal to the number of chars of the placeholder "$|$"
|
||||
let moves = (total_size - char_index - 3) as i32;
|
||||
Some(moves)
|
||||
}else{
|
||||
None
|
||||
};
|
||||
|
||||
match config.backend {
|
||||
BackendType::Inject => {
|
||||
// Send the expected string. On linux, newlines are managed automatically
|
||||
// while on windows and macos, we need to emulate a Enter key press.
|
||||
|
||||
if cfg!(target_os = "linux") {
|
||||
self.keyboard_manager.send_string(&target_string);
|
||||
}else{
|
||||
// To handle newlines, substitute each "\n" char with an Enter key press.
|
||||
let splits = target_string.split('\n');
|
||||
|
||||
for (i, split) in splits.enumerate() {
|
||||
if i > 0 {
|
||||
self.keyboard_manager.send_enter();
|
||||
}
|
||||
|
||||
self.keyboard_manager.send_string(split);
|
||||
}
|
||||
}
|
||||
},
|
||||
BackendType::Clipboard => {
|
||||
self.clipboard_manager.set_clipboard(&target_string);
|
||||
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
|
||||
},
|
||||
}
|
||||
|
||||
if let Some(moves) = cursor_rewind {
|
||||
// Simulate left arrow key presses to bring the cursor into the desired position
|
||||
self.keyboard_manager.move_cursor_left(moves);
|
||||
}
|
||||
},
|
||||
|
||||
// Image Match
|
||||
MatchContentType::Image(content) => {
|
||||
// Make sure the image exist beforehand
|
||||
if content.path.exists() {
|
||||
self.clipboard_manager.set_clipboard_image(&content.path);
|
||||
self.keyboard_manager.trigger_paste(&config.paste_shortcut);
|
||||
}else{
|
||||
error!("Image not found in path: {:?}", content.path);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ pub enum KeyModifier {
|
|||
ALT,
|
||||
META,
|
||||
BACKSPACE,
|
||||
OFF,
|
||||
}
|
||||
|
||||
impl Default for KeyModifier {
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
use std::ffi::CString;
|
||||
use crate::bridge::linux::*;
|
||||
use super::PasteShortcut;
|
||||
use log::error;
|
||||
|
||||
pub struct LinuxKeyboardManager {
|
||||
}
|
||||
|
@ -36,16 +38,32 @@ impl super::KeyboardManager for LinuxKeyboardManager {
|
|||
// On linux this is not needed, so NOOP
|
||||
}
|
||||
|
||||
fn trigger_paste(&self) {
|
||||
fn trigger_paste(&self, shortcut: &PasteShortcut) {
|
||||
unsafe {
|
||||
let is_terminal = is_current_window_terminal();
|
||||
match shortcut {
|
||||
PasteShortcut::Default => {
|
||||
let is_terminal = is_current_window_terminal();
|
||||
|
||||
// Terminals use a different keyboard combination to paste from clipboard,
|
||||
// so we need to check the correct situation.
|
||||
if is_terminal == 0 {
|
||||
trigger_paste();
|
||||
}else{
|
||||
trigger_terminal_paste();
|
||||
// Terminals use a different keyboard combination to paste from clipboard,
|
||||
// so we need to check the correct situation.
|
||||
if is_terminal == 0 {
|
||||
trigger_paste();
|
||||
}else{
|
||||
trigger_terminal_paste();
|
||||
}
|
||||
},
|
||||
PasteShortcut::CtrlV => {
|
||||
trigger_paste();
|
||||
},
|
||||
PasteShortcut::CtrlShiftV => {
|
||||
trigger_terminal_paste();
|
||||
},
|
||||
PasteShortcut::ShiftInsert=> {
|
||||
trigger_shift_ins_paste();
|
||||
},
|
||||
_ => {
|
||||
error!("Linux backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
use std::ffi::CString;
|
||||
use crate::bridge::macos::*;
|
||||
use super::PasteShortcut;
|
||||
use log::error;
|
||||
|
||||
pub struct MacKeyboardManager {
|
||||
}
|
||||
|
@ -39,9 +41,18 @@ impl super::KeyboardManager for MacKeyboardManager {
|
|||
}
|
||||
}
|
||||
|
||||
fn trigger_paste(&self) {
|
||||
fn trigger_paste(&self, shortcut: &PasteShortcut) {
|
||||
unsafe {
|
||||
trigger_paste();
|
||||
match shortcut {
|
||||
PasteShortcut::Default => {
|
||||
unsafe {
|
||||
trigger_paste();
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
error!("MacOS backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use serde::{Serialize, Deserialize, Deserializer};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
|
@ -29,11 +31,26 @@ mod macos;
|
|||
pub trait KeyboardManager {
|
||||
fn send_string(&self, s: &str);
|
||||
fn send_enter(&self);
|
||||
fn trigger_paste(&self);
|
||||
fn trigger_paste(&self, shortcut: &PasteShortcut);
|
||||
fn delete_string(&self, count: i32);
|
||||
fn move_cursor_left(&self, count: i32);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum PasteShortcut {
|
||||
Default, // Default one for the current system
|
||||
CtrlV, // Classic Ctrl+V shortcut
|
||||
CtrlShiftV, // Could be used to paste without formatting in many applications
|
||||
ShiftInsert, // Often used in Linux systems
|
||||
MetaV, // Corresponding to Win+V on Windows and Linux, CMD+V on macOS
|
||||
}
|
||||
|
||||
impl Default for PasteShortcut{
|
||||
fn default() -> Self {
|
||||
PasteShortcut::Default
|
||||
}
|
||||
}
|
||||
|
||||
// WINDOWS IMPLEMENTATION
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_manager() -> impl KeyboardManager {
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
use widestring::{U16CString};
|
||||
use crate::bridge::windows::*;
|
||||
use super::PasteShortcut;
|
||||
use log::error;
|
||||
|
||||
pub struct WindowsKeyboardManager {
|
||||
}
|
||||
|
@ -44,9 +46,18 @@ impl super::KeyboardManager for WindowsKeyboardManager {
|
|||
}
|
||||
}
|
||||
|
||||
fn trigger_paste(&self) {
|
||||
fn trigger_paste(&self, shortcut: &PasteShortcut) {
|
||||
unsafe {
|
||||
trigger_paste();
|
||||
match shortcut {
|
||||
PasteShortcut::Default => {
|
||||
unsafe {
|
||||
trigger_paste();
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
error!("Windows backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,24 +22,42 @@ use crate::event::{KeyEvent, KeyModifier};
|
|||
use crate::event::KeyEventReceiver;
|
||||
use serde_yaml::Mapping;
|
||||
use regex::Regex;
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
pub(crate) mod scrolling;
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct Match {
|
||||
pub trigger: String,
|
||||
pub replace: String,
|
||||
pub vars: Vec<MatchVariable>,
|
||||
pub content: MatchContentType,
|
||||
pub word: bool,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub _has_vars: bool,
|
||||
|
||||
// Automatically calculated from the trigger, used by the matcher to check for correspondences.
|
||||
#[serde(skip_serializing)]
|
||||
pub _trigger_sequence: Vec<TriggerEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub enum MatchContentType {
|
||||
Text(TextContent),
|
||||
Image(ImageContent),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct TextContent {
|
||||
pub replace: String,
|
||||
pub vars: Vec<MatchVariable>,
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
pub _has_vars: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct ImageContent {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl <'de> serde::Deserialize<'de> for Match {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where
|
||||
D: Deserializer<'de> {
|
||||
|
@ -53,15 +71,10 @@ impl<'a> From<&'a AutoMatch> for Match{
|
|||
fn from(other: &'a AutoMatch) -> Self {
|
||||
lazy_static! {
|
||||
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)\\s*\\}\\}").unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: may need to replace windows newline (\r\n) with newline only (\n)
|
||||
|
||||
let new_replace = other.replace.clone();
|
||||
|
||||
// Check if the match contains variables
|
||||
let has_vars = VAR_REGEX.is_match(&other.replace);
|
||||
|
||||
// Calculate the trigger sequence
|
||||
let mut trigger_sequence = Vec::new();
|
||||
let trigger_chars : Vec<char> = other.trigger.chars().collect();
|
||||
|
@ -72,12 +85,55 @@ impl<'a> From<&'a AutoMatch> for Match{
|
|||
trigger_sequence.push(TriggerEntry::WordSeparator);
|
||||
}
|
||||
|
||||
let content = if let Some(replace) = &other.replace { // Text match
|
||||
let new_replace = replace.clone();
|
||||
|
||||
// Check if the match contains variables
|
||||
let has_vars = VAR_REGEX.is_match(replace);
|
||||
|
||||
let content = TextContent {
|
||||
replace: new_replace,
|
||||
vars: other.vars.clone(),
|
||||
_has_vars: has_vars,
|
||||
};
|
||||
|
||||
MatchContentType::Text(content)
|
||||
}else if let Some(image_path) = &other.image_path { // Image match
|
||||
// On Windows, we have to replace the forward / with the backslash \ in the path
|
||||
let new_path = if cfg!(target_os = "windows") {
|
||||
image_path.replace("/", "\\")
|
||||
}else{
|
||||
image_path.to_owned()
|
||||
};
|
||||
|
||||
// Calculate variables in path
|
||||
let new_path = if new_path.contains("$CONFIG") {
|
||||
let config_dir = crate::context::get_config_dir();
|
||||
let config_path = fs::canonicalize(&config_dir);
|
||||
let config_path = if let Ok(config_path) = config_path {
|
||||
config_path.to_string_lossy().into_owned()
|
||||
}else{
|
||||
"".to_owned()
|
||||
};
|
||||
new_path.replace("$CONFIG", &config_path)
|
||||
}else{
|
||||
new_path.to_owned()
|
||||
};
|
||||
|
||||
let content = ImageContent {
|
||||
path: PathBuf::from(new_path)
|
||||
};
|
||||
|
||||
MatchContentType::Image(content)
|
||||
}else {
|
||||
eprintln!("ERROR: no action specified for match {}, please specify either 'replace' or 'image_path'", other.trigger);
|
||||
std::process::exit(2);
|
||||
};
|
||||
|
||||
Self {
|
||||
trigger: other.trigger.clone(),
|
||||
replace: new_replace,
|
||||
vars: other.vars.clone(),
|
||||
content,
|
||||
word: other.word.clone(),
|
||||
_has_vars: has_vars,
|
||||
_trigger_sequence: trigger_sequence,
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +143,12 @@ impl<'a> From<&'a AutoMatch> for Match{
|
|||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct AutoMatch {
|
||||
pub trigger: String,
|
||||
pub replace: String,
|
||||
|
||||
#[serde(default = "default_replace")]
|
||||
pub replace: Option<String>,
|
||||
|
||||
#[serde(default = "default_image_path")]
|
||||
pub image_path: Option<String>,
|
||||
|
||||
#[serde(default = "default_vars")]
|
||||
pub vars: Vec<MatchVariable>,
|
||||
|
@ -98,6 +159,8 @@ struct AutoMatch {
|
|||
|
||||
fn default_vars() -> Vec<MatchVariable> {Vec::new()}
|
||||
fn default_word() -> bool {false}
|
||||
fn default_replace() -> Option<String> {None}
|
||||
fn default_image_path() -> Option<String> {None}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct MatchVariable {
|
||||
|
@ -154,7 +217,14 @@ mod tests {
|
|||
|
||||
let _match : Match = serde_yaml::from_str(match_str).unwrap();
|
||||
|
||||
assert_eq!(_match._has_vars, false);
|
||||
match _match.content {
|
||||
MatchContentType::Text(content) => {
|
||||
assert_eq!(content._has_vars, false);
|
||||
},
|
||||
_ => {
|
||||
assert!(false);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -166,7 +236,14 @@ mod tests {
|
|||
|
||||
let _match : Match = serde_yaml::from_str(match_str).unwrap();
|
||||
|
||||
assert_eq!(_match._has_vars, true);
|
||||
match _match.content {
|
||||
MatchContentType::Text(content) => {
|
||||
assert_eq!(content._has_vars, true);
|
||||
},
|
||||
_ => {
|
||||
assert!(false);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -178,7 +255,14 @@ mod tests {
|
|||
|
||||
let _match : Match = serde_yaml::from_str(match_str).unwrap();
|
||||
|
||||
assert_eq!(_match._has_vars, true);
|
||||
match _match.content {
|
||||
MatchContentType::Text(content) => {
|
||||
assert_eq!(content._has_vars, true);
|
||||
},
|
||||
_ => {
|
||||
assert!(false);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -212,4 +296,23 @@ mod tests {
|
|||
assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t'));
|
||||
assert_eq!(_match._trigger_sequence[4], TriggerEntry::WordSeparator);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_with_image_content() {
|
||||
let match_str = r###"
|
||||
trigger: "test"
|
||||
image_path: "/path/to/file"
|
||||
"###;
|
||||
|
||||
let _match : Match = serde_yaml::from_str(match_str).unwrap();
|
||||
|
||||
match _match.content {
|
||||
MatchContentType::Image(content) => {
|
||||
assert_eq!(content.path, PathBuf::from("/path/to/file"));
|
||||
},
|
||||
_ => {
|
||||
assert!(false);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -194,6 +194,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
|||
let config = self.config_manager.default_config();
|
||||
|
||||
if m == config.toggle_key {
|
||||
if m == KeyModifier::OFF { return }
|
||||
let mut toggle_press_time = self.toggle_press_time.borrow_mut();
|
||||
if let Ok(elapsed) = toggle_press_time.elapsed() {
|
||||
if elapsed.as_millis() < u128::from(config.toggle_interval) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user