diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h index d982f2c..8f414d9 100644 --- a/native/libmacbridge/bridge.h +++ b/native/libmacbridge/bridge.h @@ -104,6 +104,18 @@ extern "C" void register_context_menu_click_callback(ContextMenuClickCallback ca */ int32_t check_accessibility(); +/* + * Prompt to authorize the accessibility features. + * @return + */ +int32_t prompt_accessibility(); + +/* + * Open Security & Privacy settings panel + * @return + */ +void open_settings_panel(); + /* * Return the active NSRunningApplication path */ diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm index f543ff9..361bfa3 100644 --- a/native/libmacbridge/bridge.mm +++ b/native/libmacbridge/bridge.mm @@ -251,6 +251,16 @@ int32_t show_context_menu(MenuItem * items, int32_t count) { // 10.9+ only, see this url for compatibility: // http://stackoverflow.com/questions/17693408/enable-access-for-assistive-devices-programmatically-on-10-9 int32_t check_accessibility() { + NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @NO}; + return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts); +} + +int32_t prompt_accessibility() { NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @YES}; return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts); } + +void open_settings_panel() { + NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; +} diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs index 08d12ab..830e994 100644 --- a/src/bridge/macos.rs +++ b/src/bridge/macos.rs @@ -34,6 +34,8 @@ extern { // System pub fn check_accessibility() -> i32; + pub fn prompt_accessibility() -> i32; + pub fn open_settings_panel(); 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; diff --git a/src/check.rs b/src/check.rs index aa85a66..34b4f80 100644 --- a/src/check.rs +++ b/src/check.rs @@ -49,8 +49,7 @@ pub fn check_dependencies() -> bool { #[cfg(target_os = "macos")] pub fn check_dependencies() -> bool { - // TODO: check accessibility - + // Nothing to do here true } diff --git a/src/config/mod.rs b/src/config/mod.rs index 9f0bd2d..4b7f686 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -46,6 +46,7 @@ fn default_filter_exec() -> String{ "".to_owned() } fn default_disabled() -> bool{ false } fn default_log_level() -> i32 { 0 } fn default_ipc_server_port() -> i32 { 34982 } +fn default_use_system_agent() -> bool { true } fn default_config_caching_interval() -> i32 { 800 } fn default_toggle_interval() -> u32 { 230 } fn default_backspace_limit() -> i32 { 3 } @@ -75,6 +76,9 @@ pub struct Configs { #[serde(default = "default_ipc_server_port")] pub ipc_server_port: i32, + #[serde(default = "default_use_system_agent")] + pub use_system_agent: bool, + #[serde(default = "default_config_caching_interval")] pub config_caching_interval: i32, @@ -126,6 +130,8 @@ impl Configs { validate_field!(result, self.toggle_key, KeyModifier::default()); validate_field!(result, self.toggle_interval, default_toggle_interval()); validate_field!(result, self.backspace_limit, default_backspace_limit()); + validate_field!(result, self.ipc_server_port, default_ipc_server_port()); + validate_field!(result, self.use_system_agent, default_use_system_agent()); result } diff --git a/src/context/macos.rs b/src/context/macos.rs index 08e47d6..3e6c0ea 100644 --- a/src/context/macos.rs +++ b/src/context/macos.rs @@ -37,7 +37,7 @@ impl MacContext { pub fn new(send_channel: Sender) -> Box { // Check accessibility unsafe { - let res = check_accessibility(); + let res = prompt_accessibility(); if res == 0 { error!("Accessibility must be enabled to make espanso work on MacOS."); diff --git a/src/install.rs b/src/install.rs new file mode 100644 index 0000000..afdfe60 --- /dev/null +++ b/src/install.rs @@ -0,0 +1,106 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019 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 . + */ + +// This functions are used to register/unregister espanso from the system daemon manager. + +use crate::config::ConfigSet; +use std::fs::create_dir_all; +use std::process::{Command, ExitStatus}; + +// INSTALLATION + +#[cfg(target_os = "linux")] +pub fn install(config_set: ConfigSet) { + // TODO +} + +#[cfg(target_os = "macos")] +const MAC_PLIST_CONTENT : &str = include_str!("res/mac/com.federicoterzi.espanso.plist"); +#[cfg(target_os = "macos")] +const MAC_PLIST_FILENAME : &str = "com.federicoterzi.espanso.plist"; + +#[cfg(target_os = "macos")] +pub fn install(config_set: ConfigSet) { + let home_dir = dirs::home_dir().expect("Could not get user home directory"); + let library_dir = home_dir.join("Library"); + let agents_dir = library_dir.join("LaunchAgents"); + + // Make sure agents directory exists + if !agents_dir.exists() { + create_dir_all(agents_dir.clone()).expect("Could not create LaunchAgents directory"); + } + + let plist_file = agents_dir.join(MAC_PLIST_FILENAME); + if !plist_file.exists() { + println!("Creating LaunchAgents entry: {}", plist_file.to_str().unwrap_or_default()); + + let espanso_path = std::env::current_exe().expect("Could not get espanso executable path"); + println!("Entry will point to: {}", espanso_path.to_str().unwrap_or_default()); + + let plist_content = String::from(MAC_PLIST_CONTENT) + .replace("{{{espanso_path}}}", espanso_path.to_str().unwrap_or_default()); + + std::fs::write(plist_file.clone(), plist_content).expect("Unable to write plist file"); + + println!("Entry created correctly!") + } + + println!("Reloading entry..."); + + let res = Command::new("launchctl") + .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()]) + .output(); + + let res = Command::new("launchctl") + .args(&["load", "-w", plist_file.to_str().unwrap_or_default()]) + .status(); + + if let Ok(status) = res { + if status.success() { + println!("Entry loaded correctly!") + } + }else{ + println!("Error loading new entry"); + } +} + +#[cfg(target_os = "macos")] +pub fn uninstall(config_set: ConfigSet) { + let home_dir = dirs::home_dir().expect("Could not get user home directory"); + let library_dir = home_dir.join("Library"); + let agents_dir = library_dir.join("LaunchAgents"); + + let plist_file = agents_dir.join(MAC_PLIST_FILENAME); + if plist_file.exists() { + let _res = Command::new("launchctl") + .args(&["unload", "-w", plist_file.to_str().unwrap_or_default()]) + .output(); + + std::fs::remove_file(&plist_file).expect("Could not remove espanso entry"); + + println!("Entry removed correctly!") + }else{ + println!("espanso is not installed"); + } +} + +#[cfg(target_os = "windows")] +pub fn install(config_set: ConfigSet) { + println!("Windows does not support system daemon integration.") +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 9a6814e..151cc5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,6 +51,7 @@ mod bridge; mod engine; mod config; mod system; +mod install; mod context; mod matcher; mod keyboard; @@ -93,6 +94,10 @@ fn main() { .about("Tool to detect current window properties, to simplify filters creation.")) .subcommand(SubCommand::with_name("daemon") .about("Start the daemon without spawning a new process.")) + .subcommand(SubCommand::with_name("install") + .about("MacOS and Linux only. Register espanso in the system daemon manager.")) + .subcommand(SubCommand::with_name("uninstall") + .about("MacOS and Linux only. Unregister espanso from the system daemon manager.")) .subcommand(SubCommand::with_name("log") .about("Print the latest daemon logs.")) .subcommand(SubCommand::with_name("start") @@ -150,6 +155,16 @@ fn main() { return; } + if let Some(_) = matches.subcommand_matches("install") { + install_main(config_set); + return; + } + + if let Some(_) = matches.subcommand_matches("uninstall") { + uninstall_main(config_set); + return; + } + if let Some(_) = matches.subcommand_matches("log") { log_main(); return; @@ -290,11 +305,11 @@ fn start_main(config_set: ConfigSet) { precheck_guard(); - detach_daemon(config_set); + start_daemon(config_set); } #[cfg(target_os = "windows")] -fn detach_daemon(_: ConfigSet) { +fn start_daemon(_: ConfigSet) { unsafe { let res = bridge::windows::start_daemon_process(); if res < 0 { @@ -303,8 +318,40 @@ fn detach_daemon(_: ConfigSet) { } } +#[cfg(target_os = "macos")] +fn start_daemon(config_set: ConfigSet) { + if config_set.default.use_system_agent { + use std::process::Command; + + let res = Command::new("launchctl") + .args(&["start", "com.federicoterzi.espanso"]) + .status(); + + if let Ok(status) = res { + if status.success() { + println!("Daemon started correctly!") + }else{ + println!("Error starting launchd daemon with status: {}", status); + } + }else{ + println!("Error starting launchd daemon: {}", res.unwrap_err()); + } + }else{ + fork_daemon(config_set); + } +} + +#[cfg(target_os = "linux")] +fn start_daemon(config_set: ConfigSet) { + if config_set.default.use_system_agent { + // TODO: systemd + }else{ + fork_daemon(config_set); + } +} + #[cfg(not(target_os = "windows"))] -fn detach_daemon(config_set: ConfigSet) { +fn fork_daemon(config_set: ConfigSet) { unsafe { let pid = libc::fork(); if pid < 0 { @@ -496,6 +543,14 @@ fn log_main() { } } +fn install_main(config_set: ConfigSet) { + install::install(config_set); +} + +fn uninstall_main(config_set: ConfigSet) { + install::uninstall(config_set); +} + fn acquire_lock() -> Option { let espanso_dir = context::get_data_dir(); let lock_file_path = espanso_dir.join("espanso.lock"); diff --git a/src/res/mac/com.federicoterzi.espanso.plist b/src/res/mac/com.federicoterzi.espanso.plist index 926fdf6..0728b20 100644 --- a/src/res/mac/com.federicoterzi.espanso.plist +++ b/src/res/mac/com.federicoterzi.espanso.plist @@ -6,9 +6,14 @@ com.federicoterzi.espanso ProgramArguments - /Users/freddy/Documents/espanso + {{{espanso_path}}} + daemon RunAtLoad + StandardErrorPath + /tmp/espanso.err + StandardOutPath + /tmp/espanso.out - + \ No newline at end of file