commit
4878bc58bd
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -371,7 +371,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "espanso"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
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)",
|
||||
|
@ -384,6 +384,7 @@ dependencies = [
|
|||
"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-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)",
|
||||
"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)",
|
||||
|
@ -802,6 +803,14 @@ dependencies = [
|
|||
"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]]
|
||||
name = "native-tls"
|
||||
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-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 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 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"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "espanso"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||
license = "GPL-3.0"
|
||||
description = "Cross-platform Text Expander written in Rust"
|
||||
|
@ -36,6 +36,9 @@ notify = "4.0.13"
|
|||
libc = "0.2.62"
|
||||
signal-hook = "0.1.15"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
named_pipe = "0.4.1"
|
||||
|
||||
[build-dependencies]
|
||||
cmake = "0.1.31"
|
||||
|
||||
|
|
|
@ -77,14 +77,20 @@ xdo_t * xdo_context;
|
|||
|
||||
// Callback invoked when a new key event occur.
|
||||
void event_callback (XPointer, XRecordInterceptData*);
|
||||
int error_callback(Display *display, XErrorEvent *error);
|
||||
|
||||
KeypressCallback keypress_callback;
|
||||
X11ErrorCallback x11_error_callback;
|
||||
void * context_instance;
|
||||
|
||||
void register_keypress_callback(KeypressCallback callback) {
|
||||
keypress_callback = callback;
|
||||
}
|
||||
|
||||
void register_error_callback(X11ErrorCallback callback) {
|
||||
x11_error_callback = callback;
|
||||
}
|
||||
|
||||
int32_t check_x11() {
|
||||
Display *check_disp = XOpenDisplay(NULL);
|
||||
|
||||
|
@ -156,6 +162,9 @@ int32_t initialize(void * _context_instance) {
|
|||
|
||||
xdo_context = xdo_new(NULL);
|
||||
|
||||
// Setup a custom error handler
|
||||
XSetErrorHandler(&error_callback);
|
||||
|
||||
/**
|
||||
* Note: We might never get a MappingNotify event if the
|
||||
* modifier and keymap information was never cached in Xlib.
|
||||
|
@ -272,6 +281,11 @@ void event_callback(XPointer p, XRecordInterceptData *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() {
|
||||
char keys[32];
|
||||
XQueryKeymap(xdo_context->xdpy, keys); // Get the current status of the keyboard
|
||||
|
|
|
@ -49,7 +49,6 @@ extern "C" void cleanup();
|
|||
* 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);
|
||||
|
||||
extern KeypressCallback keypress_callback;
|
||||
|
||||
/*
|
||||
|
@ -57,6 +56,17 @@ extern KeypressCallback keypress_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
|
||||
*/
|
||||
|
|
|
@ -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() {
|
||||
std::vector<INPUT> vec;
|
||||
|
||||
|
|
|
@ -87,6 +87,11 @@ extern "C" void delete_string(int32_t count, int32_t delay);
|
|||
*/
|
||||
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)
|
||||
*/
|
||||
|
|
17
packager.py
17
packager.py
|
@ -82,11 +82,24 @@ def build_windows(package_info):
|
|||
msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*")
|
||||
print("Found Redists: ", msvc_dirs)
|
||||
|
||||
print("Determining best redist...")
|
||||
|
||||
if len(msvc_dirs) == 0:
|
||||
raise Exception("Cannot find redistributable dlls")
|
||||
|
||||
msvc_dir = msvc_dirs[-1] # Take the most recent version of the toolchain
|
||||
print("Using: ",msvc_dir)
|
||||
msvc_dir = None
|
||||
|
||||
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")
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: espanso
|
||||
version: 0.6.2
|
||||
version: 0.6.3
|
||||
summary: A Cross-platform Text Expander written in Rust
|
||||
description: |
|
||||
espanso is a Cross-platform, Text Expander written in Rust.
|
||||
|
|
|
@ -32,6 +32,14 @@ extern "C" {
|
|||
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 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
|
||||
pub fn register_keypress_callback(
|
||||
|
|
|
@ -65,6 +65,7 @@ extern "C" {
|
|||
pub fn send_multi_vkey(vk: i32, count: i32);
|
||||
pub fn delete_string(count: i32, delay: i32);
|
||||
pub fn trigger_paste();
|
||||
pub fn trigger_shift_paste();
|
||||
pub fn trigger_copy();
|
||||
|
||||
// PROCESSES
|
||||
|
|
87
src/cli.rs
Normal file
87
src/cli.rs
Normal 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
|
||||
}
|
|
@ -21,7 +21,7 @@ use crate::bridge::linux::*;
|
|||
use crate::config::Configs;
|
||||
use crate::event::KeyModifier::*;
|
||||
use crate::event::*;
|
||||
use log::{debug, error};
|
||||
use log::{debug, error, warn};
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{c_char, c_void};
|
||||
use std::process::exit;
|
||||
|
@ -59,6 +59,7 @@ impl LinuxContext {
|
|||
let context_ptr = &*context as *const LinuxContext as *const c_void;
|
||||
|
||||
register_keypress_callback(keypress_callback);
|
||||
register_error_callback(error_callback);
|
||||
|
||||
let res = initialize(context_ptr);
|
||||
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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -173,8 +173,7 @@ extern "C" fn keypress_callback(
|
|||
}
|
||||
} else if event_type == 1 {
|
||||
// Modifier event
|
||||
if is_key_down == 1 {
|
||||
// Keyup event
|
||||
if is_key_down == 0 {
|
||||
let modifier: Option<KeyModifier> = match (key_code, variant) {
|
||||
(0x5B, _) => Some(LEFT_META),
|
||||
(0x5C, _) => Some(RIGHT_META),
|
||||
|
|
|
@ -132,22 +132,28 @@ impl<
|
|||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(?P<name>\\w+)\\s*\\}\\}").unwrap();
|
||||
}
|
||||
fn find_match_by_trigger(&self, trigger: &str) -> Option<Match> {
|
||||
let config = self.config_manager.active_config();
|
||||
|
||||
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) {
|
||||
if let Some(m) = config
|
||||
.matches
|
||||
.iter()
|
||||
.find(|m| m.triggers.iter().any(|t| t == trigger))
|
||||
{
|
||||
Some(m.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_match(
|
||||
&self,
|
||||
m: &Match,
|
||||
trailing_separator: Option<char>,
|
||||
trigger_offset: usize,
|
||||
skip_delete: bool,
|
||||
) {
|
||||
let config = self.config_manager.active_config();
|
||||
|
||||
if !config.enable_active {
|
||||
|
@ -163,7 +169,9 @@ impl<
|
|||
m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
|
||||
};
|
||||
|
||||
if !skip_delete {
|
||||
self.keyboard_manager.delete_string(&config, char_count);
|
||||
}
|
||||
|
||||
let mut previous_clipboard_content: Option<String> = None;
|
||||
|
||||
|
@ -287,6 +295,24 @@ impl<
|
|||
// Re-allow espanso to interpret actions
|
||||
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) {
|
||||
let message = if status {
|
||||
|
@ -432,6 +458,15 @@ impl<
|
|||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,6 +130,9 @@ pub enum SystemEvent {
|
|||
|
||||
// Notification
|
||||
NotifyRequest(String),
|
||||
|
||||
// Trigger an expansion from IPC
|
||||
Trigger(String),
|
||||
}
|
||||
|
||||
// Receivers
|
||||
|
|
|
@ -88,7 +88,8 @@ impl super::Extension for ScriptExtension {
|
|||
|
||||
match 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 = error_str.to_string();
|
||||
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 val = value.as_bool();
|
||||
val.unwrap_or(true)
|
||||
}else{
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
|
@ -154,10 +155,7 @@ mod tests {
|
|||
Value::from("args"),
|
||||
Value::from(vec!["echo", "hello world"]),
|
||||
);
|
||||
params.insert(
|
||||
Value::from("trim"),
|
||||
Value::from(false),
|
||||
);
|
||||
params.insert(Value::from("trim"), Value::from(false));
|
||||
|
||||
let extension = ScriptExtension::new();
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
|
|
@ -34,6 +34,7 @@ pub enum Shell {
|
|||
Cmd,
|
||||
Powershell,
|
||||
WSL,
|
||||
WSL2,
|
||||
Bash,
|
||||
Sh,
|
||||
}
|
||||
|
@ -45,27 +46,32 @@ impl Shell {
|
|||
let mut command = Command::new("cmd");
|
||||
command.args(&["/C", &cmd]);
|
||||
command
|
||||
},
|
||||
}
|
||||
Shell::Powershell => {
|
||||
let mut command = Command::new("powershell");
|
||||
command.args(&["-Command", &cmd]);
|
||||
command
|
||||
},
|
||||
}
|
||||
Shell::WSL => {
|
||||
let mut command = Command::new("bash");
|
||||
command.args(&["-c", &cmd]);
|
||||
command
|
||||
}
|
||||
Shell::WSL2 => {
|
||||
let mut command = Command::new("wsl");
|
||||
command.args(&["bash", "-c", &cmd]);
|
||||
command
|
||||
},
|
||||
}
|
||||
Shell::Bash => {
|
||||
let mut command = Command::new("bash");
|
||||
command.args(&["-c", &cmd]);
|
||||
command
|
||||
},
|
||||
}
|
||||
Shell::Sh => {
|
||||
let mut command = Command::new("sh");
|
||||
command.args(&["-c", &cmd]);
|
||||
command
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Inject the $CONFIG variable
|
||||
|
@ -79,6 +85,7 @@ impl Shell {
|
|||
"cmd" => Some(Shell::Cmd),
|
||||
"powershell" => Some(Shell::Powershell),
|
||||
"wsl" => Some(Shell::WSL),
|
||||
"wsl2" => Some(Shell::WSL2),
|
||||
"bash" => Some(Shell::Bash),
|
||||
"sh" => Some(Shell::Sh),
|
||||
_ => None,
|
||||
|
@ -169,7 +176,7 @@ impl super::Extension for ShellExtension {
|
|||
let should_trim = if let Some(value) = trim_opt {
|
||||
let val = value.as_bool();
|
||||
val.unwrap_or(true)
|
||||
}else{
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
|
|
|
@ -51,6 +51,9 @@ impl super::KeyboardManager for WindowsKeyboardManager {
|
|||
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.")
|
||||
}
|
||||
|
|
80
src/main.rs
80
src/main.rs
|
@ -53,6 +53,7 @@ use crate::ui::UIManager;
|
|||
|
||||
mod bridge;
|
||||
mod check;
|
||||
mod cli;
|
||||
mod clipboard;
|
||||
mod config;
|
||||
mod context;
|
||||
|
@ -158,6 +159,39 @@ fn main() {
|
|||
.subcommand(SubCommand::with_name("default")
|
||||
.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
|
||||
.subcommand(SubCommand::with_name("package")
|
||||
.about("Espanso package manager commands")
|
||||
|
@ -274,6 +308,11 @@ fn main() {
|
|||
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("install") {
|
||||
install_main(config_set, matches);
|
||||
|
@ -480,18 +519,14 @@ fn register_signals(_: Configs) {}
|
|||
fn register_signals(config: Configs) {
|
||||
// On Unix, also listen for signals so that we can terminate the
|
||||
// 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");
|
||||
thread::Builder::new()
|
||||
.name("signal monitor".to_string())
|
||||
.spawn(move || {
|
||||
for signal in signals.forever() {
|
||||
info!("Received signal: {:?}, terminating worker", signal);
|
||||
send_command_or_warn(
|
||||
Service::Worker,
|
||||
config,
|
||||
IPCCommand::exit_worker(),
|
||||
);
|
||||
send_command_or_warn(Service::Worker, config, IPCCommand::exit_worker());
|
||||
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
|
||||
|
@ -1027,7 +1062,13 @@ fn install_main(_config_set: ConfigSet, matches: &ArgMatches) {
|
|||
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());
|
||||
|
||||
|
@ -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) {
|
||||
// Determine which is the file to edit
|
||||
let config = matches.value_of("config").unwrap_or("default");
|
||||
|
|
|
@ -67,6 +67,7 @@ impl IPCCommand {
|
|||
"notify" => Some(Event::System(SystemEvent::NotifyRequest(
|
||||
self.payload.clone(),
|
||||
))),
|
||||
"trigger" => Some(Event::System(SystemEvent::Trigger(self.payload.clone()))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -101,6 +102,10 @@ impl IPCCommand {
|
|||
id: "notify".to_owned(),
|
||||
payload: message,
|
||||
}),
|
||||
Event::System(SystemEvent::Trigger(trigger)) => Some(IPCCommand {
|
||||
id: "trigger".to_owned(),
|
||||
payload: trigger,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -125,6 +130,13 @@ impl IPCCommand {
|
|||
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>) {
|
||||
|
@ -200,13 +212,13 @@ pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient {
|
|||
#[cfg(target_os = "windows")]
|
||||
pub fn get_ipc_server(
|
||||
service: Service,
|
||||
config: Configs,
|
||||
_: Configs,
|
||||
event_channel: Sender<Event>,
|
||||
) -> impl IPCServer {
|
||||
windows::WindowsIPCServer::new(service, config, event_channel)
|
||||
windows::WindowsIPCServer::new(service, event_channel)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_ipc_client(service: Service, config: Configs) -> impl IPCClient {
|
||||
windows::WindowsIPCClient::new(service, config)
|
||||
pub fn get_ipc_client(service: Service, _: Configs) -> impl IPCClient {
|
||||
windows::WindowsIPCClient::new(service)
|
||||
}
|
||||
|
|
|
@ -23,32 +23,33 @@ use std::net::{TcpListener, TcpStream};
|
|||
use std::sync::mpsc::Sender;
|
||||
|
||||
use crate::config::Configs;
|
||||
use crate::context;
|
||||
use crate::event::*;
|
||||
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 {
|
||||
service: Service,
|
||||
config: Configs,
|
||||
event_channel: Sender<Event>,
|
||||
}
|
||||
|
||||
fn to_port(config: &Configs, service: &Service) -> u16 {
|
||||
let port = match service {
|
||||
Service::Daemon => config.ipc_server_port,
|
||||
Service::Worker => config.worker_ipc_server_port,
|
||||
};
|
||||
port as u16
|
||||
fn get_pipe_name(service: &Service) -> String {
|
||||
match service {
|
||||
Service::Daemon => DAEMON_WIN_PIPE_NAME.to_owned(),
|
||||
Service::Worker => WORKER_WIN_PIPE_NAME.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsIPCServer {
|
||||
pub fn new(
|
||||
service: Service,
|
||||
config: Configs,
|
||||
event_channel: Sender<Event>,
|
||||
) -> WindowsIPCServer {
|
||||
pub fn new(service: Service, event_channel: Sender<Event>) -> WindowsIPCServer {
|
||||
WindowsIPCServer {
|
||||
service,
|
||||
config,
|
||||
event_channel,
|
||||
}
|
||||
}
|
||||
|
@ -57,20 +58,20 @@ impl WindowsIPCServer {
|
|||
impl super::IPCServer for WindowsIPCServer {
|
||||
fn start(&self) {
|
||||
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()
|
||||
.name("ipc_server".to_string())
|
||||
.spawn(move || {
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", server_port))
|
||||
.expect("Error binding to IPC server port");
|
||||
let options = PipeOptions::new(&pipe_name);
|
||||
|
||||
info!(
|
||||
"Binded to IPC tcp socket: {}",
|
||||
listener.local_addr().unwrap().to_string()
|
||||
);
|
||||
info!("Binding to named pipe: {}", pipe_name);
|
||||
|
||||
for stream in listener.incoming() {
|
||||
process_event(&event_channel, stream);
|
||||
loop {
|
||||
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");
|
||||
|
@ -79,20 +80,19 @@ impl super::IPCServer for WindowsIPCServer {
|
|||
|
||||
pub struct WindowsIPCClient {
|
||||
service: Service,
|
||||
config: Configs,
|
||||
}
|
||||
|
||||
impl WindowsIPCClient {
|
||||
pub fn new(service: Service, config: Configs) -> WindowsIPCClient {
|
||||
WindowsIPCClient { service, config }
|
||||
pub fn new(service: Service) -> WindowsIPCClient {
|
||||
WindowsIPCClient { service }
|
||||
}
|
||||
}
|
||||
|
||||
impl super::IPCClient for WindowsIPCClient {
|
||||
fn send_command(&self, command: IPCCommand) -> Result<(), String> {
|
||||
let port = to_port(&self.config, &self.service);
|
||||
let stream = TcpStream::connect(("127.0.0.1", port));
|
||||
let pipe_name = get_pipe_name(&self.service);
|
||||
let client = PipeClient::connect_ms(pipe_name, CLIENT_TIMEOUT);
|
||||
|
||||
send_command(command, stream)
|
||||
send_command(command, client)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,7 +106,8 @@ impl MacSystemManager {
|
|||
if let Ok(path) = string {
|
||||
if !path.trim().is_empty() {
|
||||
let process = path.trim().to_string();
|
||||
let app_name = if let Some(name) = Self::get_app_name_from_path(&process) {
|
||||
let app_name =
|
||||
if let Some(name) = Self::get_app_name_from_path(&process) {
|
||||
name
|
||||
} else {
|
||||
process.to_owned()
|
||||
|
@ -138,14 +139,15 @@ impl MacSystemManager {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -161,4 +163,3 @@ mod tests {
|
|||
assert_eq!(app_name.unwrap(), "SecurityAgent")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user