From 1ead48cbc2d32230dd25d4cc416ee04c190d9068 Mon Sep 17 00:00:00 2001 From: Warren Hood Date: Sat, 24 Aug 2024 14:03:27 +0200 Subject: [PATCH] Added an install button and switched back to using anyhow --- Cargo.lock | 8 +++++ mcmpmgr/Cargo.toml | 1 + mcmpmgr/src/main.rs | 38 +++++++++++++------- mcmpmgr/src/mod_meta.rs | 12 +++---- mcmpmgr/src/modpack.rs | 26 +++++++------- mcmpmgr/src/profiles.rs | 21 +++++++---- mcmpmgr/src/providers/mod.rs | 7 ++-- mcmpmgr/src/providers/modrinth.rs | 49 +++++++++++++------------ mcmpmgr/src/resolver.rs | 45 ++++++++++++----------- mmm/Cargo.toml | 1 + mmm/src/main.rs | 60 +++++++++++++++++++++++++++++++ 11 files changed, 179 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9802dc..9bf47f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "approx" version = "0.5.1" @@ -2063,6 +2069,7 @@ dependencies = [ name = "mcmpmgr" version = "0.1.0" dependencies = [ + "anyhow", "clap", "git2", "home", @@ -2165,6 +2172,7 @@ dependencies = [ name = "mmm" version = "0.1.0" dependencies = [ + "anyhow", "iced", "mcmpmgr", "rfd", diff --git a/mcmpmgr/Cargo.toml b/mcmpmgr/Cargo.toml index c60efd4..cec2b87 100644 --- a/mcmpmgr/Cargo.toml +++ b/mcmpmgr/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.86" clap = { version = "4.5.15", features = ["derive"] } git2 = "0.19.0" home = "0.5.9" diff --git a/mcmpmgr/src/main.rs b/mcmpmgr/src/main.rs index b306266..77c59c9 100644 --- a/mcmpmgr/src/main.rs +++ b/mcmpmgr/src/main.rs @@ -4,12 +4,13 @@ mod profiles; mod providers; mod resolver; +use anyhow::{Error, Result}; use clap::{Args, Parser, Subcommand}; use mod_meta::{ModMeta, ModProvider}; use modpack::ModpackMeta; use profiles::{PackSource, Profile}; use providers::DownloadSide; -use std::{error::Error, path::PathBuf}; +use std::path::PathBuf; /// A Minecraft Modpack Manager #[derive(Parser)] @@ -152,7 +153,7 @@ enum ProfileCommands { } #[tokio::main(flavor = "multi_thread")] -async fn main() -> Result<(), Box> { +async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); if let Some(command) = cli.command { @@ -168,13 +169,15 @@ async fn main() -> Result<(), Box> { let pack_name = if let Some(name) = name { name } else { - dir.file_name() - .ok_or(format!( + let dir_name = dir.file_name(); + if let Some(dir_name) = dir_name { + dir_name.to_string_lossy().to_string() + } else { + anyhow::bail!( "Cannot find pack name based on directory '{}'", dir.display() - ))? - .to_string_lossy() - .into() + ) + } }; println!( "Initializing project '{}' at '{}'...", @@ -392,9 +395,14 @@ async fn main() -> Result<(), Box> { } ProfileCommands::Install { name } => { let userdata = profiles::Data::load()?; - let profile = userdata - .get_profile(&name) - .ok_or(format!("Profile '{name}' does not exist"))?; + let profile = userdata.get_profile(&name); + + let profile = if let Some(profile) = profile { + profile + } else { + anyhow::bail!("Profile '{name}' does not exist") + }; + println!("Installing profile '{name}'..."); profile.install().await?; println!("Installed profile '{name}' successfully"); @@ -406,9 +414,13 @@ async fn main() -> Result<(), Box> { } ProfileCommands::Show { name } => { let userdata = profiles::Data::load()?; - let profile = userdata - .get_profile(&name) - .ok_or(format!("Profile '{name}' does not exist"))?; + let profile = userdata.get_profile(&name); + + let profile = if let Some(profile) = profile { + profile + } else { + anyhow::bail!("Profile '{name}' does not exist") + }; println!("Profile name : {name}"); println!("Mods folder : {}", profile.mods_folder.display()); println!("Modpack source: {}", profile.pack_source); diff --git a/mcmpmgr/src/mod_meta.rs b/mcmpmgr/src/mod_meta.rs index 49ae5c7..05812a9 100644 --- a/mcmpmgr/src/mod_meta.rs +++ b/mcmpmgr/src/mod_meta.rs @@ -1,6 +1,6 @@ -use std::{borrow::BorrowMut, error::Error}; - +use anyhow::Result; use serde::{Deserialize, Serialize}; +use std::{borrow::BorrowMut, error::Error}; use crate::modpack::ModLoader; @@ -15,14 +15,14 @@ pub enum ModProvider { } impl std::str::FromStr for ModProvider { - type Err = String; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "curseforge" => Ok(ModProvider::CurseForge), "modrinth" => Ok(ModProvider::Modrinth), "raw" => Ok(ModProvider::Raw), - _ => Err(format!("Invalid mod launcher: {}", s)), + _ => anyhow::bail!("Invalid mod provider: {}", s), } } } @@ -47,11 +47,11 @@ impl PartialEq for ModMeta { impl Eq for ModMeta {} impl ModMeta { - pub fn new(mod_name: &str) -> Result> { + pub fn new(mod_name: &str) -> Result { if mod_name.contains("@") { let mod_name_and_version: Vec<&str> = mod_name.split("@").collect(); if mod_name_and_version.len() != 2 { - return Err(format!("Invalid mod with version constraint: '{}'", &mod_name).into()); + anyhow::bail!("Invalid mod with version constraint: '{}'", &mod_name) } return Ok(Self { name: mod_name_and_version[0].into(), diff --git a/mcmpmgr/src/modpack.rs b/mcmpmgr/src/modpack.rs index 34792fd..ebe3518 100644 --- a/mcmpmgr/src/modpack.rs +++ b/mcmpmgr/src/modpack.rs @@ -1,8 +1,8 @@ use crate::mod_meta::{ModMeta, ModProvider}; +use anyhow::Result; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, - error::Error, path::{Path, PathBuf}, }; @@ -25,13 +25,13 @@ impl ToString for ModLoader { } impl std::str::FromStr for ModLoader { - type Err = String; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s { "Fabric" => Ok(Self::Fabric), "Forge" => Ok(Self::Forge), - _ => Err(format!("Invalid mod launcher: {}", s)), + _ => anyhow::bail!("Invalid mod launcher: {}", s), } } } @@ -60,20 +60,19 @@ impl ModpackMeta { self.mods.values().into_iter() } - pub fn load_from_directory(directory: &Path) -> Result> { + pub fn load_from_directory(directory: &Path) -> Result { let modpack_meta_file_path = directory.join(PathBuf::from(MODPACK_FILENAME)); if !modpack_meta_file_path.exists() { - return Err(format!( + anyhow::bail!( "Directory '{}' does not seem to be a valid modpack project directory.", directory.display() ) - .into()); }; let modpack_contents = std::fs::read_to_string(modpack_meta_file_path)?; Ok(toml::from_str(&modpack_contents)?) } - pub fn load_from_current_directory() -> Result> { + pub fn load_from_current_directory() -> Result { Self::load_from_directory(&std::env::current_dir()?) } @@ -84,9 +83,9 @@ impl ModpackMeta { self } - pub fn add_mod(mut self, mod_meta: &ModMeta) -> Result> { + pub fn add_mod(mut self, mod_meta: &ModMeta) -> Result { if self.forbidden_mods.contains(&mod_meta.name) { - return Err(format!("Cannot add forbidden mod {} to modpack", mod_meta.name).into()); + anyhow::bail!("Cannot add forbidden mod {} to modpack", mod_meta.name) } else { self.mods .insert(mod_meta.name.to_string(), mod_meta.clone()); @@ -104,14 +103,13 @@ impl ModpackMeta { self } - pub fn init_project(&self, directory: &Path) -> Result<(), Box> { + pub fn init_project(&self, directory: &Path) -> Result<()> { let modpack_meta_file_path = directory.join(PathBuf::from(MODPACK_FILENAME)); if modpack_meta_file_path.exists() { - return Err(format!( + anyhow::bail!( "{MODPACK_FILENAME} already exists at {}", modpack_meta_file_path.display() ) - .into()); } self.save_to_file(&modpack_meta_file_path)?; @@ -119,7 +117,7 @@ impl ModpackMeta { Ok(()) } - pub fn save_to_file(&self, path: &PathBuf) -> Result<(), Box> { + pub fn save_to_file(&self, path: &PathBuf) -> Result<()> { std::fs::write( path, toml::to_string(self).expect("MC Modpack Meta should be serializable"), @@ -128,7 +126,7 @@ impl ModpackMeta { Ok(()) } - pub fn save_current_dir_project(&self) -> Result<(), Box> { + pub fn save_current_dir_project(&self) -> Result<()> { let modpack_meta_file_path = std::env::current_dir()?.join(PathBuf::from(MODPACK_FILENAME)); self.save_to_file(&modpack_meta_file_path)?; Ok(()) diff --git a/mcmpmgr/src/profiles.rs b/mcmpmgr/src/profiles.rs index 8b81a65..1e0ae92 100644 --- a/mcmpmgr/src/profiles.rs +++ b/mcmpmgr/src/profiles.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -57,7 +58,7 @@ impl Profile { } } - pub async fn install(&self) -> Result<(), Box> { + pub async fn install(&self) -> Result<()> { let (pack_lock, temp_dir) = match &self.pack_source { PackSource::Git { url } => { let (pack_lock, packdir) = PinnedPackMeta::load_from_git_repo(&url, true).await?; @@ -112,13 +113,19 @@ impl Data { self.profiles.remove(profile_name); } - fn get_config_folder_path() -> Result> { - home::home_dir() - .and_then(|home_dir| Some(home_dir.join(format!(".config/{CONFIG_DIR_NAME}")))) - .ok_or("Unable to locate home directory".into()) + fn get_config_folder_path() -> Result { + let home_dir = home::home_dir() + .and_then(|home_dir| Some(home_dir.join(format!(".config/{CONFIG_DIR_NAME}")))); + + if let Some(home_dir) = home_dir { + Ok(home_dir) + } + else { + anyhow::bail!("Unable to locate home directory") + } } - pub fn load() -> Result> { + pub fn load() -> Result { let config_dir = Self::get_config_folder_path()?; if !config_dir.exists() { println!("Creating config directory {config_dir:#?}..."); @@ -135,7 +142,7 @@ impl Data { }) } - pub fn save(&self) -> Result<(), Box> { + pub fn save(&self) -> Result<()> { let config_dir = Self::get_config_folder_path()?; if !config_dir.exists() { println!("Creating config directory {config_dir:#?}..."); diff --git a/mcmpmgr/src/providers/mod.rs b/mcmpmgr/src/providers/mod.rs index ab043d0..e00b578 100644 --- a/mcmpmgr/src/providers/mod.rs +++ b/mcmpmgr/src/providers/mod.rs @@ -29,17 +29,14 @@ pub enum DownloadSide { } impl FromStr for DownloadSide { - type Err = String; + type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { "both" => Ok(DownloadSide::Both), "client" => Ok(DownloadSide::Client), "server" => Ok(DownloadSide::Server), - _ => Err(format!( - "Invalid side {}. Expected one of: both, server, clide", - s - )), + _ => anyhow::bail!("Invalid side {}. Expected one of: both, server, clide", s), } } } diff --git a/mcmpmgr/src/providers/modrinth.rs b/mcmpmgr/src/providers/modrinth.rs index 0d6538b..dd3a377 100644 --- a/mcmpmgr/src/providers/modrinth.rs +++ b/mcmpmgr/src/providers/modrinth.rs @@ -1,5 +1,6 @@ +use anyhow::{Error, Result}; use serde::{Deserialize, Serialize}; -use std::{collections::HashSet, error::Error}; +use std::collections::HashSet; use super::PinnedMod; use crate::{ @@ -65,7 +66,7 @@ impl Modrinth { } } - async fn get_project(&self, project_id: &str) -> Result> { + async fn get_project(&self, project_id: &str) -> Result { let project: ModrinthProject = self .client .get(format!("https://api.modrinth.com/v2/project/{project_id}")) @@ -84,7 +85,7 @@ impl Modrinth { pack_meta: &ModpackMeta, loader_override: Option, game_version_override: Option, - ) -> Result> { + ) -> Result { let project_versions = self .get_project_versions( project_id, @@ -113,20 +114,15 @@ impl Modrinth { return Ok(mod_meta); } } - Err(format!( + anyhow::bail!( "Couldn't find project '{}' with version '{}'", project_id, project_version.unwrap_or("*") ) - .into()) } /// Resolve a list of mod candidates in order of newest to oldest - pub async fn resolve( - &self, - mod_meta: &ModMeta, - pack_meta: &ModpackMeta, - ) -> Result> { + pub async fn resolve(&self, mod_meta: &ModMeta, pack_meta: &ModpackMeta) -> Result { let versions = self .get_project_versions( &mod_meta.name, @@ -138,21 +134,28 @@ impl Modrinth { .await?; let package = if mod_meta.version == "*" { - versions.first().ok_or(format!( - "Cannot find package {} for loader={} and mc version={}", - mod_meta.name, - pack_meta.modloader.to_string().to_lowercase(), - pack_meta.mc_version - ))? + let version = versions.first(); + if let Some(version) = version { + version + } else { + anyhow::bail!( + "Cannot find package {} for loader={} and mc version={}", + mod_meta.name, + pack_meta.modloader.to_string().to_lowercase(), + pack_meta.mc_version + ) + } } else { - versions + let version = versions .iter() .filter(|v| v.version_number == mod_meta.version) - .nth(0) - .ok_or(format!( - "Cannot find package {}@{}", - mod_meta.name, mod_meta.version - ))? + .nth(0); + + if let Some(version) = version { + version + } else { + anyhow::bail!("Cannot find package {}@{}", mod_meta.name, mod_meta.version) + } }; let mut deps_meta = HashSet::new(); @@ -207,7 +210,7 @@ impl Modrinth { ignore_game_version_and_loader: bool, // For deps we might as well let them use anything loader_override: Option, game_version_override: Option, - ) -> Result, Box> { + ) -> Result> { let loader = loader_override .unwrap_or(pack_meta.modloader.clone()) .to_string() diff --git a/mcmpmgr/src/resolver.rs b/mcmpmgr/src/resolver.rs index b2f3092..108e7f5 100644 --- a/mcmpmgr/src/resolver.rs +++ b/mcmpmgr/src/resolver.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha512}; use std::{ @@ -35,7 +36,7 @@ impl PinnedPackMeta { &self, mods_dir: &PathBuf, download_side: DownloadSide, - ) -> Result<(), Box> { + ) -> Result<()> { let files = std::fs::read_dir(mods_dir)?; let mut pinned_files_cache = HashSet::new(); for file in files.into_iter() { @@ -80,11 +81,12 @@ impl PinnedPackMeta { "Sha512 hash mismatch for file {}\nExpected:\n{}\nGot:\n{}", filename, sha512, sha512_hash ); - return Err(format!( + anyhow::bail!( "Sha512 hash mismatch for file {}\nExpected:\n{}\nGot:\n{}", - filename, sha512, sha512_hash + filename, + sha512, + sha512_hash ) - .into()); } tokio::fs::write(mods_dir.join(filename), file_contents).await?; @@ -154,7 +156,7 @@ impl PinnedPackMeta { mod_metadata: &ModMeta, pack_metadata: &ModpackMeta, ignore_transitive_versions: bool, - ) -> Result<(), Box> { + ) -> Result<()> { if let Some(mod_meta) = self.mods.get(&mod_metadata.name) { if mod_metadata.version != "*" && mod_metadata.version == mod_meta.version { // Skip already pinned mods @@ -199,7 +201,7 @@ impl PinnedPackMeta { &mut self, mod_metadata: &ModMeta, pack_metadata: &ModpackMeta, - ) -> Result, Box> { + ) -> Result> { if pack_metadata.forbidden_mods.contains(&mod_metadata.name) { println!("Skipping adding forbidden mod {}...", mod_metadata.name); return Ok(vec![]); @@ -247,11 +249,12 @@ impl PinnedPackMeta { }; } - Err(format!( + anyhow::bail!( "Failed to pin mod '{}' (providers={:#?}) with constraint {} and all its deps", - mod_metadata.name, mod_metadata.providers, mod_metadata.version + mod_metadata.name, + mod_metadata.providers, + mod_metadata.version ) - .into()) } fn get_dependent_mods(&self, mod_name: &str) -> HashSet { @@ -274,7 +277,7 @@ impl PinnedPackMeta { mod_name: &str, pack_metadata: &ModpackMeta, force: bool, - ) -> Result<(), Box> { + ) -> Result<()> { if !self.mods.contains_key(mod_name) { eprintln!( "Skipping removing non-existent mod {} from modpack", @@ -288,11 +291,11 @@ impl PinnedPackMeta { if force { println!("Forcefully removing mod {} even though it is depended on by the following mods:\n{:#?}", mod_name, dependent_mods); } else { - return Err(format!( + anyhow::bail!( "Cannot remove mod {}.The following mods depend on it:\n{:#?}", - mod_name, dependent_mods + mod_name, + dependent_mods ) - .into()); } } let removed_mod = self.mods.remove(mod_name); @@ -304,7 +307,7 @@ impl PinnedPackMeta { } /// Remove all mods from lockfile that aren't in the pack metadata or depended on by another mod - fn prune_mods(&mut self, pack_metadata: &ModpackMeta) -> Result<(), Box> { + fn prune_mods(&mut self, pack_metadata: &ModpackMeta) -> Result<()> { let mods_to_remove: HashSet = self .mods .keys() @@ -329,7 +332,7 @@ impl PinnedPackMeta { &mut self, modpack_meta: &ModpackMeta, ignore_transitive_versions: bool, - ) -> Result<(), Box> { + ) -> Result<()> { for mod_meta in modpack_meta.iter_mods() { self.pin_mod_and_deps(mod_meta, modpack_meta, ignore_transitive_versions) .await?; @@ -337,7 +340,7 @@ impl PinnedPackMeta { Ok(()) } - pub fn save_to_file(&self, path: &PathBuf) -> Result<(), Box> { + pub fn save_to_file(&self, path: &PathBuf) -> Result<()> { std::fs::write( path, toml::to_string(self).expect("Pinned pack meta should be serializable"), @@ -346,12 +349,12 @@ impl PinnedPackMeta { Ok(()) } - pub fn save_current_dir_lock(&self) -> Result<(), Box> { + pub fn save_current_dir_lock(&self) -> Result<()> { self.save_to_dir(&std::env::current_dir()?)?; Ok(()) } - pub fn save_to_dir(&self, dir: &PathBuf) -> Result<(), Box> { + pub fn save_to_dir(&self, dir: &PathBuf) -> Result<()> { let modpack_lock_file_path = dir.join(PathBuf::from(MODPACK_LOCK_FILENAME)); self.save_to_file(&modpack_lock_file_path)?; Ok(()) @@ -360,7 +363,7 @@ impl PinnedPackMeta { pub async fn load_from_directory( directory: &Path, ignore_transitive_versions: bool, - ) -> Result> { + ) -> Result { let modpack_lock_file_path = directory.join(PathBuf::from(MODPACK_LOCK_FILENAME)); if !modpack_lock_file_path.exists() { let mut new_modpack_lock = Self::new(); @@ -378,7 +381,7 @@ impl PinnedPackMeta { pub async fn load_from_current_directory( ignore_transitive_versions: bool, - ) -> Result> { + ) -> Result { Self::load_from_directory(&std::env::current_dir()?, ignore_transitive_versions).await } @@ -386,7 +389,7 @@ impl PinnedPackMeta { pub async fn load_from_git_repo( git_url: &str, ignore_transitive_versions: bool, - ) -> Result<(Self, tempfile::TempDir), Box> { + ) -> Result<(Self, tempfile::TempDir)> { let pack_dir = tempfile::tempdir()?; println!( "Cloning modpack from git repo {} to {:#?}...", diff --git a/mmm/Cargo.toml b/mmm/Cargo.toml index 4501090..8fa0fa3 100644 --- a/mmm/Cargo.toml +++ b/mmm/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.86" iced = { version = "0.12.1", features = ["tokio"] } mcmpmgr = { version = "0.1.0", path = "../mcmpmgr" } rfd = "0.14.1" diff --git a/mmm/src/main.rs b/mmm/src/main.rs index edc1e96..8e64cd9 100644 --- a/mmm/src/main.rs +++ b/mmm/src/main.rs @@ -1,3 +1,4 @@ +use std::fmt::format; use std::path::PathBuf; use std::str::FromStr; @@ -24,6 +25,7 @@ struct ManagerGUI { previous_view: ManagerView, profile_edit_settings: ProfileSettings, profile_save_error: Option, + current_install_status: ProfileInstallStatus } #[derive(Debug, Clone)] @@ -84,6 +86,22 @@ enum Message { EditPackSource(String), SaveProfile, DeleteProfile(String), + InstallProfile(String), + ProfileInstalled(ProfileInstallStatus) +} + +#[derive(Debug, Clone)] +enum ProfileInstallStatus { + NotStarted, + Installing, + Success, + Error(String) +} + +impl Default for ProfileInstallStatus { + fn default() -> Self { + Self::NotStarted + } } impl Application for ManagerGUI { @@ -206,6 +224,34 @@ impl Application for ManagerGUI { Command::none() } + Message::InstallProfile(name) => { + self.current_install_status = ProfileInstallStatus::Installing; + let profile_name = name.clone(); + let profile = self.userdata.get_profile(&name).cloned(); + Command::perform( + async move { + if let Some(profile) = profile { + let result = profile.install().await; + if let Err(err) = result { + ProfileInstallStatus::Error(format!("{}", err)) + } + else { + ProfileInstallStatus::Success + } + } + else { + ProfileInstallStatus::Error(format!("Profile '{}' doesn't exist", name)) + } + }, + Message::ProfileInstalled + ) + }, + Message::ProfileInstalled(result) => { + self.current_install_status = result; + + Command::none() + }, + } } @@ -283,6 +329,7 @@ impl ManagerGUI { ] .spacing(20), row!["Mods to download", text(profile.side),].spacing(5), + button("Install").on_press(Message::InstallProfile(profile_name.into())), row![ button("Back").on_press(Message::SwitchView(ManagerView::ProfileSelect)), button("Edit profile").on_press(Message::SwitchView( @@ -305,6 +352,19 @@ impl ManagerGUI { profile_view = profile_view.push(text(err)); } + match &self.current_install_status { + ProfileInstallStatus::NotStarted => {}, + ProfileInstallStatus::Installing => { + profile_view = profile_view.push(text("Installing...")); + }, + ProfileInstallStatus::Success => { + profile_view = profile_view.push(text("Installed")); + }, + ProfileInstallStatus::Error(err) => { + profile_view = profile_view.push(text(format!("Failed to install profile: {}", err))); + }, + }; + profile_view .align_items(Alignment::Center) .spacing(10)