Merge pull request #349 from federico-terzi/dev

Version 0.6.3
This commit is contained in:
Federico Terzi 2020-06-29 20:32:41 +02:00 committed by GitHub
commit 4878bc58bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 404 additions and 84 deletions

12
Cargo.lock generated
View File

@ -371,7 +371,7 @@ dependencies = [
[[package]] [[package]]
name = "espanso" name = "espanso"
version = "0.6.2" version = "0.6.3"
dependencies = [ dependencies = [
"backtrace 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
@ -384,6 +384,7 @@ dependencies = [
"libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"log-panics 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "log-panics 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"named_pipe 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -802,6 +803,14 @@ dependencies = [
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "named_pipe"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.3" version = "0.2.3"
@ -1911,6 +1920,7 @@ dependencies = [
"checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23" "checksum mio 0.6.19 (registry+https://github.com/rust-lang/crates.io-index)" = "83f51996a3ed004ef184e16818edc51fadffe8e7ca68be67f9dee67d84d0ff23"
"checksum mio-extras 2.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" "checksum mio-extras 2.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
"checksum named_pipe 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad9c443cce91fc3e12f017290db75dde490d685cdaaf508d7159d7cf41f0eb2b"
"checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" "checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e"
"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "espanso" name = "espanso"
version = "0.6.2" version = "0.6.3"
authors = ["Federico Terzi <federicoterzi96@gmail.com>"] authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
license = "GPL-3.0" license = "GPL-3.0"
description = "Cross-platform Text Expander written in Rust" description = "Cross-platform Text Expander written in Rust"
@ -36,6 +36,9 @@ notify = "4.0.13"
libc = "0.2.62" libc = "0.2.62"
signal-hook = "0.1.15" signal-hook = "0.1.15"
[target.'cfg(windows)'.dependencies]
named_pipe = "0.4.1"
[build-dependencies] [build-dependencies]
cmake = "0.1.31" cmake = "0.1.31"

View File

@ -77,14 +77,20 @@ xdo_t * xdo_context;
// Callback invoked when a new key event occur. // Callback invoked when a new key event occur.
void event_callback (XPointer, XRecordInterceptData*); void event_callback (XPointer, XRecordInterceptData*);
int error_callback(Display *display, XErrorEvent *error);
KeypressCallback keypress_callback; KeypressCallback keypress_callback;
X11ErrorCallback x11_error_callback;
void * context_instance; void * context_instance;
void register_keypress_callback(KeypressCallback callback) { void register_keypress_callback(KeypressCallback callback) {
keypress_callback = callback; keypress_callback = callback;
} }
void register_error_callback(X11ErrorCallback callback) {
x11_error_callback = callback;
}
int32_t check_x11() { int32_t check_x11() {
Display *check_disp = XOpenDisplay(NULL); Display *check_disp = XOpenDisplay(NULL);
@ -156,6 +162,9 @@ int32_t initialize(void * _context_instance) {
xdo_context = xdo_new(NULL); xdo_context = xdo_new(NULL);
// Setup a custom error handler
XSetErrorHandler(&error_callback);
/** /**
* Note: We might never get a MappingNotify event if the * Note: We might never get a MappingNotify event if the
* modifier and keymap information was never cached in Xlib. * modifier and keymap information was never cached in Xlib.
@ -272,6 +281,11 @@ void event_callback(XPointer p, XRecordInterceptData *hook)
XRecordFreeData(hook); XRecordFreeData(hook);
} }
int error_callback(Display *display, XErrorEvent *error) {
x11_error_callback(context_instance, error->error_code, error->request_code, error->minor_code);
return 0;
}
void release_all_keys() { void release_all_keys() {
char keys[32]; char keys[32];
XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard

View File

@ -49,7 +49,6 @@ extern "C" void cleanup();
* while the second is the size of the array. * while the second is the size of the array.
*/ */
typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code); typedef void (*KeypressCallback)(void * self, const char *buffer, int32_t len, int32_t event_type, int32_t key_code);
extern KeypressCallback keypress_callback; extern KeypressCallback keypress_callback;
/* /*
@ -57,6 +56,17 @@ extern KeypressCallback keypress_callback;
*/ */
extern "C" void register_keypress_callback(KeypressCallback callback); extern "C" void register_keypress_callback(KeypressCallback callback);
/*
* Called when a X11 error occurs
*/
typedef void (*X11ErrorCallback)(void * self, char error_code, char request_code, char minor_code);
extern X11ErrorCallback x11_error_callback;
/*
* Register the callback that will be called when an X11 error occurs
*/
extern "C" void register_error_callback(X11ErrorCallback callback);
/* /*
* Type the given string by simulating Key Presses * Type the given string by simulating Key Presses
*/ */

View File

@ -566,6 +566,38 @@ void send_multi_vkey_with_delay(int32_t vk, int32_t count, int32_t delay) {
} }
} }
void trigger_shift_paste() {
std::vector<INPUT> vec;
INPUT input = { 0 };
input.type = INPUT_KEYBOARD;
input.ki.wScan = 0;
input.ki.time = 0;
input.ki.dwExtraInfo = 0;
input.ki.wVk = VK_CONTROL;
input.ki.dwFlags = 0; // 0 for key press
vec.push_back(input);
input.ki.wVk = VK_SHIFT; // SHIFT KEY
vec.push_back(input);
input.ki.wVk = 0x56; // V KEY
vec.push_back(input);
input.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
vec.push_back(input);
input.ki.wVk = VK_SHIFT; // SHIFT KEY
vec.push_back(input);
input.ki.wVk = VK_CONTROL;
vec.push_back(input);
SendInput(vec.size(), vec.data(), sizeof(INPUT));
}
void trigger_paste() { void trigger_paste() {
std::vector<INPUT> vec; std::vector<INPUT> vec;

View File

@ -87,6 +87,11 @@ extern "C" void delete_string(int32_t count, int32_t delay);
*/ */
extern "C" void trigger_paste(); extern "C" void trigger_paste();
/*
* Send the Paste keyboard shortcut (CTRL+SHIFT+V)
*/
extern "C" void trigger_shift_paste();
/* /*
* Send the copy keyboard shortcut (CTRL+C) * Send the copy keyboard shortcut (CTRL+C)
*/ */

View File

@ -82,11 +82,24 @@ def build_windows(package_info):
msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*") msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*")
print("Found Redists: ", msvc_dirs) print("Found Redists: ", msvc_dirs)
print("Determining best redist...")
if len(msvc_dirs) == 0: if len(msvc_dirs) == 0:
raise Exception("Cannot find redistributable dlls") raise Exception("Cannot find redistributable dlls")
msvc_dir = msvc_dirs[-1] # Take the most recent version of the toolchain msvc_dir = None
print("Using: ",msvc_dir)
for curr_dir in msvc_dirs:
dll_files = glob.glob(curr_dir + "\\x64\\*CRT\\*.dll")
print("Found dlls", dll_files, "in", curr_dir)
if any("vcruntime140_1.dll" in x.lower() for x in dll_files):
msvc_dir = curr_dir
break
if msvc_dir is None:
raise Exception("Cannot find redist with VCRUNTIME140_1.dll")
print("Using: ", msvc_dir)
dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll") dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll")

View File

@ -1,5 +1,5 @@
name: espanso name: espanso
version: 0.6.2 version: 0.6.3
summary: A Cross-platform Text Expander written in Rust summary: A Cross-platform Text Expander written in Rust
description: | description: |
espanso is a Cross-platform, Text Expander written in Rust. espanso is a Cross-platform, Text Expander written in Rust.

View File

@ -32,6 +32,14 @@ extern "C" {
pub fn get_active_window_class(buffer: *mut c_char, size: i32) -> i32; pub fn get_active_window_class(buffer: *mut c_char, size: i32) -> i32;
pub fn get_active_window_executable(buffer: *mut c_char, size: i32) -> i32; pub fn get_active_window_executable(buffer: *mut c_char, size: i32) -> i32;
pub fn is_current_window_special() -> i32; pub fn is_current_window_special() -> i32;
pub fn register_error_callback(
cb: extern "C" fn(
_self: *mut c_void,
error_code: c_char,
request_code: c_char,
minor_code: c_char,
),
);
// Keyboard // Keyboard
pub fn register_keypress_callback( pub fn register_keypress_callback(

View File

@ -65,6 +65,7 @@ extern "C" {
pub fn send_multi_vkey(vk: i32, count: i32); pub fn send_multi_vkey(vk: i32, count: i32);
pub fn delete_string(count: i32, delay: i32); pub fn delete_string(count: i32, delay: i32);
pub fn trigger_paste(); pub fn trigger_paste();
pub fn trigger_shift_paste();
pub fn trigger_copy(); pub fn trigger_copy();
// PROCESSES // PROCESSES

87
src/cli.rs Normal file
View File

@ -0,0 +1,87 @@
/*
* 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::ConfigSet;
use crate::matcher::{Match, MatchContentType};
use serde::Serialize;
pub fn list_matches(config_set: ConfigSet, onlytriggers: bool, preserve_newlines: bool) {
let matches = filter_matches(config_set);
for m in matches {
for trigger in m.triggers.iter() {
if onlytriggers {
println!("{}", trigger);
} else {
match m.content {
MatchContentType::Text(ref text) => {
let replace = if preserve_newlines {
text.replace.to_owned()
} else {
text.replace.replace("\n", " ")
};
println!("{} - {}", trigger, replace)
}
MatchContentType::Image(_) => {
// Skip image matches for now
}
}
}
}
}
}
#[derive(Debug, Serialize)]
struct JsonMatchEntry {
triggers: Vec<String>,
replace: String,
}
pub fn list_matches_as_json(config_set: ConfigSet) {
let matches = filter_matches(config_set);
let mut entries = Vec::new();
for m in matches {
match m.content {
MatchContentType::Text(ref text) => entries.push(JsonMatchEntry {
triggers: m.triggers,
replace: text.replace.clone(),
}),
MatchContentType::Image(_) => {
// Skip image matches for now
}
}
}
let output = serde_json::to_string(&entries);
println!("{}", output.unwrap_or_default())
}
fn filter_matches(config_set: ConfigSet) -> Vec<Match> {
let mut output = Vec::new();
output.extend(config_set.default.matches);
// TODO: consider specific matches by class, title or exe path
// for specific in config_set.specific {
// output.extend(specific.matches)
// }
output
}

View File

@ -129,7 +129,7 @@ fn default_show_notifications() -> bool {
true true
} }
fn default_auto_restart() -> bool { fn default_auto_restart() -> bool {
true true
} }
fn default_show_icon() -> bool { fn default_show_icon() -> bool {
true true

View File

@ -21,7 +21,7 @@ use crate::bridge::linux::*;
use crate::config::Configs; use crate::config::Configs;
use crate::event::KeyModifier::*; use crate::event::KeyModifier::*;
use crate::event::*; use crate::event::*;
use log::{debug, error}; use log::{debug, error, warn};
use std::ffi::CStr; use std::ffi::CStr;
use std::os::raw::{c_char, c_void}; use std::os::raw::{c_char, c_void};
use std::process::exit; use std::process::exit;
@ -59,6 +59,7 @@ impl LinuxContext {
let context_ptr = &*context as *const LinuxContext as *const c_void; let context_ptr = &*context as *const LinuxContext as *const c_void;
register_keypress_callback(keypress_callback); register_keypress_callback(keypress_callback);
register_error_callback(error_callback);
let res = initialize(context_ptr); let res = initialize(context_ptr);
if res <= 0 { if res <= 0 {
@ -155,3 +156,15 @@ extern "C" fn keypress_callback(
} }
} }
} }
extern "C" fn error_callback(
_self: *mut c_void,
error_code: c_char,
request_code: c_char,
minor_code: c_char,
) {
warn!(
"X11 reported an error code: {}, request_code: {} and minor_code: {}",
error_code, request_code, minor_code
);
}

View File

@ -173,8 +173,7 @@ extern "C" fn keypress_callback(
} }
} else if event_type == 1 { } else if event_type == 1 {
// Modifier event // Modifier event
if is_key_down == 1 { if is_key_down == 0 {
// Keyup event
let modifier: Option<KeyModifier> = match (key_code, variant) { let modifier: Option<KeyModifier> = match (key_code, variant) {
(0x5B, _) => Some(LEFT_META), (0x5B, _) => Some(LEFT_META),
(0x5C, _) => Some(RIGHT_META), (0x5C, _) => Some(RIGHT_META),

View File

@ -132,22 +132,28 @@ impl<
None None
} }
} }
}
lazy_static! { fn find_match_by_trigger(&self, trigger: &str) -> Option<Match> {
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap(); let config = self.config_manager.active_config();
}
impl< if let Some(m) = config
'a, .matches
S: KeyboardManager, .iter()
C: ClipboardManager, .find(|m| m.triggers.iter().any(|t| t == trigger))
M: ConfigManager<'a>, {
U: UIManager, Some(m.clone())
R: Renderer, } else {
> MatchReceiver for Engine<'a, S, C, M, U, R> None
{ }
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) { }
fn inject_match(
&self,
m: &Match,
trailing_separator: Option<char>,
trigger_offset: usize,
skip_delete: bool,
) {
let config = self.config_manager.active_config(); let config = self.config_manager.active_config();
if !config.enable_active { if !config.enable_active {
@ -163,7 +169,9 @@ impl<
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
}; };
self.keyboard_manager.delete_string(&config, char_count); if !skip_delete {
self.keyboard_manager.delete_string(&config, char_count);
}
let mut previous_clipboard_content: Option<String> = None; let mut previous_clipboard_content: Option<String> = None;
@ -287,6 +295,24 @@ impl<
// Re-allow espanso to interpret actions // Re-allow espanso to interpret actions
self.is_injecting.store(false, Release); self.is_injecting.store(false, Release);
} }
}
lazy_static! {
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
}
impl<
'a,
S: KeyboardManager,
C: ClipboardManager,
M: ConfigManager<'a>,
U: UIManager,
R: Renderer,
> MatchReceiver for Engine<'a, S, C, M, U, R>
{
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) {
self.inject_match(m, trailing_separator, trigger_offset, false);
}
fn on_enable_update(&self, status: bool) { fn on_enable_update(&self, status: bool) {
let message = if status { let message = if status {
@ -432,6 +458,15 @@ impl<
self.ui_manager.notify(&message); self.ui_manager.notify(&message);
} }
} }
SystemEvent::Trigger(trigger) => {
let m = self.find_match_by_trigger(&trigger);
match m {
Some(m) => {
self.inject_match(&m, None, 0, true);
}
None => warn!("No match found with trigger: {}", trigger),
}
}
} }
} }
} }

View File

@ -130,6 +130,9 @@ pub enum SystemEvent {
// Notification // Notification
NotifyRequest(String), NotifyRequest(String),
// Trigger an expansion from IPC
Trigger(String),
} }
// Receivers // Receivers

View File

@ -88,7 +88,8 @@ impl super::Extension for ScriptExtension {
match output { match output {
Ok(output) => { Ok(output) => {
let mut output_str = String::from_utf8_lossy(output.stdout.as_slice()).to_string(); let mut output_str =
String::from_utf8_lossy(output.stdout.as_slice()).to_string();
let error_str = String::from_utf8_lossy(output.stderr.as_slice()); let error_str = String::from_utf8_lossy(output.stderr.as_slice());
let error_str = error_str.to_string(); let error_str = error_str.to_string();
let error_str = error_str.trim(); let error_str = error_str.trim();
@ -103,7 +104,7 @@ impl super::Extension for ScriptExtension {
let should_trim = if let Some(value) = trim_opt { let should_trim = if let Some(value) = trim_opt {
let val = value.as_bool(); let val = value.as_bool();
val.unwrap_or(true) val.unwrap_or(true)
}else{ } else {
true true
}; };
@ -154,10 +155,7 @@ mod tests {
Value::from("args"), Value::from("args"),
Value::from(vec!["echo", "hello world"]), Value::from(vec!["echo", "hello world"]),
); );
params.insert( params.insert(Value::from("trim"), Value::from(false));
Value::from("trim"),
Value::from(false),
);
let extension = ScriptExtension::new(); let extension = ScriptExtension::new();
let output = extension.calculate(&params, &vec![]); let output = extension.calculate(&params, &vec![]);

View File

@ -34,6 +34,7 @@ pub enum Shell {
Cmd, Cmd,
Powershell, Powershell,
WSL, WSL,
WSL2,
Bash, Bash,
Sh, Sh,
} }
@ -45,27 +46,32 @@ impl Shell {
let mut command = Command::new("cmd"); let mut command = Command::new("cmd");
command.args(&["/C", &cmd]); command.args(&["/C", &cmd]);
command command
}, }
Shell::Powershell => { Shell::Powershell => {
let mut command = Command::new("powershell"); let mut command = Command::new("powershell");
command.args(&["-Command", &cmd]); command.args(&["-Command", &cmd]);
command command
}, }
Shell::WSL => { Shell::WSL => {
let mut command = Command::new("bash");
command.args(&["-c", &cmd]);
command
}
Shell::WSL2 => {
let mut command = Command::new("wsl"); let mut command = Command::new("wsl");
command.args(&["bash", "-c", &cmd]); command.args(&["bash", "-c", &cmd]);
command command
}, }
Shell::Bash => { Shell::Bash => {
let mut command = Command::new("bash"); let mut command = Command::new("bash");
command.args(&["-c", &cmd]); command.args(&["-c", &cmd]);
command command
}, }
Shell::Sh => { Shell::Sh => {
let mut command = Command::new("sh"); let mut command = Command::new("sh");
command.args(&["-c", &cmd]); command.args(&["-c", &cmd]);
command command
}, }
}; };
// Inject the $CONFIG variable // Inject the $CONFIG variable
@ -79,6 +85,7 @@ impl Shell {
"cmd" => Some(Shell::Cmd), "cmd" => Some(Shell::Cmd),
"powershell" => Some(Shell::Powershell), "powershell" => Some(Shell::Powershell),
"wsl" => Some(Shell::WSL), "wsl" => Some(Shell::WSL),
"wsl2" => Some(Shell::WSL2),
"bash" => Some(Shell::Bash), "bash" => Some(Shell::Bash),
"sh" => Some(Shell::Sh), "sh" => Some(Shell::Sh),
_ => None, _ => None,
@ -169,7 +176,7 @@ impl super::Extension for ShellExtension {
let should_trim = if let Some(value) = trim_opt { let should_trim = if let Some(value) = trim_opt {
let val = value.as_bool(); let val = value.as_bool();
val.unwrap_or(true) val.unwrap_or(true)
}else{ } else {
true true
}; };

View File

@ -51,6 +51,9 @@ impl super::KeyboardManager for WindowsKeyboardManager {
trigger_paste(); trigger_paste();
} }
}, },
PasteShortcut::CtrlShiftV => {
trigger_shift_paste();
},
_ => { _ => {
error!("Windows backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.") error!("Windows backend does not support this Paste Shortcut, please open an issue on GitHub if you need it.")
} }

View File

@ -53,6 +53,7 @@ use crate::ui::UIManager;
mod bridge; mod bridge;
mod check; mod check;
mod cli;
mod clipboard; mod clipboard;
mod config; mod config;
mod context; mod context;
@ -158,6 +159,39 @@ fn main() {
.subcommand(SubCommand::with_name("default") .subcommand(SubCommand::with_name("default")
.about("Print the default configuration file path.")) .about("Print the default configuration file path."))
) )
.subcommand(SubCommand::with_name("match")
.about("List and execute matches from the CLI")
.subcommand(SubCommand::with_name("list")
.about("Print all matches to standard output")
.arg(Arg::with_name("json")
.short("j")
.long("json")
.help("Return the matches as json")
.required(false)
.takes_value(false)
)
.arg(Arg::with_name("onlytriggers")
.short("t")
.long("onlytriggers")
.help("Print only triggers without replacement")
.required(false)
.takes_value(false)
)
.arg(Arg::with_name("preservenewlines")
.short("n")
.long("preservenewlines")
.help("Preserve newlines when printing replacements")
.required(false)
.takes_value(false)
)
)
.subcommand(SubCommand::with_name("exec")
.about("Triggers the expansion of the given match")
.arg(Arg::with_name("trigger")
.help("The trigger of the match to be expanded")
)
)
)
// Package manager // Package manager
.subcommand(SubCommand::with_name("package") .subcommand(SubCommand::with_name("package")
.about("Espanso package manager commands") .about("Espanso package manager commands")
@ -274,6 +308,11 @@ fn main() {
return; return;
} }
if let Some(matches) = matches.subcommand_matches("match") {
match_main(config_set, matches);
return;
}
if let Some(matches) = matches.subcommand_matches("package") { if let Some(matches) = matches.subcommand_matches("package") {
if let Some(matches) = matches.subcommand_matches("install") { if let Some(matches) = matches.subcommand_matches("install") {
install_main(config_set, matches); install_main(config_set, matches);
@ -480,18 +519,14 @@ fn register_signals(_: Configs) {}
fn register_signals(config: Configs) { fn register_signals(config: Configs) {
// On Unix, also listen for signals so that we can terminate the // On Unix, also listen for signals so that we can terminate the
// worker if the daemon receives a signal // worker if the daemon receives a signal
use signal_hook::{iterator::Signals, SIGTERM, SIGINT}; use signal_hook::{iterator::Signals, SIGINT, SIGTERM};
let signals = Signals::new(&[SIGTERM, SIGINT]).expect("unable to register for signals"); let signals = Signals::new(&[SIGTERM, SIGINT]).expect("unable to register for signals");
thread::Builder::new() thread::Builder::new()
.name("signal monitor".to_string()) .name("signal monitor".to_string())
.spawn(move || { .spawn(move || {
for signal in signals.forever() { for signal in signals.forever() {
info!("Received signal: {:?}, terminating worker", signal); info!("Received signal: {:?}, terminating worker", signal);
send_command_or_warn( send_command_or_warn(Service::Worker, config, IPCCommand::exit_worker());
Service::Worker,
config,
IPCCommand::exit_worker(),
);
std::thread::sleep(Duration::from_millis(200)); std::thread::sleep(Duration::from_millis(200));
@ -1027,7 +1062,13 @@ fn install_main(_config_set: ConfigSet, matches: &ArgMatches) {
exit(1); exit(1);
}); });
let repository = matches.value_of("repository_url").unwrap_or("hub"); let mut repository = matches.value_of("repository_url").unwrap_or("hub");
// Remove trailing .git string if present
// See: https://github.com/federico-terzi/espanso/issues/326
if repository.ends_with(".git") {
repository = repository.trim_end_matches(".git")
}
let package_resolver = Box::new(ZipPackageResolver::new()); let package_resolver = Box::new(ZipPackageResolver::new());
@ -1223,6 +1264,31 @@ fn path_main(_config_set: ConfigSet, matches: &ArgMatches) {
} }
} }
fn match_main(config_set: ConfigSet, matches: &ArgMatches) {
if let Some(matches) = matches.subcommand_matches("list") {
let json = matches.is_present("json");
let onlytriggers = matches.is_present("onlytriggers");
let preserve_newlines = matches.is_present("preservenewlines");
if !json {
crate::cli::list_matches(config_set, onlytriggers, preserve_newlines);
} else {
crate::cli::list_matches_as_json(config_set);
}
} else if let Some(matches) = matches.subcommand_matches("exec") {
let trigger = matches.value_of("trigger").unwrap_or_else(|| {
eprintln!("missing trigger");
exit(1);
});
send_command_or_warn(
Service::Worker,
config_set.default.clone(),
IPCCommand::trigger(trigger),
);
}
}
fn edit_main(matches: &ArgMatches) { fn edit_main(matches: &ArgMatches) {
// Determine which is the file to edit // Determine which is the file to edit
let config = matches.value_of("config").unwrap_or("default"); let config = matches.value_of("config").unwrap_or("default");

View File

@ -67,6 +67,7 @@ impl IPCCommand {
"notify" => Some(Event::System(SystemEvent::NotifyRequest( "notify" => Some(Event::System(SystemEvent::NotifyRequest(
self.payload.clone(), self.payload.clone(),
))), ))),
"trigger" => Some(Event::System(SystemEvent::Trigger(self.payload.clone()))),
_ => None, _ => None,
} }
} }
@ -101,6 +102,10 @@ impl IPCCommand {
id: "notify".to_owned(), id: "notify".to_owned(),
payload: message, payload: message,
}), }),
Event::System(SystemEvent::Trigger(trigger)) => Some(IPCCommand {
id: "trigger".to_owned(),
payload: trigger,
}),
_ => None, _ => None,
} }
} }
@ -125,6 +130,13 @@ impl IPCCommand {
payload: "".to_owned(), payload: "".to_owned(),
} }
} }
pub fn trigger(trigger: &str) -> IPCCommand {
Self {
id: "trigger".to_owned(),
payload: trigger.to_owned(),
}
}
} }
fn process_event<R: Read, E: Error>(event_channel: &Sender<Event>, stream: Result<R, E>) { fn process_event<R: Read, E: Error>(event_channel: &Sender<Event>, stream: Result<R, E>) {
@ -200,13 +212,13 @@ pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_ipc_server( pub fn get_ipc_server(
service: Service, service: Service,
config: Configs, _: Configs,
event_channel: Sender<Event>, event_channel: Sender<Event>,
) -> impl IPCServer { ) -> impl IPCServer {
windows::WindowsIPCServer::new(service, config, event_channel) windows::WindowsIPCServer::new(service, event_channel)
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_ipc_client(service: Service, config: Configs) -> impl IPCClient { pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient {
windows::WindowsIPCClient::new(service, config) windows::WindowsIPCClient::new(service)
} }

View File

@ -23,32 +23,33 @@ use std::net::{TcpListener, TcpStream};
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use crate::config::Configs; use crate::config::Configs;
use crate::context;
use crate::event::*; use crate::event::*;
use crate::protocol::{process_event, send_command, Service}; use crate::protocol::{process_event, send_command, Service};
use named_pipe::{PipeClient, PipeOptions, PipeServer};
use std::io::Error;
use std::path::PathBuf;
const DAEMON_WIN_PIPE_NAME: &str = "\\\\.\\pipe\\espansodaemon";
const WORKER_WIN_PIPE_NAME: &str = "\\\\.\\pipe\\espansoworker";
const CLIENT_TIMEOUT: u32 = 2000;
pub struct WindowsIPCServer { pub struct WindowsIPCServer {
service: Service, service: Service,
config: Configs,
event_channel: Sender<Event>, event_channel: Sender<Event>,
} }
fn to_port(config: &Configs, service: &Service) -> u16 { fn get_pipe_name(service: &Service) -> String {
let port = match service { match service {
Service::Daemon => config.ipc_server_port, Service::Daemon => DAEMON_WIN_PIPE_NAME.to_owned(),
Service::Worker => config.worker_ipc_server_port, Service::Worker => WORKER_WIN_PIPE_NAME.to_owned(),
}; }
port as u16
} }
impl WindowsIPCServer { impl WindowsIPCServer {
pub fn new( pub fn new(service: Service, event_channel: Sender<Event>) -> WindowsIPCServer {
service: Service,
config: Configs,
event_channel: Sender<Event>,
) -> WindowsIPCServer {
WindowsIPCServer { WindowsIPCServer {
service, service,
config,
event_channel, event_channel,
} }
} }
@ -57,20 +58,20 @@ impl WindowsIPCServer {
impl super::IPCServer for WindowsIPCServer { impl super::IPCServer for WindowsIPCServer {
fn start(&self) { fn start(&self) {
let event_channel = self.event_channel.clone(); let event_channel = self.event_channel.clone();
let server_port = to_port(&self.config, &self.service); let pipe_name = get_pipe_name(&self.service);
std::thread::Builder::new() std::thread::Builder::new()
.name("ipc_server".to_string()) .name("ipc_server".to_string())
.spawn(move || { .spawn(move || {
let listener = TcpListener::bind(format!("127.0.0.1:{}", server_port)) let options = PipeOptions::new(&pipe_name);
.expect("Error binding to IPC server port");
info!( info!("Binding to named pipe: {}", pipe_name);
"Binded to IPC tcp socket: {}",
listener.local_addr().unwrap().to_string()
);
for stream in listener.incoming() { loop {
process_event(&event_channel, stream); let server = options
.single()
.expect("unable to initialize IPC named pipe");
let pipe_server = server.wait();
process_event(&event_channel, pipe_server);
} }
}) })
.expect("Unable to spawn IPC server thread"); .expect("Unable to spawn IPC server thread");
@ -79,20 +80,19 @@ impl super::IPCServer for WindowsIPCServer {
pub struct WindowsIPCClient { pub struct WindowsIPCClient {
service: Service, service: Service,
config: Configs,
} }
impl WindowsIPCClient { impl WindowsIPCClient {
pub fn new(service: Service, config: Configs) -> WindowsIPCClient { pub fn new(service: Service) -> WindowsIPCClient {
WindowsIPCClient { service, config } WindowsIPCClient { service }
} }
} }
impl super::IPCClient for WindowsIPCClient { impl super::IPCClient for WindowsIPCClient {
fn send_command(&self, command: IPCCommand) -> Result<(), String> { fn send_command(&self, command: IPCCommand) -> Result<(), String> {
let port = to_port(&self.config, &self.service); let pipe_name = get_pipe_name(&self.service);
let stream = TcpStream::connect(("127.0.0.1", port)); let client = PipeClient::connect_ms(pipe_name, CLIENT_TIMEOUT);
send_command(command, stream) send_command(command, client)
} }
} }

View File

@ -106,11 +106,12 @@ impl MacSystemManager {
if let Ok(path) = string { if let Ok(path) = string {
if !path.trim().is_empty() { if !path.trim().is_empty() {
let process = path.trim().to_string(); let process = path.trim().to_string();
let app_name = if let Some(name) = Self::get_app_name_from_path(&process) { let app_name =
name if let Some(name) = Self::get_app_name_from_path(&process) {
} else { name
process.to_owned() } else {
}; process.to_owned()
};
return Some((app_name, process)); return Some((app_name, process));
} }
@ -138,14 +139,15 @@ impl MacSystemManager {
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_get_app_name_from_path() { fn test_get_app_name_from_path() {
let app_name = MacSystemManager::get_app_name_from_path("/Applications/iTerm.app/Contents/MacOS/iTerm2"); let app_name = MacSystemManager::get_app_name_from_path(
"/Applications/iTerm.app/Contents/MacOS/iTerm2",
);
assert_eq!(app_name.unwrap(), "iTerm") assert_eq!(app_name.unwrap(), "iTerm")
} }
@ -161,4 +163,3 @@ mod tests {
assert_eq!(app_name.unwrap(), "SecurityAgent") assert_eq!(app_name.unwrap(), "SecurityAgent")
} }
} }