diff --git a/espanso/src/cli/edit.rs b/espanso/src/cli/edit.rs new file mode 100644 index 0000000..3050508 --- /dev/null +++ b/espanso/src/cli/edit.rs @@ -0,0 +1,230 @@ +/* + * 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::path::{Path, PathBuf}; +use std::process::Command; + +use super::{CliModule, CliModuleArgs}; + +#[cfg(target_os = "linux")] +fn default_editor() -> String { + "/bin/nano".to_owned() +} +#[cfg(target_os = "macos")] +fn default_editor() -> String { + "/usr/bin/nano".to_owned() +} +#[cfg(target_os = "windows")] +fn default_editor() -> String { + "C:\\Windows\\System32\\notepad.exe".to_owned() +} + +pub fn new() -> CliModule { + CliModule { + requires_paths: true, + subcommand: "edit".to_string(), + entry: edit_main, + ..Default::default() + } +} + +fn edit_main(args: CliModuleArgs) -> i32 { + let paths = args.paths.expect("missing paths argument"); + let cli_args = args.cli_args.expect("missing cli_args"); + + if !paths.config.is_dir() { + panic!( + "config directory does not exist in path: {:?}", + paths.config + ); + } + + // Determine which is the file to edit + let target_file = cli_args.value_of("target_file"); + let target_path = determine_target_path(&paths.config, target_file); + + println!( + "Editing file: {}", + &target_path.to_string_lossy().to_string() + ); + + open_editor(&target_path); + + // The previous version automatically reloaded the config after saving. + // Given that the new version automatically reloads config changes, this could be avoided. + // Nevertheless, this assumption might be wrong, so I'm keeping the necessary code. + // TODO: evaluate if reload is needed after v2 becomes stable + + // // Based on the fact that the file already exists or not, we should detect in different + // // ways if a reload is needed + // let should_reload = if target_path.exists() { + // // Get the last modified date, so that we can detect if the user actually edits the file + // // before reloading + // let metadata = std::fs::metadata(&target_path).expect("cannot gather file metadata"); + // let last_modified = metadata + // .modified() + // .expect("cannot read file last modified date"); + + // let result = open_editor(&target_path); + // if result { + // let new_metadata = std::fs::metadata(&target_path).expect("cannot gather file metadata"); + // let new_last_modified = new_metadata + // .modified() + // .expect("cannot read file last modified date"); + + // if last_modified != new_last_modified { + // println!("File has been modified, reloading configuration"); + // true + // } else { + // println!("File has not been modified, avoiding reload"); + // false + // } + // } else { + // false + // } + // } else { + // let result = open_editor(&target_path); + // if result { + // // If the file has been created, we should reload the espanso config + // if target_path.exists() { + // println!("A new file has been created, reloading configuration"); + // true + // } else { + // println!("No file has been created, avoiding reload"); + // false + // } + // } else { + // false + // } + // }; + + // let no_restart: bool = if cli_args.is_present("norestart") { + // println!("Skipping automatic restart"); + // true + // } else { + // false + // }; + + // if should_reload && !no_restart { + // // Check if the new configuration is valid + + // if let Err(err) = crate::config::load_config(&paths.config, &paths.packages) { + // eprintln!("Unable to reload espanso due to a configuration error:"); + // eprintln!("{:?}", err); + // return 1; + // }; + + // restart_espanso(&paths_overrides).expect("unable to restart espanso"); + // } + + 0 +} + +fn determine_target_path(config_path: &Path, target_file: Option<&str>) -> PathBuf { + if let Some(target_file) = target_file { + match target_file { + "default" => { + if espanso_config::is_legacy_config(config_path) { + config_path.join("default.yml") + } else { + config_path.join("config").join("default.yml") + } + } + "base" => { + if espanso_config::is_legacy_config(config_path) { + panic!("'base' alias cannot be used in compatibility mode, please migrate your configuration by running 'espanso migrate'") + } else { + config_path.join("match").join("base.yml") + } + } + custom => { + if !custom.ends_with(".yml") && !custom.ends_with(".yaml") { + if espanso_config::is_legacy_config(config_path) { + config_path.join("user").join(format!("{}.yml", custom)) + } else { + config_path.join("match").join(format!("{}.yml", custom)) + } + } else { + config_path.join(custom) + } + } + } + } else if espanso_config::is_legacy_config(config_path) { + config_path.join("default.yml") + } else { + config_path.join("match").join("base.yml") + } +} + +pub fn open_editor(file_path: &Path) -> bool { + // Check if another editor is defined in the environment variables + let editor_var = std::env::var_os("EDITOR"); + let visual_var = std::env::var_os("VISUAL"); + + // Prioritize the editors specified by the environment variable, use the default one + let editor: String = if let Some(editor_var) = editor_var { + editor_var.to_string_lossy().to_string() + } else if let Some(visual_var) = visual_var { + visual_var.to_string_lossy().to_string() + } else { + default_editor() + }; + + // Start the editor and wait for its termination + let status = if cfg!(target_os = "windows") { + Command::new(&editor).arg(file_path).spawn() + } else { + // On Unix, spawn the editor using the shell so that it can + // accept parameters. See issue #245 + Command::new("/bin/bash") + .arg("-c") + .arg(format!("{} '{}'", editor, file_path.to_string_lossy())) + .spawn() + }; + + if let Ok(mut child) = status { + // Wait for the user to edit the configuration + let result = child.wait(); + + if let Ok(exit_status) = result { + exit_status.success() + } else { + false + } + } else { + println!("Error: could not start editor at: {}", &editor); + false + } +} + +// fn restart_espanso(paths_overrides: &PathsOverrides) -> Result<()> { +// let espanso_exe_path = std::env::current_exe()?; + +// let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string()); +// command.args(&["restart"]); +// command.with_paths_overrides(paths_overrides); + +// let output = command.output()?; + +// if output.status.success() { +// Ok(()) +// } else { +// bail!("restart command returned a non-zero exit code"); +// } +// } diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs index 64ccff6..2cdba94 100644 --- a/espanso/src/cli/mod.rs +++ b/espanso/src/cli/mod.rs @@ -24,6 +24,7 @@ use espanso_config::{config::ConfigStore, error::NonFatalErrorSet, matches::stor use espanso_path::Paths; pub mod daemon; +pub mod edit; pub mod env_path; pub mod launcher; pub mod log; diff --git a/espanso/src/main.rs b/espanso/src/main.rs index c005612..4ea4050 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -60,6 +60,7 @@ const LOG_FILE_NAME: &str = "espanso.log"; lazy_static! { static ref CLI_HANDLERS: Vec = vec![ cli::path::new(), + cli::edit::new(), cli::launcher::new(), cli::log::new(), cli::worker::new(), @@ -215,17 +216,20 @@ fn main() { // .subcommand(SubCommand::with_name("toggle") // .about("Toggle the status of the espanso replacement engine.")) // ) - // .subcommand(SubCommand::with_name("edit") - // .about("Open the default text editor to edit config files and reload them automatically when exiting") - // .arg(Arg::with_name("config") - // .help("Defaults to \"default\". The configuration file name to edit (without the .yml extension).")) - // .arg(Arg::with_name("norestart") - // .short("n") - // .long("norestart") - // .required(false) - // .takes_value(false) - // .help("Avoid restarting espanso after editing the file")) - // ) + .subcommand(SubCommand::with_name("edit") + .about("Shortcut to open the default text editor to edit config files") + .arg(Arg::with_name("target_file") + .help(r#"Defaults to "match/base.yml", it contains the relative path of the file you want to edit, +such as 'config/default.yml' or 'match/base.yml'. +For convenience, you can also specify the name directly and Espanso will figure out the path. +For example, specifying 'email' is equivalent to 'match/email.yml'."#)) + // .arg(Arg::with_name("norestart") + // .short("n") + // .long("norestart") + // .required(false) + // .takes_value(false) + // .help("Avoid restarting espanso after editing the file")) + ) // .subcommand(SubCommand::with_name("detect") // .about("Tool to detect current window properties, to simplify filters creation.")) .subcommand(