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