diff options
-rw-r--r-- | src/install.rs | 142 | ||||
-rw-r--r-- | src/main.rs | 192 | ||||
-rw-r--r-- | src/uiutil.rs | 55 |
3 files changed, 205 insertions, 184 deletions
diff --git a/src/install.rs b/src/install.rs new file mode 100644 index 0000000..56754d3 --- /dev/null +++ b/src/install.rs @@ -0,0 +1,142 @@ +//! Implementation of the `modderbaas install` subcommand. + +use std::{io::Write, path::Path, str::FromStr}; + +use anyhow::{bail, Context, Result}; +use clap::ArgMatches; +use itertools::Itertools; +use termcolor::StandardStream; + +use modderbaas::{ + baas::{Baas, Installer}, + minemod::{self, ModContainer}, + util, ContentDb, Downloader, MineMod, Source, World, +}; + +use super::uiutil::{ask_continue, user_choice}; + +/// Install the given mods, installing dependencies if needed. +pub fn install_mods( + output: &mut StandardStream, + baas: &Baas, + world: &World, + mods: &[&str], + matches: &ArgMatches, +) -> Result<()> { + let target_dir = Path::new(matches.value_of("target").unwrap()); + let dry_run = matches.is_present("dry-run"); + let chown = matches.is_present("chown"); + + let content_db = ContentDb::new(); + let downloader = Downloader::new()?; + let sources = mods + .iter() + .map(|&s| Source::from_str(s)) + .collect::<Result<Vec<_>, _>>()?; + + let installer = InteractiveInstaller { + output, + content_db, + target_dir, + dry_run, + chown, + }; + baas.install(installer, &downloader, world, sources)?; + + writeln!(output, "Done!")?; + + Ok(()) +} + +/// The installer that interactively asks the user about choices. +struct InteractiveInstaller<'o, 'p> { + output: &'o mut StandardStream, + content_db: ContentDb, + target_dir: &'p Path, + dry_run: bool, + chown: bool, +} + +impl<'o, 'p> Installer for InteractiveInstaller<'o, 'p> { + type Error = anyhow::Error; + + fn resolve(&mut self, mod_id: &str) -> Result<Source> { + writeln!(&mut self.output, "Searching for candidates: {}", mod_id)?; + + let candidates = self.content_db.resolve(mod_id)?; + if candidates.is_empty() { + bail!("Could not find a suitable mod for '{}'", mod_id); + } else if candidates.len() == 1 { + Ok(Source::Http(candidates.into_iter().next().unwrap().url)) + } else { + let items = candidates + .into_iter() + .map(|c| { + ( + format!("{} by {} - {}", c.title, c.author, c.short_description), + c, + ) + }) + .collect::<Vec<_>>(); + writeln!( + &mut self.output, + "{} candidates found, please select one:", + items.len() + )?; + let candidate = user_choice(&items, self.output)?; + Ok(Source::Http(candidate.url.clone())) + } + } + + fn install_mod(&mut self, mod_or_pack: &dyn ModContainer) -> Result<Box<dyn ModContainer>> { + let mod_id = mod_or_pack.name()?; + writeln!(&mut self.output, "Installing {}", mod_id)?; + + if self.dry_run { + // Re-open so we get a fresh Box<> + return Ok(minemod::open_mod_or_pack(mod_or_pack.path())?); + } + + let installed = mod_or_pack + .install_to(self.target_dir) + .context(format!("Error installing '{}'", mod_id))?; + + #[cfg(unix)] + { + use nix::{ + sys::stat, + unistd::{Gid, Uid}, + }; + if self.chown { + let perms = stat::stat(self.target_dir)?; + let (uid, gid) = (Uid::from_raw(perms.st_uid), Gid::from_raw(perms.st_gid)); + util::chown_recursive(installed.path(), Some(uid), Some(gid))?; + } + } + + Ok(installed) + } + + fn display_changes(&mut self, to_install: &[Box<dyn ModContainer>]) -> Result<()> { + writeln!( + &mut self.output, + "Installing {} new mods:", + to_install.len() + )?; + writeln!(&mut self.output, "{}", to_install.iter().join(", "))?; + + ask_continue(self.output) + } + + fn enable_mod(&mut self, world: &World, minemod: &MineMod) -> Result<()> { + let mod_id = minemod.mod_id()?; + writeln!(&mut self.output, "Enabling {}", mod_id)?; + + if !self.dry_run { + world + .enable_mod(&mod_id) + .context(format!("Error enabling '{}'", mod_id))?; + } + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 817d39b..5390479 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,4 @@ -use std::{ - fmt::Display, - io::{self, Write}, - path::Path, - str::FromStr, -}; +use std::io::Write; use anyhow::{anyhow, bail, Context, Result}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -11,11 +6,13 @@ use itertools::Itertools; use log::debug; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; -use modderbaas::{ - baas::{Baas, Installer}, - minemod::{self, ModContainer}, - util, ContentDb, Downloader, MineMod, Snapshot, Source, World, -}; +use modderbaas::{baas::Baas, MineMod, Snapshot, World}; + +mod uiutil; +use uiutil::{ask_continue, user_choice}; + +mod install; +use install::install_mods; fn main() -> Result<()> { stderrlog::new() @@ -206,176 +203,3 @@ fn enable_mods( writeln!(output, "Done!")?; Ok(()) } - -/// Install the given mods, installing dependencies if needed. -fn install_mods( - output: &mut StandardStream, - baas: &Baas, - world: &World, - mods: &[&str], - matches: &ArgMatches, -) -> Result<()> { - let target_dir = Path::new(matches.value_of("target").unwrap()); - let dry_run = matches.is_present("dry-run"); - let chown = matches.is_present("chown"); - - let content_db = ContentDb::new(); - let downloader = Downloader::new()?; - let sources = mods - .iter() - .map(|&s| Source::from_str(s)) - .collect::<Result<Vec<_>, _>>()?; - - let installer = InteractiveInstaller { - output, - content_db, - target_dir, - dry_run, - chown, - }; - baas.install(installer, &downloader, world, sources)?; - - writeln!(output, "Done!")?; - - Ok(()) -} - -/// The installer that interactively asks the user about choices. -struct InteractiveInstaller<'o, 'p> { - output: &'o mut StandardStream, - content_db: ContentDb, - target_dir: &'p Path, - dry_run: bool, - chown: bool, -} - -impl<'o, 'p> Installer for InteractiveInstaller<'o, 'p> { - type Error = anyhow::Error; - - fn resolve(&mut self, mod_id: &str) -> Result<Source> { - writeln!(&mut self.output, "Searching for candidates: {}", mod_id)?; - - let candidates = self.content_db.resolve(mod_id)?; - if candidates.is_empty() { - bail!("Could not find a suitable mod for '{}'", mod_id); - } else if candidates.len() == 1 { - Ok(Source::Http(candidates.into_iter().next().unwrap().url)) - } else { - let items = candidates - .into_iter() - .map(|c| { - ( - format!("{} by {} - {}", c.title, c.author, c.short_description), - c, - ) - }) - .collect::<Vec<_>>(); - writeln!( - &mut self.output, - "{} candidates found, please select one:", - items.len() - )?; - let candidate = user_choice(&items, self.output)?; - Ok(Source::Http(candidate.url.clone())) - } - } - - fn install_mod(&mut self, mod_or_pack: &dyn ModContainer) -> Result<Box<dyn ModContainer>> { - let mod_id = mod_or_pack.name()?; - writeln!(&mut self.output, "Installing {}", mod_id)?; - - if self.dry_run { - // Re-open so we get a fresh Box<> - return Ok(minemod::open_mod_or_pack(mod_or_pack.path())?); - } - - let installed = mod_or_pack - .install_to(self.target_dir) - .context(format!("Error installing '{}'", mod_id))?; - - #[cfg(unix)] - { - use nix::{ - sys::stat, - unistd::{Gid, Uid}, - }; - if self.chown { - let perms = stat::stat(self.target_dir)?; - let (uid, gid) = (Uid::from_raw(perms.st_uid), Gid::from_raw(perms.st_gid)); - util::chown_recursive(installed.path(), Some(uid), Some(gid))?; - } - } - - Ok(installed) - } - - fn display_changes(&mut self, to_install: &[Box<dyn ModContainer>]) -> Result<()> { - writeln!( - &mut self.output, - "Installing {} new mods:", - to_install.len() - )?; - writeln!(&mut self.output, "{}", to_install.iter().join(", "))?; - - ask_continue(self.output) - } - - fn enable_mod(&mut self, world: &World, minemod: &MineMod) -> Result<()> { - let mod_id = minemod.mod_id()?; - writeln!(&mut self.output, "Enabling {}", mod_id)?; - - if !self.dry_run { - world - .enable_mod(&mod_id) - .context(format!("Error enabling '{}'", mod_id))?; - } - Ok(()) - } -} - -/// Presents the user with a choice of items and awaits a selection. -fn user_choice<'i, L: Display, I>( - items: &'i [(L, I)], - output: &mut StandardStream, -) -> Result<&'i I> { - for (i, (label, _)) in items.iter().enumerate() { - output.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?; - write!(output, "[{}]", i)?; - output.reset()?; - writeln!(output, " {}", label)?; - } - - let stdin = io::stdin(); - loop { - write!(output, "Enter a number: ")?; - output.flush()?; - let mut buffer = String::new(); - stdin.read_line(&mut buffer)?; - if let Ok(number) = buffer.trim().parse::<usize>() { - if number < items.len() { - return Ok(&items[number].1); - } - } - } -} - -/// Ask the user whether they want to continue. -/// -/// Returns `Ok(())` if the program should continue, and an error otherwise. -fn ask_continue(output: &mut StandardStream) -> Result<()> { - let stdin = io::stdin(); - loop { - output.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?; - write!(output, "Continue? [Y/n] ")?; - output.reset()?; - output.flush()?; - - let mut buffer = String::new(); - stdin.read_line(&mut buffer)?; - if buffer == "\n" || buffer == "Y\n" || buffer == "y\n" { - return Ok(()); - } else if buffer == "N\n" || buffer == "n\n" { - bail!("Cancelled by user"); - } - } -} diff --git a/src/uiutil.rs b/src/uiutil.rs new file mode 100644 index 0000000..188e20c --- /dev/null +++ b/src/uiutil.rs @@ -0,0 +1,55 @@ +//! Utility functions for user interaction. +use std::{ + fmt::Display, + io::{self, Write}, +}; + +use anyhow::{bail, Result}; +use termcolor::{Color, ColorSpec, StandardStream, WriteColor}; + +/// Presents the user with a choice of items and awaits a selection. +pub fn user_choice<'i, L: Display, I>( + items: &'i [(L, I)], + output: &mut StandardStream, +) -> Result<&'i I> { + for (i, (label, _)) in items.iter().enumerate() { + output.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?; + write!(output, "[{}]", i)?; + output.reset()?; + writeln!(output, " {}", label)?; + } + + let stdin = io::stdin(); + loop { + write!(output, "Enter a number: ")?; + output.flush()?; + let mut buffer = String::new(); + stdin.read_line(&mut buffer)?; + if let Ok(number) = buffer.trim().parse::<usize>() { + if number < items.len() { + return Ok(&items[number].1); + } + } + } +} + +/// Ask the user whether they want to continue. +/// +/// Returns `Ok(())` if the program should continue, and an error otherwise. +pub fn ask_continue(output: &mut StandardStream) -> Result<()> { + let stdin = io::stdin(); + loop { + output.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?; + write!(output, "Continue? [Y/n] ")?; + output.reset()?; + output.flush()?; + + let mut buffer = String::new(); + stdin.read_line(&mut buffer)?; + if buffer == "\n" || buffer == "Y\n" || buffer == "y\n" { + return Ok(()); + } else if buffer == "N\n" || buffer == "n\n" { + bail!("Cancelled by user"); + } + } +} |