Add secure input notification on macOS

This commit is contained in:
Federico Terzi 2020-04-03 18:22:31 +02:00
parent 2c8c28087d
commit dd8e5c8f9c
18 changed files with 250 additions and 68 deletions

View File

@ -44,6 +44,7 @@ fn print_config() {
println!("cargo:rustc-link-lib=dylib=c++"); println!("cargo:rustc-link-lib=dylib=c++");
println!("cargo:rustc-link-lib=static=macbridge"); println!("cargo:rustc-link-lib=static=macbridge");
println!("cargo:rustc-link-lib=framework=Cocoa"); println!("cargo:rustc-link-lib=framework=Cocoa");
println!("cargo:rustc-link-lib=framework=IOKit");
} }
fn main() fn main()

View File

@ -158,6 +158,15 @@ int32_t set_clipboard(char * text);
*/ */
int32_t set_clipboard_image(char * path); int32_t set_clipboard_image(char * path);
/*
* If a process is currently holding SecureInput, then return 1 and set the pid pointer to the corresponding PID.
*/
int32_t get_secure_input_process(int64_t *pid);
/*
* Find the executable path corresponding to the given PID, return 0 if no process was found.
*/
int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size);
}; };
#endif //ESPANSO_BRIDGE_H #endif //ESPANSO_BRIDGE_H

View File

@ -20,9 +20,11 @@
#include "bridge.h" #include "bridge.h"
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#include <IOKit/IOKitLib.h>
#include "AppDelegate.h" #include "AppDelegate.h"
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <libproc.h>
extern "C" { extern "C" {
} }
@ -334,3 +336,47 @@ void open_settings_panel() {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]];
} }
// Taken (with a few modifications) from the MagicKeys project: https://github.com/zsszatmari/MagicKeys
int32_t get_secure_input_process(int64_t *pid) {
NSArray *consoleUsersArray;
io_service_t rootService;
int32_t result = 0;
if ((rootService = IORegistryGetRootEntry(kIOMasterPortDefault)) != 0)
{
if ((consoleUsersArray = (NSArray *)IORegistryEntryCreateCFProperty((io_registry_entry_t)rootService, CFSTR("IOConsoleUsers"), kCFAllocatorDefault, 0)) != nil)
{
if ([consoleUsersArray isKindOfClass:[NSArray class]]) // Be careful - ensure this really is an array
{
for (NSDictionary *consoleUserDict in consoleUsersArray) {
NSNumber *secureInputPID;
if ((secureInputPID = [consoleUserDict objectForKey:@"kCGSSessionSecureInputPID"]) != nil)
{
if ([secureInputPID isKindOfClass:[NSNumber class]])
{
*pid = ((UInt64) [secureInputPID intValue]);
result = 1;
break;
}
}
}
}
CFRelease((CFTypeRef)consoleUsersArray);
}
IOObjectRelease((io_object_t) rootService);
}
return result;
}
int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size) {
int res = proc_pidpath((pid_t) pid, buff, buff_size);
if ( res <= 0 ) {
return 0;
} else {
return 1;
}
}

View File

@ -39,6 +39,8 @@ extern {
pub fn open_settings_panel(); pub fn open_settings_panel();
pub fn get_active_app_bundle(buffer: *mut c_char, size: i32) -> i32; pub fn get_active_app_bundle(buffer: *mut c_char, size: i32) -> i32;
pub fn get_active_app_identifier(buffer: *mut c_char, size: i32) -> i32; pub fn get_active_app_identifier(buffer: *mut c_char, size: i32) -> i32;
pub fn get_secure_input_process(pid:*mut i64) -> i32;
pub fn get_path_from_pid(pid:i64, buffer: *mut c_char, size: i32) -> i32;
// Clipboard // Clipboard
pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32; pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32;

View File

@ -64,6 +64,9 @@ fn default_enable_active() -> bool { true }
fn default_backspace_limit() -> i32 { 3 } fn default_backspace_limit() -> i32 { 3 }
fn default_restore_clipboard_delay() -> i32 { 300 } fn default_restore_clipboard_delay() -> i32 { 300 }
fn default_exclude_default_entries() -> bool {false} fn default_exclude_default_entries() -> bool {false}
fn default_secure_input_watcher_enabled() -> bool {true}
fn default_secure_input_notification() -> bool {true}
fn default_secure_input_watcher_interval() -> i32 {5000}
fn default_matches() -> Vec<Match> { Vec::new() } fn default_matches() -> Vec<Match> { Vec::new() }
fn default_global_vars() -> Vec<MatchVariable> { Vec::new() } fn default_global_vars() -> Vec<MatchVariable> { Vec::new() }
@ -138,6 +141,15 @@ pub struct Configs {
#[serde(default = "default_restore_clipboard_delay")] #[serde(default = "default_restore_clipboard_delay")]
pub restore_clipboard_delay: i32, pub restore_clipboard_delay: i32,
#[serde(default = "default_secure_input_watcher_enabled")]
pub secure_input_watcher_enabled: bool,
#[serde(default = "default_secure_input_watcher_interval")]
pub secure_input_watcher_interval: i32,
#[serde(default = "default_secure_input_notification")]
pub secure_input_notification: bool,
#[serde(default)] #[serde(default)]
pub backend: BackendType, pub backend: BackendType,
@ -190,6 +202,9 @@ impl Configs {
validate_field!(result, self.passive_arg_escape, default_passive_arg_escape()); validate_field!(result, self.passive_arg_escape, default_passive_arg_escape());
validate_field!(result, self.passive_key, default_passive_key()); validate_field!(result, self.passive_key, default_passive_key());
validate_field!(result, self.restore_clipboard_delay, default_restore_clipboard_delay()); validate_field!(result, self.restore_clipboard_delay, default_restore_clipboard_delay());
validate_field!(result, self.secure_input_watcher_enabled, default_secure_input_watcher_enabled());
validate_field!(result, self.secure_input_watcher_interval, default_secure_input_watcher_interval());
validate_field!(result, self.secure_input_notification, default_secure_input_notification());
result result
} }

View File

@ -29,6 +29,7 @@ use std::{thread, time};
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering::Acquire; use std::sync::atomic::Ordering::Acquire;
use crate::config::Configs;
#[repr(C)] #[repr(C)]
pub struct LinuxContext { pub struct LinuxContext {
@ -37,7 +38,7 @@ pub struct LinuxContext {
} }
impl LinuxContext { impl LinuxContext {
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<LinuxContext> { pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<LinuxContext> {
// Check if the X11 context is available // Check if the X11 context is available
let x11_available = unsafe { let x11_available = unsafe {
check_x11() check_x11()

View File

@ -20,25 +20,30 @@
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::os::raw::{c_void, c_char}; use std::os::raw::{c_void, c_char};
use crate::bridge::macos::*; use crate::bridge::macos::*;
use crate::event::{Event, KeyEvent, KeyModifier, ActionType}; use crate::event::{Event, KeyEvent, KeyModifier, ActionType, SystemEvent};
use crate::event::KeyModifier::*; use crate::event::KeyModifier::*;
use std::ffi::{CString, CStr}; use std::ffi::{CString, CStr};
use std::fs; use std::{fs, thread};
use log::{info, error, debug}; use log::{info, error, debug};
use std::process::exit; use std::process::exit;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering::Acquire; use std::sync::atomic::Ordering::Acquire;
use crate::config::Configs;
use std::cell::RefCell;
use crate::system::macos::MacSystemManager;
const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png"); const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png");
pub struct MacContext { pub struct MacContext {
pub send_channel: Sender<Event>, pub send_channel: Sender<Event>,
is_injecting: Arc<AtomicBool>, is_injecting: Arc<AtomicBool>,
secure_input_watcher_enabled: bool,
secure_input_watcher_interval: i32,
} }
impl MacContext { impl MacContext {
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<MacContext> { pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<MacContext> {
// Check accessibility // Check accessibility
unsafe { unsafe {
let res = prompt_accessibility(); let res = prompt_accessibility();
@ -53,7 +58,9 @@ impl MacContext {
let context = Box::new(MacContext { let context = Box::new(MacContext {
send_channel, send_channel,
is_injecting is_injecting,
secure_input_watcher_enabled: config.secure_input_watcher_enabled,
secure_input_watcher_interval: config.secure_input_watcher_interval,
}); });
// Initialize the status icon path // Initialize the status icon path
@ -81,10 +88,59 @@ impl MacContext {
context context
} }
fn start_secure_input_watcher(&self) {
let send_channel = self.send_channel.clone();
let secure_input_watcher_interval = self.secure_input_watcher_interval as u64;
let secure_input_watcher = thread::Builder::new().name("secure_input_watcher".to_string()).spawn(move || {
let mut last_secure_input_pid: Option<i64> = None;
loop {
let pid = MacSystemManager::get_secure_input_pid();
if let Some(pid) = pid { // Some application is currently on SecureInput
let should_notify = if let Some(old_pid) = last_secure_input_pid { // We already detected a SecureInput app
if old_pid != pid { // The old app is different from the current one, we should take action
true
}else{ // We already notified this application before
false
}
}else{ // First time we see this SecureInput app, we should take action
true
};
if should_notify {
let secure_input_app = crate::system::macos::MacSystemManager::get_secure_input_application();
if let Some((app_name, path)) = secure_input_app {
let event = Event::System(SystemEvent::SecureInputEnabled(app_name, path));
send_channel.send(event);
}
}
last_secure_input_pid = Some(pid);
}else{ // No app is currently keeping SecureInput
if let Some(old_pid) = last_secure_input_pid { // If there was an app with SecureInput, notify that is now free
let event = Event::System(SystemEvent::SecureInputDisabled);
send_channel.send(event);
}
last_secure_input_pid = None
}
thread::sleep(std::time::Duration::from_millis(secure_input_watcher_interval));
}
});
}
} }
impl super::Context for MacContext { impl super::Context for MacContext {
fn eventloop(&self) { fn eventloop(&self) {
// Start the SecureInput watcher thread
if self.secure_input_watcher_enabled {
self.start_secure_input_watcher();
}
unsafe { unsafe {
eventloop(); eventloop();
} }

View File

@ -32,6 +32,7 @@ use std::path::PathBuf;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::sync::{Once, Arc}; use std::sync::{Once, Arc};
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use crate::config::Configs;
pub trait Context { pub trait Context {
fn eventloop(&self); fn eventloop(&self);
@ -39,20 +40,20 @@ pub trait Context {
// MAC IMPLEMENTATION // MAC IMPLEMENTATION
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> { pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
macos::MacContext::new(send_channel, is_injecting) macos::MacContext::new(config, send_channel, is_injecting)
} }
// LINUX IMPLEMENTATION // LINUX IMPLEMENTATION
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> { pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
linux::LinuxContext::new(send_channel, is_injecting) linux::LinuxContext::new(config, send_channel, is_injecting)
} }
// WINDOWS IMPLEMENTATION // WINDOWS IMPLEMENTATION
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> { pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<dyn Context> {
windows::WindowsContext::new(send_channel, is_injecting) windows::WindowsContext::new(config, send_channel, is_injecting)
} }
// espanso directories // espanso directories

View File

@ -28,6 +28,7 @@ use log::{info, error, debug};
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering::Acquire; use std::sync::atomic::Ordering::Acquire;
use crate::config::Configs;
const BMP_BINARY : &[u8] = include_bytes!("../res/win/espanso.bmp"); const BMP_BINARY : &[u8] = include_bytes!("../res/win/espanso.bmp");
const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico"); const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico");
@ -38,7 +39,7 @@ pub struct WindowsContext {
} }
impl WindowsContext { impl WindowsContext {
pub fn new(send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<WindowsContext> { pub fn new(config: Configs, send_channel: Sender<Event>, is_injecting: Arc<AtomicBool>) -> Box<WindowsContext> {
// Initialize image resources // Initialize image resources
let espanso_dir = super::get_data_dir(); let espanso_dir = super::get_data_dir();

View File

@ -24,7 +24,7 @@ use crate::config::BackendType;
use crate::clipboard::ClipboardManager; use crate::clipboard::ClipboardManager;
use log::{info, warn, debug, error}; use log::{info, warn, debug, error};
use crate::ui::{UIManager, MenuItem, MenuItemType}; use crate::ui::{UIManager, MenuItem, MenuItemType};
use crate::event::{ActionEventReceiver, ActionType}; use crate::event::{ActionEventReceiver, ActionType, SystemEventReceiver, SystemEvent};
use crate::extension::Extension; use crate::extension::Extension;
use crate::render::{Renderer, RenderResult}; use crate::render::{Renderer, RenderResult};
use std::cell::RefCell; use std::cell::RefCell;
@ -333,4 +333,24 @@ impl <'a, S: KeyboardManager, C: ClipboardManager,
_ => {} _ => {}
} }
} }
}
impl <'a, S: KeyboardManager, C: ClipboardManager,
M: ConfigManager<'a>, U: UIManager, R: Renderer> SystemEventReceiver for Engine<'a, S, C, M, U, R>{
fn on_system_event(&self, e: SystemEvent) {
match e {
// MacOS specific
SystemEvent::SecureInputEnabled(app_name, path) => {
info!("SecureInput has been acquired by {}, preventing espanso from working correctly. Full path: {}", app_name, path);
if self.config_manager.default_config().secure_input_notification {
self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000);
}
},
SystemEvent::SecureInputDisabled => {
info!("SecureInput has been disabled.");
},
}
}
} }

View File

@ -17,7 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::event::{KeyEventReceiver, ActionEventReceiver, Event}; use crate::event::{KeyEventReceiver, ActionEventReceiver, Event, SystemEventReceiver};
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
pub trait EventManager { pub trait EventManager {
@ -28,15 +28,18 @@ pub struct DefaultEventManager<'a> {
receive_channel: Receiver<Event>, receive_channel: Receiver<Event>,
key_receivers: Vec<&'a dyn KeyEventReceiver>, key_receivers: Vec<&'a dyn KeyEventReceiver>,
action_receivers: Vec<&'a dyn ActionEventReceiver>, action_receivers: Vec<&'a dyn ActionEventReceiver>,
system_receivers: Vec<&'a dyn SystemEventReceiver>,
} }
impl<'a> DefaultEventManager<'a> { impl<'a> DefaultEventManager<'a> {
pub fn new(receive_channel: Receiver<Event>, key_receivers: Vec<&'a dyn KeyEventReceiver>, pub fn new(receive_channel: Receiver<Event>, key_receivers: Vec<&'a dyn KeyEventReceiver>,
action_receivers: Vec<&'a dyn ActionEventReceiver>) -> DefaultEventManager<'a> { action_receivers: Vec<&'a dyn ActionEventReceiver>,
system_receivers: Vec<&'a dyn SystemEventReceiver>) -> DefaultEventManager<'a> {
DefaultEventManager { DefaultEventManager {
receive_channel, receive_channel,
key_receivers, key_receivers,
action_receivers, action_receivers,
system_receivers
} }
} }
} }
@ -53,6 +56,9 @@ impl <'a> EventManager for DefaultEventManager<'a> {
Event::Action(action_event) => { Event::Action(action_event) => {
self.action_receivers.iter().for_each(|&receiver| receiver.on_action_event(action_event.clone())); self.action_receivers.iter().for_each(|&receiver| receiver.on_action_event(action_event.clone()));
} }
Event::System(system_event) => {
self.system_receivers.iter().for_each(move |&receiver| receiver.on_system_event(system_event.clone()));
}
} }
}, },
Err(e) => panic!("Broken event channel {}", e), Err(e) => panic!("Broken event channel {}", e),

View File

@ -24,7 +24,8 @@ use serde::{Serialize, Deserialize};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Event { pub enum Event {
Action(ActionType), Action(ActionType),
Key(KeyEvent) Key(KeyEvent),
System(SystemEvent),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -132,6 +133,13 @@ impl KeyModifier {
} }
} }
#[derive(Debug, Clone)]
pub enum SystemEvent {
// MacOS specific
SecureInputEnabled(String, String), // AppName, App Path
SecureInputDisabled,
}
// Receivers // Receivers
pub trait KeyEventReceiver { pub trait KeyEventReceiver {
@ -142,6 +150,10 @@ pub trait ActionEventReceiver {
fn on_action_event(&self, e: ActionType); fn on_action_event(&self, e: ActionType);
} }
pub trait SystemEventReceiver {
fn on_system_event(&self, e: SystemEvent);
}
// TESTS // TESTS
#[cfg(test)] #[cfg(test)]

View File

@ -338,7 +338,7 @@ fn daemon_main(config_set: ConfigSet) {
// we could reinterpret the characters we are injecting // we could reinterpret the characters we are injecting
let is_injecting = Arc::new(std::sync::atomic::AtomicBool::new(false)); let is_injecting = Arc::new(std::sync::atomic::AtomicBool::new(false));
let context = context::new(send_channel.clone(), is_injecting.clone()); let context = context::new(config_set.default.clone(), send_channel.clone(), is_injecting.clone());
let config_set_copy = config_set.clone(); let config_set_copy = config_set.clone();
thread::Builder::new().name("daemon_background".to_string()).spawn(move || { thread::Builder::new().name("daemon_background".to_string()).spawn(move || {
@ -382,6 +382,7 @@ fn daemon_background(receive_channel: Receiver<Event>, config_set: ConfigSet, is
receive_channel, receive_channel,
vec!(&matcher), vec!(&matcher),
vec!(&engine, &matcher), vec!(&engine, &matcher),
vec!(&engine),
); );
info!("espanso is running!"); info!("espanso is running!");

View File

@ -20,7 +20,7 @@
use std::os::raw::c_char; use std::os::raw::c_char;
use std::ffi::CStr; use std::ffi::CStr;
use crate::bridge::macos::{get_active_app_bundle, get_active_app_identifier}; use crate::bridge::macos::{get_active_app_bundle, get_active_app_identifier, get_secure_input_process, get_path_from_pid};
pub struct MacSystemManager { pub struct MacSystemManager {
@ -75,70 +75,65 @@ impl MacSystemManager {
} }
} }
/// Check whether an application is currently holding the Secure Input.
/// Return None if no application has claimed SecureInput, its PID otherwise.
pub fn get_secure_input_pid() -> Option<i64> {
unsafe {
let mut pid: i64 = -1;
let res = get_secure_input_process(&mut pid as *mut i64);
if res > 0{
Some(pid)
}else{
None
}
}
}
/// Check whether an application is currently holding the Secure Input. /// Check whether an application is currently holding the Secure Input.
/// Return None if no application has claimed SecureInput, Some((AppName, AppPath)) otherwise. /// Return None if no application has claimed SecureInput, Some((AppName, AppPath)) otherwise.
pub fn get_secure_input_application() -> Option<(String, String)> { pub fn get_secure_input_application() -> Option<(String, String)> {
use std::process::Command;
use regex::Regex; use regex::Regex;
let output = Command::new("ioreg")
.arg("-d")
.arg("1")
.arg("-k")
.arg("IOConsoleUsers")
.arg("-w")
.arg("0")
.output();
lazy_static! {
static ref PID_REGEX: Regex = Regex::new("\"kCGSSessionSecureInputPID\"=(\\d+)").unwrap();
};
lazy_static! { lazy_static! {
static ref APP_REGEX: Regex = Regex::new("/([^/]+).app/").unwrap(); static ref APP_REGEX: Regex = Regex::new("/([^/]+).app/").unwrap();
}; };
if let Ok(output) = output { unsafe {
let output_str = String::from_utf8_lossy(output.stdout.as_slice()); let pid = MacSystemManager::get_secure_input_pid();
let caps = PID_REGEX.captures(&output_str);
if let Some(caps) = caps { if let Some(pid) = pid {
// Get the PID of the process that is handling SecureInput // Size of the buffer is ruled by the PROC_PIDPATHINFO_MAXSIZE constant.
let pid_str = caps.get(1).map_or("", |m| m.as_str()); // the underlying proc_pidpath REQUIRES a buffer of that dimension, otherwise it fail silently.
let pid = pid_str.parse::<i32>().expect("Invalid pid value"); let mut buffer : [c_char; 4096] = [0; 4096];
let res = get_path_from_pid(pid, buffer.as_mut_ptr(), buffer.len() as i32);
// Find the process that is handling the SecureInput if res > 0 {
let output = Command::new("ps") let c_string = CStr::from_ptr(buffer.as_ptr());
.arg("-p") let string = c_string.to_str();
.arg(pid.to_string()) if let Ok(path) = string {
.arg("-o") if !path.trim().is_empty() {
.arg("command=") let process = path.trim().to_string();
.output(); let caps = APP_REGEX.captures(&process);
let app_name = if let Some(caps) = caps {
caps.get(1).map_or("", |m| m.as_str()).to_owned()
}else{
process.to_owned()
};
if let Ok(output) = output { Some((app_name, process))
let output_str = String::from_utf8_lossy(output.stdout.as_slice());
if !output_str.trim().is_empty() {
let process = output_str.trim().to_string();
let caps = APP_REGEX.captures(&process);
let app_name = if let Some(caps) = caps {
caps.get(1).map_or("", |m| m.as_str()).to_owned()
}else{ }else{
process.to_owned() None
}; }
Some((app_name, process))
}else{ }else{
None None
} }
}else{ // Can't obtain process name }else{
None None
} }
}else{ // No process is holding SecureInput }else{
None None
} }
}else{ // Can't execute the query to the IOKit registry
None
} }
} }
} }

View File

@ -30,10 +30,14 @@ pub struct LinuxUIManager {
impl super::UIManager for LinuxUIManager { impl super::UIManager for LinuxUIManager {
fn notify(&self, message: &str) { fn notify(&self, message: &str) {
self.notify_delay(message, 2000);
}
fn notify_delay(&self, message: &str, duration: i32) {
let res = Command::new("notify-send") let res = Command::new("notify-send")
.args(&["-i", self.icon_path.to_str().unwrap_or_default(), .args(&["-i", self.icon_path.to_str().unwrap_or_default(),
"-t", "2000", "espanso", message]) "-t", &duration.to_string(), "espanso", message])
.output(); .output();
if let Err(e) = res { if let Err(e) = res {
error!("Could not send a notification, error: {}", e); error!("Could not send a notification, error: {}", e);

View File

@ -29,7 +29,6 @@ use std::os::raw::c_char;
use crate::context; use crate::context;
const NOTIFY_HELPER_BINARY : &'static [u8] = include_bytes!("../res/mac/EspansoNotifyHelper.zip"); const NOTIFY_HELPER_BINARY : &'static [u8] = include_bytes!("../res/mac/EspansoNotifyHelper.zip");
const DEFAULT_NOTIFICATION_DELAY : f64 = 1.5;
pub struct MacUIManager { pub struct MacUIManager {
notify_helper_path: PathBuf notify_helper_path: PathBuf
@ -37,12 +36,18 @@ pub struct MacUIManager {
impl super::UIManager for MacUIManager { impl super::UIManager for MacUIManager {
fn notify(&self, message: &str) { fn notify(&self, message: &str) {
self.notify_delay(message, 1500);
}
fn notify_delay(&self, message: &str, duration: i32) {
let executable_path = self.notify_helper_path.join("Contents"); let executable_path = self.notify_helper_path.join("Contents");
let executable_path = executable_path.join("MacOS"); let executable_path = executable_path.join("MacOS");
let executable_path = executable_path.join("EspansoNotifyHelper"); let executable_path = executable_path.join("EspansoNotifyHelper");
let duration_float = duration as f64 / 1000.0;
let res = Command::new(executable_path) let res = Command::new(executable_path)
.args(&["espanso", message, &DEFAULT_NOTIFICATION_DELAY.to_string()]) .args(&["espanso", message, &duration_float.to_string()])
.spawn(); .spawn();
if let Err(e) = res { if let Err(e) = res {

View File

@ -28,6 +28,7 @@ mod macos;
pub trait UIManager { pub trait UIManager {
fn notify(&self, message: &str); fn notify(&self, message: &str);
fn notify_delay(&self, message: &str, duration: i32);
fn show_menu(&self, menu: Vec<MenuItem>); fn show_menu(&self, menu: Vec<MenuItem>);
fn cleanup(&self); fn cleanup(&self);
} }

View File

@ -31,17 +31,23 @@ pub struct WindowsUIManager {
impl super::UIManager for WindowsUIManager { impl super::UIManager for WindowsUIManager {
fn notify(&self, message: &str) { fn notify(&self, message: &str) {
self.notify_delay(message, 2000);
}
fn notify_delay(&self, message: &str, duration: i32) {
let current_id: i32 = { let current_id: i32 = {
let mut id = self.id.lock().unwrap(); let mut id = self.id.lock().unwrap();
*id += 1; *id += 1;
*id *id
}; };
let step = duration / 10;
// Setup a timeout to close the notification // Setup a timeout to close the notification
let id = Arc::clone(&self.id); let id = Arc::clone(&self.id);
let _ = thread::Builder::new().name("notification_thread".to_string()).spawn(move || { let _ = thread::Builder::new().name("notification_thread".to_string()).spawn(move || {
for _ in 1..10 { for _ in 1..10 {
let duration = time::Duration::from_millis(200); let duration = time::Duration::from_millis(step as u64);
thread::sleep(duration); thread::sleep(duration);
let new_id = id.lock().unwrap(); let new_id = id.lock().unwrap();