diff --git a/espanso/src/cli/env_path.rs b/espanso/src/cli/env_path.rs new file mode 100644 index 0000000..c17bb3a --- /dev/null +++ b/espanso/src/cli/env_path.rs @@ -0,0 +1,70 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 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 . + */ + +use crate::exit_code::{ADD_TO_PATH_FAILURE, ADD_TO_PATH_SUCCESS}; + +use super::{CliModule, CliModuleArgs}; +use log::error; + +pub fn new() -> CliModule { + CliModule { + enable_logs: true, + disable_logs_terminal_output: true, + requires_paths: true, + log_mode: super::LogMode::AppendOnly, + subcommand: "env-path".to_string(), + entry: env_path_main, + ..Default::default() + } +} + +fn env_path_main(args: CliModuleArgs) -> i32 { + let paths = args.paths.expect("missing paths argument"); + let cli_args = args.cli_args.expect("missing cli_args"); + + let elevated_priviledge_prompt = cli_args.is_present("prompt"); + + if cli_args.subcommand_matches("register").is_some() { + if let Err(error) = crate::path::add_espanso_to_path(elevated_priviledge_prompt) { + error_print_and_log(&format!( + "Unable to add 'espanso' command to PATH: {}", + error + )); + return ADD_TO_PATH_FAILURE; + } + } else if cli_args.subcommand_matches("unregister").is_some() { + if let Err(error) = crate::path::remove_espanso_from_path(elevated_priviledge_prompt) { + error_print_and_log(&format!( + "Unable to remove 'espanso' command from PATH: {}", + error + )); + return ADD_TO_PATH_FAILURE; + } + } else { + eprintln!("Please specify a subcommand, either `espanso env-path register` to add the 'espanso' command or `espanso env-path unregister` to remove it"); + return ADD_TO_PATH_FAILURE; + } + + ADD_TO_PATH_SUCCESS +} + +fn error_print_and_log(msg: &str) { + error!("{}", msg); + eprintln!("{}", msg); +} diff --git a/espanso/src/cli/launcher/mod.rs b/espanso/src/cli/launcher/mod.rs index 882c342..a440e75 100644 --- a/espanso/src/cli/launcher/mod.rs +++ b/espanso/src/cli/launcher/mod.rs @@ -130,6 +130,8 @@ fn launcher_main(args: CliModuleArgs) -> i32 { preferences.set_completed_wizard(true); } + // TODO: initialize config directory if not present + 0 } diff --git a/espanso/src/cli/migrate.rs b/espanso/src/cli/migrate.rs index 663e32c..ed68779 100644 --- a/espanso/src/cli/migrate.rs +++ b/espanso/src/cli/migrate.rs @@ -17,16 +17,9 @@ * along with espanso. If not, see . */ -use std::{path::PathBuf, sync::Mutex}; +use std::{path::PathBuf}; -use crate::{ - exit_code::{ - MIGRATE_ALREADY_NEW_FORMAT, MIGRATE_CLEAN_FAILURE, MIGRATE_DIRTY_FAILURE, - MIGRATE_LEGACY_INSTANCE_RUNNING, MIGRATE_SUCCESS, MIGRATE_UNEXPECTED_FAILURE, - MIGRATE_USER_ABORTED, - }, - lock::acquire_legacy_lock, -}; +use crate::{exit_code::{MIGRATE_ALREADY_NEW_FORMAT, MIGRATE_CLEAN_FAILURE, MIGRATE_DIRTY_FAILURE, MIGRATE_LEGACY_INSTANCE_RUNNING, MIGRATE_SUCCESS, MIGRATE_USER_ABORTED, configure_custom_panic_hook, update_panic_exit_code}, lock::acquire_legacy_lock}; use super::{CliModule, CliModuleArgs}; use colored::*; @@ -35,10 +28,6 @@ use fs_extra::dir::CopyOptions; use log::{error, info}; use tempdir::TempDir; -lazy_static! { - static ref CURRENT_PANIC_EXIT_CODE: Mutex = Mutex::new(MIGRATE_UNEXPECTED_FAILURE); -} - pub fn new() -> CliModule { CliModule { enable_logs: true, @@ -188,48 +177,6 @@ fn find_available_backup_dir() -> PathBuf { panic!("could not generate valid backup directory"); } -fn configure_custom_panic_hook() { - let previous_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |info| { - (*previous_hook)(info); - - // Part of this code is taken from the "rust-log-panics" crate - let thread = std::thread::current(); - let thread = thread.name().unwrap_or(""); - - let msg = match info.payload().downcast_ref::<&'static str>() { - Some(s) => *s, - None => match info.payload().downcast_ref::() { - Some(s) => &**s, - None => "Box", - }, - }; - - match info.location() { - Some(location) => { - eprintln!( - "ERROR: '{}' panicked at '{}': {}:{}", - thread, - msg, - location.file(), - location.line(), - ); - } - None => eprintln!("ERROR: '{}' panicked at '{}'", thread, msg,), - } - - let exit_code = CURRENT_PANIC_EXIT_CODE.lock().unwrap(); - std::process::exit(*exit_code); - })); -} - -fn update_panic_exit_code(exit_code: i32) { - let mut lock = CURRENT_PANIC_EXIT_CODE - .lock() - .expect("unable to update panic exit code"); - *lock = exit_code; -} - fn error_print_and_log(msg: &str) { error!("{}", msg); eprintln!("{}", msg); diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs index 8b29a34..091ed4f 100644 --- a/espanso/src/cli/mod.rs +++ b/espanso/src/cli/mod.rs @@ -22,6 +22,7 @@ use espanso_config::{config::ConfigStore, matches::store::MatchStore}; use espanso_path::Paths; pub mod daemon; +pub mod env_path; pub mod launcher; pub mod log; pub mod migrate; diff --git a/espanso/src/exit_code.rs b/espanso/src/exit_code.rs index 6294aba..d97b81e 100644 --- a/espanso/src/exit_code.rs +++ b/espanso/src/exit_code.rs @@ -35,4 +35,56 @@ pub const MIGRATE_LEGACY_INSTANCE_RUNNING: i32 = 2; pub const MIGRATE_USER_ABORTED: i32 = 3; pub const MIGRATE_CLEAN_FAILURE: i32 = 50; pub const MIGRATE_DIRTY_FAILURE: i32 = 51; -pub const MIGRATE_UNEXPECTED_FAILURE: i32 = 101; \ No newline at end of file +pub const MIGRATE_UNEXPECTED_FAILURE: i32 = 101; + +pub const ADD_TO_PATH_SUCCESS: i32 = 0; +pub const ADD_TO_PATH_FAILURE: i32 = 1; + +use std::sync::Mutex; + +lazy_static! { + static ref CURRENT_PANIC_EXIT_CODE: Mutex = Mutex::new(MIGRATE_UNEXPECTED_FAILURE); +} + +pub fn configure_custom_panic_hook() { + let previous_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + (*previous_hook)(info); + + // Part of this code is taken from the "rust-log-panics" crate + let thread = std::thread::current(); + let thread = thread.name().unwrap_or(""); + + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match info.payload().downcast_ref::() { + Some(s) => &**s, + None => "Box", + }, + }; + + match info.location() { + Some(location) => { + eprintln!( + "ERROR: '{}' panicked at '{}': {}:{}", + thread, + msg, + location.file(), + location.line(), + ); + } + None => eprintln!("ERROR: '{}' panicked at '{}'", thread, msg,), + } + + let exit_code = CURRENT_PANIC_EXIT_CODE.lock().unwrap(); + std::process::exit(*exit_code); + })); +} + +pub fn update_panic_exit_code(exit_code: i32) { + let mut lock = CURRENT_PANIC_EXIT_CODE + .lock() + .expect("unable to update panic exit code"); + *lock = exit_code; +} + diff --git a/espanso/src/main.rs b/espanso/src/main.rs index efd9ac0..88e0613 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -43,6 +43,7 @@ mod icon; mod ipc; mod lock; mod logging; +mod path; mod preferences; mod util; @@ -58,6 +59,7 @@ lazy_static! { cli::daemon::new(), cli::modulo::new(), cli::migrate::new(), + cli::env_path::new(), ]; } @@ -121,6 +123,25 @@ fn main() { .takes_value(true) .help("Specify a custom path for the espanso runtime directory"), ) + .subcommand( + SubCommand::with_name("env-path") + .arg( + Arg::with_name("prompt") + .long("prompt") + .required(false) + .takes_value(false) + .help("Prompt for permissions if the operation requires elevated privileges."), + ) + .subcommand( + SubCommand::with_name("register") + .about("Add 'espanso' command to PATH"), + ) + .subcommand( + SubCommand::with_name("unregister") + .about("Remove 'espanso' command from PATH"), + ) + .about("Add or remove the 'espanso' command from the PATH (macOS and Windows only)"), + ) // .subcommand(SubCommand::with_name("cmd") // .about("Send a command to the espanso daemon.") // .subcommand(SubCommand::with_name("exit") diff --git a/espanso/src/path/macos.rs b/espanso/src/path/macos.rs new file mode 100644 index 0000000..c8821d0 --- /dev/null +++ b/espanso/src/path/macos.rs @@ -0,0 +1,128 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 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 . + */ + +use std::io::ErrorKind; +use std::path::PathBuf; +use thiserror::Error; + +use anyhow::Result; +use log::{error, warn}; + +pub fn is_espanso_in_path() -> bool { + PathBuf::from("/usr/local/bin/espanso").is_file() +} + +pub fn add_espanso_to_path(prompt_when_necessary: bool) -> Result<()> { + let target_link_dir = PathBuf::from("/usr/local/bin"); + let exec_path = std::env::current_exe()?; + + if !target_link_dir.is_dir() { + return Err(PathError::UsrLocalBinDirDoesNotExist.into()); + } + + let target_link_path = target_link_dir.join("espanso"); + + if let Err(error) = std::os::unix::fs::symlink(&exec_path, &target_link_path) { + match error.kind() { + ErrorKind::PermissionDenied => { + if prompt_when_necessary { + warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript."); + + let params = format!( + r##"do shell script "mkdir -p /usr/local/bin && ln -sf '{}' '{}'" with administrator privileges"##, + exec_path.to_string_lossy(), + target_link_path.to_string_lossy(), + ); + + let mut child = std::process::Command::new("osascript").args(&[ + "-e", + ¶ms, + ]).spawn()?; + + let result = child.wait()?; + if !result.success() { + return Err(PathError::ElevationRequestFailure.into()); + } + } else { + return Err(PathError::SymlinkError(error).into()); + } + } + other_error => { + return Err(PathError::SymlinkError(error).into()); + } + } + } + + Ok(()) +} + +pub fn remove_espanso_from_path(prompt_when_necessary: bool) -> Result<()> { + let target_link_path = PathBuf::from("/usr/local/bin/espanso"); + + if !target_link_path.is_file() { + return Err(PathError::SymlinkNotFound.into()); + } + + if let Err(error) = std::fs::remove_file(&target_link_path) { + match error.kind() { + ErrorKind::PermissionDenied => { + if prompt_when_necessary { + warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript."); + + let params = format!( + r##"do shell script "rm '{}'" with administrator privileges"##, + target_link_path.to_string_lossy(), + ); + + let mut child = std::process::Command::new("osascript").args(&[ + "-e", + ¶ms, + ]).spawn()?; + + let result = child.wait()?; + if !result.success() { + return Err(PathError::ElevationRequestFailure.into()); + } + } else { + return Err(PathError::SymlinkError(error).into()); + } + } + other_error => { + return Err(PathError::SymlinkError(error).into()); + } + } + } + + Ok(()) +} + +#[derive(Error, Debug)] +pub enum PathError { + #[error("/usr/local/bin directory doesn't exist")] + UsrLocalBinDirDoesNotExist, + + #[error("symlink error: `{0}`")] + SymlinkError(std::io::Error), + + #[error("elevation request failed")] + ElevationRequestFailure, + + #[error("symlink does not exist, so there is nothing to remove.")] + SymlinkNotFound, +} diff --git a/espanso/src/path/mod.rs b/espanso/src/path/mod.rs new file mode 100644 index 0000000..3baeb66 --- /dev/null +++ b/espanso/src/path/mod.rs @@ -0,0 +1,30 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 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 . + */ + +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(target_os = "macos")] +pub use macos::add_espanso_to_path; +#[cfg(target_os = "macos")] +pub use macos::remove_espanso_from_path; +#[cfg(target_os = "macos")] +pub use macos::is_espanso_in_path; + +// TODO: add Linux stub \ No newline at end of file