From b87e32e91da9405cc6b07b946cd1433d83b020eb Mon Sep 17 00:00:00 2001 From: Federico Terzi <federico-terzi@users.noreply.github.com> Date: Sun, 14 Jun 2020 18:01:35 +0200 Subject: [PATCH 01/11] Update WSL call to use bash instead of wsl command --- src/extension/shell.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 7ffcffa..09ec1ad 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -34,6 +34,7 @@ pub enum Shell { Cmd, Powershell, WSL, + WSL2, Bash, Sh, } @@ -52,6 +53,11 @@ impl Shell { 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 @@ -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, From adbf1fe432a7104902f53e288e044422f6b982cc Mon Sep 17 00:00:00 2001 From: Federico Terzi <federicoterzi96@gmail.com> Date: Mon, 22 Jun 2020 18:23:23 +0200 Subject: [PATCH 02/11] Version bump 0.6.3 --- Cargo.lock | 2 +- Cargo.toml | 2 +- snapcraft.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e97ecb..81fdede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)", diff --git a/Cargo.toml b/Cargo.toml index 8824c5a..0ed1f99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/snapcraft.yaml b/snapcraft.yaml index 275a752..4d783d1 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -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. From 34c0a524553ca18b55f45933169001037ea67eb7 Mon Sep 17 00:00:00 2001 From: Federico Terzi <federicoterzi96@gmail.com> Date: Mon, 22 Jun 2020 18:31:30 +0200 Subject: [PATCH 03/11] Remove trailing .git from repository url. Fix #326 --- src/main.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 5f394df..90cdbcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1027,7 +1027,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()); From 6766d91af32bb7265ba4362317ed497cf523412a Mon Sep 17 00:00:00 2001 From: Federico Terzi <federicoterzi96@gmail.com> Date: Mon, 22 Jun 2020 19:09:51 +0200 Subject: [PATCH 04/11] Prevent espanso crash when an X11 exception occurs. Fix #312 --- native/liblinuxbridge/bridge.cpp | 14 ++++++++++++++ native/liblinuxbridge/bridge.h | 12 +++++++++++- src/bridge/linux.rs | 3 +++ src/context/linux.rs | 12 +++++++++++- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/native/liblinuxbridge/bridge.cpp b/native/liblinuxbridge/bridge.cpp index 99f13e9..ec31d09 100644 --- a/native/liblinuxbridge/bridge.cpp +++ b/native/liblinuxbridge/bridge.cpp @@ -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 diff --git a/native/liblinuxbridge/bridge.h b/native/liblinuxbridge/bridge.h index 5e2c359..5102d0c 100644 --- a/native/liblinuxbridge/bridge.h +++ b/native/liblinuxbridge/bridge.h @@ -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 */ diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs index ef04d27..d7ceede 100644 --- a/src/bridge/linux.rs +++ b/src/bridge/linux.rs @@ -32,6 +32,9 @@ 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( diff --git a/src/context/linux.rs b/src/context/linux.rs index 6a4ddb1..c1acae9 100644 --- a/src/context/linux.rs +++ b/src/context/linux.rs @@ -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,12 @@ 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); +} From 968ef578c1d4699d501d7987fa34b1ca104f3a9c Mon Sep 17 00:00:00 2001 From: Federico Terzi <federicoterzi96@gmail.com> Date: Mon, 22 Jun 2020 21:21:35 +0200 Subject: [PATCH 05/11] Add CLI option to list matches and exec a trigger. Fix #263 --- src/cli.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++ src/engine.rs | 59 +++++++++++++++++++++++-------- src/event/mod.rs | 3 ++ src/main.rs | 57 ++++++++++++++++++++++++++++++ src/protocol/mod.rs | 14 ++++++++ 5 files changed, 202 insertions(+), 15 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..601b033 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,84 @@ +/* + * 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 serde::Serialize; +use crate::config::ConfigSet; +use crate::matcher::{Match, MatchContentType}; + +pub fn list_matches(config_set: ConfigSet, onlytriggers: 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) => { + println!("{} - {}", trigger, text.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 +} \ No newline at end of file diff --git a/src/engine.rs b/src/engine.rs index f349847..2a4d060 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -132,22 +132,20 @@ 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 +161,9 @@ impl< 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; @@ -287,6 +287,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 +450,17 @@ 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) + }, + } + } } } } diff --git a/src/event/mod.rs b/src/event/mod.rs index 600c5b6..a85f317 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -130,6 +130,9 @@ pub enum SystemEvent { // Notification NotifyRequest(String), + + // Trigger an expansion from IPC + Trigger(String), } // Receivers diff --git a/src/main.rs b/src/main.rs index 90cdbcd..f7b3b18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,6 +69,7 @@ mod render; mod sysdaemon; mod system; mod ui; +mod cli; mod utils; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -158,6 +159,32 @@ 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) + ) + ) + .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 +301,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); @@ -1229,6 +1261,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"); + + if !json { + crate::cli::list_matches(config_set, onlytriggers); + }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"); diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index dae371b..d719dac 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -67,6 +67,9 @@ impl IPCCommand { "notify" => Some(Event::System(SystemEvent::NotifyRequest( self.payload.clone(), ))), + "trigger" => Some(Event::System(SystemEvent::Trigger( + self.payload.clone(), + ))), _ => None, } } @@ -101,6 +104,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 +132,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>) { From 32d7dbc88ed8f27f4a1730c416f0a38a410bf0aa Mon Sep 17 00:00:00 2001 From: Federico Terzi <federicoterzi96@gmail.com> Date: Mon, 22 Jun 2020 21:42:50 +0200 Subject: [PATCH 06/11] Add option to preserve newlines in match list --- src/cli.rs | 9 +++++++-- src/main.rs | 10 +++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 601b033..645d64d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,7 +21,7 @@ use serde::Serialize; use crate::config::ConfigSet; use crate::matcher::{Match, MatchContentType}; -pub fn list_matches(config_set: ConfigSet, onlytriggers: bool) { +pub fn list_matches(config_set: ConfigSet, onlytriggers: bool, preserve_newlines: bool) { let matches = filter_matches(config_set); for m in matches { @@ -31,7 +31,12 @@ pub fn list_matches(config_set: ConfigSet, onlytriggers: bool) { }else { match m.content { MatchContentType::Text(ref text) => { - println!("{} - {}", trigger, text.replace) + let replace = if preserve_newlines { + text.replace.to_owned() + }else{ + text.replace.replace("\n", " ") + }; + println!("{} - {}", trigger, replace) }, MatchContentType::Image(_) => { // Skip image matches for now diff --git a/src/main.rs b/src/main.rs index f7b3b18..3becc92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -177,6 +177,13 @@ fn main() { .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") @@ -1266,9 +1273,10 @@ 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); + crate::cli::list_matches(config_set, onlytriggers, preserve_newlines); }else{ crate::cli::list_matches_as_json(config_set); } From bb2cc41c4d11e64ef516ad99cf3272223608dc2a Mon Sep 17 00:00:00 2001 From: Federico Terzi <federico-terzi@users.noreply.github.com> Date: Wed, 24 Jun 2020 20:32:21 +0200 Subject: [PATCH 07/11] Include VC redist check in CI pipeline. Fix #336 --- packager.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packager.py b/packager.py index 358f1f9..1f2c371 100644 --- a/packager.py +++ b/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") From 889e2b8f8c438d2939273a8570208b7b2130537c Mon Sep 17 00:00:00 2001 From: Federico Terzi <federico-terzi@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:23:03 +0200 Subject: [PATCH 08/11] Refactor Windows IPC to use named pipes instead of localhost --- Cargo.lock | 10 +++++++++ Cargo.toml | 3 +++ src/protocol/mod.rs | 8 +++---- src/protocol/windows.rs | 49 +++++++++++++++++++++++------------------ 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81fdede..9a891de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 0ed1f99..6e49975 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index d719dac..563e560 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -214,13 +214,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) } diff --git a/src/protocol/windows.rs b/src/protocol/windows.rs index 67b2125..9cf239c 100644 --- a/src/protocol/windows.rs +++ b/src/protocol/windows.rs @@ -25,30 +25,35 @@ use std::sync::mpsc::Sender; use crate::config::Configs; use crate::event::*; use crate::protocol::{process_event, send_command, Service}; +use named_pipe::{PipeOptions, PipeServer, PipeClient}; +use crate::context; +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 { WindowsIPCServer { service, - config, event_channel, } } @@ -57,20 +62,21 @@ 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() + "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 +85,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) } } From 0cd245153f097ad9087e59598f4c4e2ebd11890b Mon Sep 17 00:00:00 2001 From: Federico Terzi <federico-terzi@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:44:29 +0200 Subject: [PATCH 09/11] Add Ctrl+Shift+V shortcut on Windows. Fix #333 --- native/libwinbridge/bridge.cpp | 32 ++++++++++++++++++++++++++++++++ native/libwinbridge/bridge.h | 5 +++++ src/bridge/windows.rs | 1 + src/keyboard/windows.rs | 3 +++ 4 files changed, 41 insertions(+) diff --git a/native/libwinbridge/bridge.cpp b/native/libwinbridge/bridge.cpp index febcb60..01024c1 100644 --- a/native/libwinbridge/bridge.cpp +++ b/native/libwinbridge/bridge.cpp @@ -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; diff --git a/native/libwinbridge/bridge.h b/native/libwinbridge/bridge.h index 8b7984b..f0ef9eb 100644 --- a/native/libwinbridge/bridge.h +++ b/native/libwinbridge/bridge.h @@ -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) */ diff --git a/src/bridge/windows.rs b/src/bridge/windows.rs index 2f75254..35cec74 100644 --- a/src/bridge/windows.rs +++ b/src/bridge/windows.rs @@ -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 diff --git a/src/keyboard/windows.rs b/src/keyboard/windows.rs index fce6718..be2cbb2 100644 --- a/src/keyboard/windows.rs +++ b/src/keyboard/windows.rs @@ -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.") } From 958d0669e919b7b588d7812e14b24df718ce8d4a Mon Sep 17 00:00:00 2001 From: Federico Terzi <federico-terzi@users.noreply.github.com> Date: Wed, 24 Jun 2020 22:10:19 +0200 Subject: [PATCH 10/11] Handle modifiers on Release instead of Press events on Windows. Fix #328 --- src/context/windows.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/context/windows.rs b/src/context/windows.rs index ec92b57..07eeffa 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -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), From 45bcaee54be50bb6034d3facec1e08ec727e976d Mon Sep 17 00:00:00 2001 From: Federico Terzi <federico-terzi@users.noreply.github.com> Date: Wed, 24 Jun 2020 22:11:01 +0200 Subject: [PATCH 11/11] Fix formatting --- src/bridge/linux.rs | 7 ++++++- src/cli.rs | 30 ++++++++++++++---------------- src/config/mod.rs | 2 +- src/context/linux.rs | 5 ++++- src/engine.rs | 24 +++++++++++++++--------- src/extension/script.rs | 10 ++++------ src/extension/shell.rs | 14 +++++++------- src/main.rs | 15 +++++---------- src/protocol/mod.rs | 4 +--- src/protocol/windows.rs | 19 +++++++------------ src/system/macos.rs | 17 +++++++++-------- 11 files changed, 73 insertions(+), 74 deletions(-) diff --git a/src/bridge/linux.rs b/src/bridge/linux.rs index d7ceede..74562ea 100644 --- a/src/bridge/linux.rs +++ b/src/bridge/linux.rs @@ -33,7 +33,12 @@ extern "C" { 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), + cb: extern "C" fn( + _self: *mut c_void, + error_code: c_char, + request_code: c_char, + minor_code: c_char, + ), ); // Keyboard diff --git a/src/cli.rs b/src/cli.rs index 645d64d..f2fc91b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -17,9 +17,9 @@ * along with espanso. If not, see <https://www.gnu.org/licenses/>. */ -use serde::Serialize; 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); @@ -28,19 +28,19 @@ pub fn list_matches(config_set: ConfigSet, onlytriggers: bool, preserve_newlines for trigger in m.triggers.iter() { if onlytriggers { println!("{}", trigger); - }else { + } else { match m.content { MatchContentType::Text(ref text) => { let replace = if preserve_newlines { text.replace.to_owned() - }else{ + } else { text.replace.replace("\n", " ") }; println!("{} - {}", trigger, replace) - }, + } MatchContentType::Image(_) => { // Skip image matches for now - }, + } } } } @@ -60,15 +60,13 @@ pub fn list_matches_as_json(config_set: ConfigSet) { for m in matches { match m.content { - MatchContentType::Text(ref text) => { - entries.push(JsonMatchEntry { - triggers: m.triggers, - replace: text.replace.clone(), - }) - }, + MatchContentType::Text(ref text) => entries.push(JsonMatchEntry { + triggers: m.triggers, + replace: text.replace.clone(), + }), MatchContentType::Image(_) => { // Skip image matches for now - }, + } } } @@ -82,8 +80,8 @@ fn filter_matches(config_set: ConfigSet) -> Vec<Match> { 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) -// } + // for specific in config_set.specific { + // output.extend(specific.matches) + // } output -} \ No newline at end of file +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 645e7c7..3e68ea1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -129,7 +129,7 @@ fn default_show_notifications() -> bool { true } fn default_auto_restart() -> bool { - true + true } fn default_show_icon() -> bool { true diff --git a/src/context/linux.rs b/src/context/linux.rs index c1acae9..2d0fe54 100644 --- a/src/context/linux.rs +++ b/src/context/linux.rs @@ -163,5 +163,8 @@ extern "C" fn error_callback( 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); + warn!( + "X11 reported an error code: {}, request_code: {} and minor_code: {}", + error_code, request_code, minor_code + ); } diff --git a/src/engine.rs b/src/engine.rs index 2a4d060..f48c810 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -136,16 +136,24 @@ impl< fn find_match_by_trigger(&self, trigger: &str) -> Option<Match> { let config = self.config_manager.active_config(); - if let Some(m) = config.matches.iter().find(|m| - m.triggers.iter().any(|t| t == trigger) - ) { + if let Some(m) = config + .matches + .iter() + .find(|m| m.triggers.iter().any(|t| t == trigger)) + { Some(m.clone()) - }else{ + } else { None } } - fn inject_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize, skip_delete: bool) { + 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 { @@ -455,10 +463,8 @@ impl< match m { Some(m) => { self.inject_match(&m, None, 0, true); - }, - None => { - warn!("No match found with trigger: {}", trigger) - }, + } + None => warn!("No match found with trigger: {}", trigger), } } } diff --git a/src/extension/script.rs b/src/extension/script.rs index 8d68c46..6144854 100644 --- a/src/extension/script.rs +++ b/src/extension/script.rs @@ -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![]); diff --git a/src/extension/shell.rs b/src/extension/shell.rs index 09ec1ad..f1b9da4 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -46,32 +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 @@ -176,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 }; diff --git a/src/main.rs b/src/main.rs index 3becc92..12f602d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,7 @@ use crate::ui::UIManager; mod bridge; mod check; +mod cli; mod clipboard; mod config; mod context; @@ -69,7 +70,6 @@ mod render; mod sysdaemon; mod system; mod ui; -mod cli; mod utils; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -519,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)); @@ -1268,7 +1264,6 @@ 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"); @@ -1277,10 +1272,10 @@ fn match_main(config_set: ConfigSet, matches: &ArgMatches) { if !json { crate::cli::list_matches(config_set, onlytriggers, preserve_newlines); - }else{ + } else { crate::cli::list_matches_as_json(config_set); } - }else if let Some(matches) = matches.subcommand_matches("exec") { + } else if let Some(matches) = matches.subcommand_matches("exec") { let trigger = matches.value_of("trigger").unwrap_or_else(|| { eprintln!("missing trigger"); exit(1); diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 563e560..a609160 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -67,9 +67,7 @@ impl IPCCommand { "notify" => Some(Event::System(SystemEvent::NotifyRequest( self.payload.clone(), ))), - "trigger" => Some(Event::System(SystemEvent::Trigger( - self.payload.clone(), - ))), + "trigger" => Some(Event::System(SystemEvent::Trigger(self.payload.clone()))), _ => None, } } diff --git a/src/protocol/windows.rs b/src/protocol/windows.rs index 9cf239c..10ff042 100644 --- a/src/protocol/windows.rs +++ b/src/protocol/windows.rs @@ -23,10 +23,10 @@ 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::{PipeOptions, PipeServer, PipeClient}; -use crate::context; +use named_pipe::{PipeClient, PipeOptions, PipeServer}; use std::io::Error; use std::path::PathBuf; @@ -46,12 +46,8 @@ fn get_pipe_name(service: &Service) -> String { } } - impl WindowsIPCServer { - pub fn new( - service: Service, - event_channel: Sender<Event>, - ) -> WindowsIPCServer { + pub fn new(service: Service, event_channel: Sender<Event>) -> WindowsIPCServer { WindowsIPCServer { service, event_channel, @@ -68,13 +64,12 @@ impl super::IPCServer for WindowsIPCServer { .spawn(move || { let options = PipeOptions::new(&pipe_name); - info!( - "Binding to named pipe: {}", - pipe_name - ); + info!("Binding to named pipe: {}", pipe_name); loop { - let server = options.single().expect("unable to initialize IPC named pipe"); + let server = options + .single() + .expect("unable to initialize IPC named pipe"); let pipe_server = server.wait(); process_event(&event_channel, pipe_server); } diff --git a/src/system/macos.rs b/src/system/macos.rs index 584c26e..f6f6ec0 100644 --- a/src/system/macos.rs +++ b/src/system/macos.rs @@ -106,11 +106,12 @@ 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) { - name - } else { - process.to_owned() - }; + let app_name = + if let Some(name) = Self::get_app_name_from_path(&process) { + name + } else { + process.to_owned() + }; return Some((app_name, process)); } @@ -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") } } -