From 393f431bc32072c4d54f9205b0848dd16116599b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Fri, 3 Sep 2021 23:23:35 +0200 Subject: [PATCH] feat(core): implement list, uninstall and update package commands --- espanso/src/cli/package/install.rs | 2 +- espanso/src/cli/package/list.rs | 53 ++++++++++++ espanso/src/cli/package/mod.rs | 32 ++++++- espanso/src/cli/package/uninstall.rs | 39 +++++++++ espanso/src/cli/package/update.rs | 124 +++++++++++++++++++++++++++ espanso/src/exit_code.rs | 4 + espanso/src/main.rs | 19 ++-- 7 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 espanso/src/cli/package/list.rs create mode 100644 espanso/src/cli/package/uninstall.rs create mode 100644 espanso/src/cli/package/update.rs diff --git a/espanso/src/cli/package/install.rs b/espanso/src/cli/package/install.rs index 2e2fe5d..0a737e4 100644 --- a/espanso/src/cli/package/install.rs +++ b/espanso/src/cli/package/install.rs @@ -27,7 +27,7 @@ use crate::info_println; pub fn install_package(paths: &Paths, matches: &ArgMatches) -> Result<()> { let package_name = matches .value_of("package_name") - .ok_or(anyhow!("missing package name"))?; + .ok_or_else(|| anyhow!("missing package name"))?; let version = matches.value_of("version"); let force = matches.is_present("force"); diff --git a/espanso/src/cli/package/list.rs b/espanso/src/cli/package/list.rs new file mode 100644 index 0000000..ea0cba0 --- /dev/null +++ b/espanso/src/cli/package/list.rs @@ -0,0 +1,53 @@ +/* + * 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 anyhow::{Context, Result}; +use clap::ArgMatches; +use espanso_package::StoredPackage; +use espanso_path::Paths; + +use crate::info_println; + +pub fn list_packages(paths: &Paths, _: &ArgMatches) -> Result<()> { + let archiver = + espanso_package::get_archiver(&paths.packages).context("unable to get package archiver")?; + + let packages = archiver.list().context("unable to list packages")?; + + if packages.is_empty() { + info_println!("No packages found!"); + return Ok(()); + } + + info_println!("Installed packages:"); + info_println!(""); + + for package in packages { + match package { + StoredPackage::Legacy(legacy) => { + info_println!("- {} (legacy)", legacy.name); + }, + StoredPackage::Modern(package) => { + info_println!("- {} - version: {} ({})", package.manifest.name, package.manifest.version, package.source); + }, + } + } + + Ok(()) +} diff --git a/espanso/src/cli/package/mod.rs b/espanso/src/cli/package/mod.rs index c21c802..67d3dfc 100644 --- a/espanso/src/cli/package/mod.rs +++ b/espanso/src/cli/package/mod.rs @@ -19,12 +19,18 @@ use crate::{ error_eprintln, - exit_code::{configure_custom_panic_hook, PACKAGE_INSTALL_FAILED, PACKAGE_SUCCESS}, + exit_code::{ + configure_custom_panic_hook, PACKAGE_INSTALL_FAILED, PACKAGE_LIST_FAILED, PACKAGE_SUCCESS, + PACKAGE_UNINSTALL_FAILED, PACKAGE_UPDATE_FAILED, PACKAGE_UPDATE_PARTIAL_FAILURE, + }, }; use super::{CliModule, CliModuleArgs}; mod install; +mod list; +mod uninstall; +mod update; pub fn new() -> CliModule { CliModule { @@ -49,9 +55,29 @@ fn package_main(args: CliModuleArgs) -> i32 { error_eprintln!("unable to install package: {:?}", err); return PACKAGE_INSTALL_FAILED; } + } else if let Some(sub_matches) = cli_args.subcommand_matches("uninstall") { + if let Err(err) = uninstall::uninstall_package(&paths, sub_matches) { + error_eprintln!("unable to uninstall package: {:?}", err); + return PACKAGE_UNINSTALL_FAILED; + } + } else if let Some(sub_matches) = cli_args.subcommand_matches("list") { + if let Err(err) = list::list_packages(&paths, sub_matches) { + error_eprintln!("unable to list packages: {:?}", err); + return PACKAGE_LIST_FAILED; + } + } else if let Some(sub_matches) = cli_args.subcommand_matches("update") { + match update::update_package(&paths, sub_matches) { + Ok(update::UpdateResults::PartialFailure) => { + error_eprintln!("some packages were updated, but not all of them. Check the previous log for more information"); + return PACKAGE_UPDATE_PARTIAL_FAILURE; + } + Err(err) => { + error_eprintln!("unable to update package: {:?}", err); + return PACKAGE_UPDATE_FAILED; + } + _ => {} + } } - // TODO: uninstall, list, update - PACKAGE_SUCCESS } diff --git a/espanso/src/cli/package/uninstall.rs b/espanso/src/cli/package/uninstall.rs new file mode 100644 index 0000000..bba7aaf --- /dev/null +++ b/espanso/src/cli/package/uninstall.rs @@ -0,0 +1,39 @@ +/* + * 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 anyhow::{anyhow, Context, Result}; +use clap::ArgMatches; +use espanso_path::Paths; + +use crate::info_println; + +pub fn uninstall_package(paths: &Paths, matches: &ArgMatches) -> Result<()> { + let package_name = matches + .value_of("package_name") + .ok_or_else(|| anyhow!("missing package name"))?; + + let archiver = + espanso_package::get_archiver(&paths.packages).context("unable to get package archiver")?; + + archiver.delete(package_name).context("unable to delete package")?; + + info_println!("package '{}' uninstalled!", package_name); + + Ok(()) +} diff --git a/espanso/src/cli/package/update.rs b/espanso/src/cli/package/update.rs new file mode 100644 index 0000000..f2d091d --- /dev/null +++ b/espanso/src/cli/package/update.rs @@ -0,0 +1,124 @@ +/* + * 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 anyhow::{anyhow, Context, Result}; +use clap::ArgMatches; +use espanso_package::{Archiver, PackageSpecifier, SaveOptions, StoredPackage}; +use espanso_path::Paths; + +use crate::{error_eprintln, info_println, warn_eprintln}; + +pub enum UpdateResults { + Success, + PartialFailure, +} + +pub fn update_package(paths: &Paths, matches: &ArgMatches) -> Result { + let package_name = matches + .value_of("package_name") + .ok_or_else(|| anyhow!("missing package name"))?; + + let archiver = + espanso_package::get_archiver(&paths.packages).context("unable to get package archiver")?; + + let packages_to_update = if package_name == "all" { + let packages = archiver.list()?; + info_println!("updating {} packages", packages.len()); + + packages + .into_iter() + .map(|package| match package { + StoredPackage::Legacy(legacy) => legacy.name, + StoredPackage::Modern(modern) => modern.manifest.name, + }) + .collect() + } else { + vec![package_name.to_owned()] + }; + + let mut update_errors = Vec::new(); + + for package_name in &packages_to_update { + if let Err(err) = perform_package_update(&*archiver, &package_name) { + error_eprintln!("error updating package '{}': {:?}", package_name, err); + update_errors.push(err); + } + } + + if update_errors.is_empty() { + Ok(UpdateResults::Success) + } else if packages_to_update.len() == update_errors.len() { + Err(update_errors.pop().expect("unable to extract error")) + } else { + Ok(UpdateResults::PartialFailure) + } +} + +fn perform_package_update(archiver: &dyn Archiver, package_name: &str) -> Result<()> { + info_println!("updating package: {}", package_name); + + let package = archiver.get(package_name)?; + + let (package_specifier, old_version) = match package { + StoredPackage::Legacy(legacy) => { + warn_eprintln!( + "detected legacy package '{}' without source information, pulling from espanso hub.", + legacy.name + ); + ( + PackageSpecifier { + name: legacy.name, + ..Default::default() + }, + None, + ) + } + StoredPackage::Modern(modern) => ((&modern).into(), Some(modern.manifest.version)), + }; + + let package_provider = espanso_package::get_provider(&package_specifier) + .context("unable to obtain compatible package provider")?; + + info_println!("using package provider: {}", package_provider.name()); + + let new_package = package_provider.download(&package_specifier)?; + + if new_package.version() == old_version.unwrap_or_default() { + info_println!("already up to date!"); + return Ok(()); + } + + archiver + .save( + &*new_package, + &package_specifier, + &SaveOptions { + overwrite_existing: true, + }, + ) + .context("unable to save package")?; + + info_println!( + "updated package '{}' to version: {}", + new_package.name(), + new_package.version() + ); + + Ok(()) +} diff --git a/espanso/src/exit_code.rs b/espanso/src/exit_code.rs index ab1a091..d2ec881 100644 --- a/espanso/src/exit_code.rs +++ b/espanso/src/exit_code.rs @@ -58,6 +58,10 @@ pub const WORKAROUND_NOT_AVAILABLE: i32 = 2; pub const PACKAGE_SUCCESS: i32 = 0; pub const PACKAGE_UNEXPECTED_FAILURE: i32 = 1; pub const PACKAGE_INSTALL_FAILED: i32 = 2; +pub const PACKAGE_UNINSTALL_FAILED: i32 = 3; +pub const PACKAGE_LIST_FAILED: i32 = 4; +pub const PACKAGE_UPDATE_FAILED: i32 = 5; +pub const PACKAGE_UPDATE_PARTIAL_FAILURE: i32 = 6; use std::sync::Mutex; diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 9781745..8548766 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -368,18 +368,21 @@ fn main() { .about("package-management commands") .subcommand(install_subcommand.clone()) .subcommand(uninstall_subcommand.clone()) + .subcommand(SubCommand::with_name("update").about( + "Update a package. If 'all' is passed as package name, attempts to update all packages.", + ).arg(Arg::with_name("package_name").help("Package name"))) .subcommand( SubCommand::with_name("list").about("List all installed packages"), // TODO: update and update all - ) - .subcommand( - SubCommand::with_name("workaround") - .subcommand( - SubCommand::with_name("secure-input") - .about("Attempt to disable secure input by automating the common steps."), - ) - .about("A collection of workarounds to solve some common problems."), ), ) + .subcommand( + SubCommand::with_name("workaround") + .subcommand( + SubCommand::with_name("secure-input") + .about("Attempt to disable secure input by automating the common steps."), + ) + .about("A collection of workarounds to solve some common problems."), + ) .subcommand( SubCommand::with_name("worker") .setting(AppSettings::Hidden)