Merge branch 'image-matches' into dev

This commit is contained in:
Federico Terzi 2019-11-29 21:45:46 +01:00
commit 615d60b17c
13 changed files with 375 additions and 110 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -426,6 +426,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 +728,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 +762,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 +910,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]

View File

@ -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,10 +119,14 @@ 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 {
// 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() {
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);
@ -137,7 +142,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
}
// Replace the variables
let result = VAR_REGEX.replace_all(&m.replace, |caps: &Captures| {
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()
@ -145,7 +150,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
result.to_string()
}else{ // No variables, simple text substitution
m.replace.clone()
content.replace.clone()
};
// If a trailing separator was counted in the match, add it back to the target string
@ -209,6 +214,19 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
// 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();
}else{
error!("Image not found in path: {:?}", content.path);
}
},
}
}
fn on_enable_update(&self, status: bool) {

View File

@ -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);
}
Self {
trigger: other.trigger.clone(),
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(),
word: other.word.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(),
content,
word: other.word.clone(),
_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, "/path/to/file");
},
_ => {
assert!(false);
},
}
}
}