Create macOS install/uninstall subcommands

This commit is contained in:
Federico Terzi 2019-09-17 00:11:31 +02:00
parent 2af285d01d
commit 8e6af972ad
9 changed files with 203 additions and 8 deletions

View File

@ -104,6 +104,18 @@ extern "C" void register_context_menu_click_callback(ContextMenuClickCallback ca
*/ */
int32_t check_accessibility(); 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 * Return the active NSRunningApplication path
*/ */

View File

@ -251,6 +251,16 @@ int32_t show_context_menu(MenuItem * items, int32_t count) {
// 10.9+ only, see this url for compatibility: // 10.9+ only, see this url for compatibility:
// http://stackoverflow.com/questions/17693408/enable-access-for-assistive-devices-programmatically-on-10-9 // http://stackoverflow.com/questions/17693408/enable-access-for-assistive-devices-programmatically-on-10-9
int32_t check_accessibility() { 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}; NSDictionary* opts = @{(__bridge id)kAXTrustedCheckOptionPrompt: @YES};
return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts); 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]];
}

View File

@ -34,6 +34,8 @@ extern {
// System // System
pub fn check_accessibility() -> i32; 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_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;

View File

@ -49,8 +49,7 @@ pub fn check_dependencies() -> bool {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn check_dependencies() -> bool { pub fn check_dependencies() -> bool {
// TODO: check accessibility // Nothing to do here
true true
} }

View File

@ -46,6 +46,7 @@ fn default_filter_exec() -> String{ "".to_owned() }
fn default_disabled() -> bool{ false } fn default_disabled() -> bool{ false }
fn default_log_level() -> i32 { 0 } fn default_log_level() -> i32 { 0 }
fn default_ipc_server_port() -> i32 { 34982 } fn default_ipc_server_port() -> i32 { 34982 }
fn default_use_system_agent() -> bool { true }
fn default_config_caching_interval() -> i32 { 800 } fn default_config_caching_interval() -> i32 { 800 }
fn default_toggle_interval() -> u32 { 230 } fn default_toggle_interval() -> u32 { 230 }
fn default_backspace_limit() -> i32 { 3 } fn default_backspace_limit() -> i32 { 3 }
@ -75,6 +76,9 @@ pub struct Configs {
#[serde(default = "default_ipc_server_port")] #[serde(default = "default_ipc_server_port")]
pub ipc_server_port: i32, pub ipc_server_port: i32,
#[serde(default = "default_use_system_agent")]
pub use_system_agent: bool,
#[serde(default = "default_config_caching_interval")] #[serde(default = "default_config_caching_interval")]
pub config_caching_interval: i32, pub config_caching_interval: i32,
@ -126,6 +130,8 @@ impl Configs {
validate_field!(result, self.toggle_key, KeyModifier::default()); validate_field!(result, self.toggle_key, KeyModifier::default());
validate_field!(result, self.toggle_interval, default_toggle_interval()); validate_field!(result, self.toggle_interval, default_toggle_interval());
validate_field!(result, self.backspace_limit, default_backspace_limit()); 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 result
} }

View File

@ -37,7 +37,7 @@ impl MacContext {
pub fn new(send_channel: Sender<Event>) -> Box<MacContext> { pub fn new(send_channel: Sender<Event>) -> Box<MacContext> {
// Check accessibility // Check accessibility
unsafe { unsafe {
let res = check_accessibility(); let res = prompt_accessibility();
if res == 0 { if res == 0 {
error!("Accessibility must be enabled to make espanso work on MacOS."); error!("Accessibility must be enabled to make espanso work on MacOS.");

106
src/install.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
// 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.")
}

View File

@ -51,6 +51,7 @@ mod bridge;
mod engine; mod engine;
mod config; mod config;
mod system; mod system;
mod install;
mod context; mod context;
mod matcher; mod matcher;
mod keyboard; mod keyboard;
@ -93,6 +94,10 @@ fn main() {
.about("Tool to detect current window properties, to simplify filters creation.")) .about("Tool to detect current window properties, to simplify filters creation."))
.subcommand(SubCommand::with_name("daemon") .subcommand(SubCommand::with_name("daemon")
.about("Start the daemon without spawning a new process.")) .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") .subcommand(SubCommand::with_name("log")
.about("Print the latest daemon logs.")) .about("Print the latest daemon logs."))
.subcommand(SubCommand::with_name("start") .subcommand(SubCommand::with_name("start")
@ -150,6 +155,16 @@ fn main() {
return; 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") { if let Some(_) = matches.subcommand_matches("log") {
log_main(); log_main();
return; return;
@ -290,11 +305,11 @@ fn start_main(config_set: ConfigSet) {
precheck_guard(); precheck_guard();
detach_daemon(config_set); start_daemon(config_set);
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn detach_daemon(_: ConfigSet) { fn start_daemon(_: ConfigSet) {
unsafe { unsafe {
let res = bridge::windows::start_daemon_process(); let res = bridge::windows::start_daemon_process();
if res < 0 { 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"))] #[cfg(not(target_os = "windows"))]
fn detach_daemon(config_set: ConfigSet) { fn fork_daemon(config_set: ConfigSet) {
unsafe { unsafe {
let pid = libc::fork(); let pid = libc::fork();
if pid < 0 { 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<File> { fn acquire_lock() -> Option<File> {
let espanso_dir = context::get_data_dir(); let espanso_dir = context::get_data_dir();
let lock_file_path = espanso_dir.join("espanso.lock"); let lock_file_path = espanso_dir.join("espanso.lock");

View File

@ -6,9 +6,14 @@
<string>com.federicoterzi.espanso</string> <string>com.federicoterzi.espanso</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/Users/freddy/Documents/espanso</string> <string>{{{espanso_path}}}</string>
<string>daemon</string>
</array> </array>
<key>RunAtLoad</key> <key>RunAtLoad</key>
<true/> <true/>
<key>StandardErrorPath</key>
<string>/tmp/espanso.err</string>
<key>StandardOutPath</key>
<string>/tmp/espanso.out</string>
</dict> </dict>
</plist> </plist>