From a4853f8fdabad445f49968d063f736f0f461314e Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Mon, 10 Feb 2025 22:22:45 -0600 Subject: basic cli functionality --- ozone-cli/src/cli.rs | 139 +++++++++++++++++++++++++++++++++++++++++--------- ozone-cli/src/main.rs | 130 +++++++++++++++++++++++++++++----------------- 2 files changed, 197 insertions(+), 72 deletions(-) (limited to 'ozone-cli/src') diff --git a/ozone-cli/src/cli.rs b/ozone-cli/src/cli.rs index 281a996..f60c4d2 100644 --- a/ozone-cli/src/cli.rs +++ b/ozone-cli/src/cli.rs @@ -1,56 +1,147 @@ use std::path::PathBuf; use clap::{Args, Parser, Subcommand}; +use clap::error::ErrorKind; +use ozone::launcher::{Instance, InstanceVersion, JavaRuntimeSetting, Resolution}; #[derive(Args, Debug)] -pub struct ProfileSelectArgs { - /// The name of the profile to select. +pub struct InstanceSelectArgs { + /// The name of the instance to select. #[arg(index = 1)] - pub profile: String + pub instance: String +} + +fn parse_resolution_argument(val: &str) -> Result, clap::Error> { + match val.to_ascii_lowercase().as_str() { + "default" => Ok(None), + s => { + let Some(idx) = s.find('x') else { + return Err(clap::Error::raw(ErrorKind::InvalidValue, "malformed resolution argument (expected 'x')")); + }; + + let width: u32 = s[..idx].parse().map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?; + let height: u32 = s[idx+1..].parse().map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?; + + if width == 0 || height == 0 { + return Err(clap::Error::raw(ErrorKind::InvalidValue, "malformed resolution argument (width and height must be positive)")); + } + + Ok(Some(Resolution { width, height })) + } + } +} + +#[derive(Args, Debug)] +pub struct InstanceSettingsArgs { + /// Specify a version of the game which this profile should launch. + #[arg(long, short = 'v', conflicts_with_all = ["latest_release", "latest_snapshot"])] + pub version: Option, + + /// If set, this profile should always use the latest release (default). + /// + /// **Note**: Whenever a new version of Minecraft is released, this profile will + /// automatically switch to that version. This could cause unintended upgrades of settings and + /// worlds. + #[arg(long, conflicts_with_all = ["version", "latest_snapshot"])] + pub latest_release: bool, + + /// If set, this profile should always use the latest snapshot. + /// + /// **Note**: Whenever a new version of Minecraft is released, this profile will + /// automatically switch to that version. This could cause unintended upgrades of settings and + /// worlds. + #[arg(long, short = 's', conflicts_with_all = ["version", "latest_release"])] + pub latest_snapshot: bool, + + /// Specify the path to the java runtime that this profile should use to run the game. + /// + /// By default, the launcher will attempt to download a compatible java runtime before running the game. + #[arg(long, short = 'j', conflicts_with_all = ["jre_component", "jre_default"])] + pub jre_path: Option, + + /// Specify the java runtime component which this profile should download and use when launching + /// the game. If you don't understand what this means, you probably don't need this argument. + #[arg(long, conflicts_with_all = ["jre_path", "jre_default"])] + pub jre_component: Option, + + /// If set, configures this profile to automatically determine the java runtime to download and + /// use when launching the game. This is the default behavior. + #[arg(long, conflicts_with_all = ["jre_path", "jre_component"])] + pub jre_default: bool, + + /// Specify the resolution of the Minecraft game window. Note that this argument is a suggestion + /// to the game process, and could be ignored or unsupported on some versions. + /// + /// Pass the resolution as _width_x_height_ (like `800x600'). + /// + /// Use the special value _default_ to reset to the default value. + #[arg(long, short = 'r', value_parser = parse_resolution_argument)] + pub resolution: Option>, +} + +impl InstanceSettingsArgs { + pub fn apply_to(&self, inst: &mut Instance) { + if let Some(ref ver) = self.version { + inst.game_version = InstanceVersion::Specific(ver.clone()); + } else if self.latest_release { + inst.game_version = InstanceVersion::LatestRelease; + } else if self.latest_snapshot { + inst.game_version = InstanceVersion::LatestSnapshot; + } + + if let Some(ref path) = self.jre_path { + inst.java_runtime = Some(JavaRuntimeSetting::Path(path.clone())); + } else if let Some(ref comp) = self.jre_component { + inst.java_runtime = Some(JavaRuntimeSetting::Component(comp.clone())); + } else if self.jre_default { + inst.java_runtime = None; + } + + if let Some(res) = self.resolution { + inst.resolution = res; + } + } } #[derive(Args, Debug)] -pub struct ProfileCreateArgs { - /// The name of the new profile. +pub struct InstanceCreateArgs { + /// The name of the new instance. Must not be an empty string. #[arg(index = 1)] pub name: String, - /// Clone profile information from an existing profile. + /// If set, clones settings from the selected instance. #[arg(long, short = 'c')] - pub clone: Option, + pub clone: bool, - /// The Minecraft version to be launched by this profile. Will use the latest release by default. - #[arg(long, short = 'v')] - pub version: Option, + /// If set, don't select the newly created instance. + #[arg(long)] + pub no_select: bool, - /// The instance in which this profile will launch the game. By default, will create a new instance - /// with the same name as this profile. - #[arg(long, short = 'i')] - pub instance: Option + #[command(flatten)] + pub settings: InstanceSettingsArgs } #[derive(Subcommand, Debug)] -pub enum ProfileCommand { - Select(ProfileSelectArgs), - Create(ProfileCreateArgs), +pub enum InstanceCommand { + Select(InstanceSelectArgs), + Create(InstanceCreateArgs), List } #[derive(Args, Debug)] -pub struct ProfileArgs { +pub struct InstanceArgs { #[command(subcommand)] - subcmd: Option + subcmd: Option } -impl ProfileArgs { - pub fn command(&self) -> &ProfileCommand { - self.subcmd.as_ref().unwrap_or(&ProfileCommand::List) +impl InstanceArgs { + pub fn command(&self) -> &InstanceCommand { + self.subcmd.as_ref().unwrap_or(&InstanceCommand::List) } } #[derive(Subcommand, Debug)] pub enum RootCommand { - Profile(ProfileArgs), - Instance, + Instance(InstanceArgs), Launch } diff --git a/ozone-cli/src/main.rs b/ozone-cli/src/main.rs index e0cbada..dc4ca4c 100644 --- a/ozone-cli/src/main.rs +++ b/ozone-cli/src/main.rs @@ -4,9 +4,10 @@ use std::error::Error; use std::process::ExitCode; use log::{error, info, trace, LevelFilter}; use clap::Parser; -use ozone::launcher::{Instance, Launcher, Profile, Settings}; +use ozone::launcher::{Instance, Launcher, Settings}; use ozone::launcher::version::{VersionList, VersionResult}; -use crate::cli::{Cli, ProfileCommand, RootCommand}; +use uuid::Uuid; +use crate::cli::{Cli, InstanceCommand, RootCommand}; async fn main_inner(cli: Cli) -> Result> { let Some(home) = cli.home.or_else(Launcher::sensible_home) else { @@ -18,83 +19,116 @@ async fn main_inner(cli: Cli) -> Result> { let mut settings = Settings::load(home.join("ozone.json")).await?; match &cli.subcmd { - RootCommand::Profile(p) => match p.command() { - ProfileCommand::List => { + RootCommand::Instance(p) => match p.command() { + InstanceCommand::List => { let mut first = true; - if settings.get_profiles().is_empty() { - eprintln!("There are no profiles. Create one with `profile create '."); + if settings.instances.is_empty() { + eprintln!("There are no instances. Create one with `profile create '."); return Ok(ExitCode::SUCCESS); } - for (name, profile) in settings.get_profiles().iter() { + for (cur_id, instance) in settings.instances.iter() { if !first { println!(); } first = false; - let sel = if settings.get_selected_profile_name().is_some_and(|n| n == name) { " (selected)" } else { "" }; - let exists = if settings.get_instances().contains_key(profile.get_instance_name()) { "" } else { " (missing!)" }; + let cur_id = *cur_id; + let sel = if settings.selected_instance.is_some_and(|id| id == cur_id) { " (selected)" } else { "" }; - println!("Profile {name}:{sel}"); - println!(" Version: {}", profile.game_version); - println!(" Instance: {}{}", profile.get_instance_name(), exists); + println!("Instance `{}'{sel}:", instance.name); + println!(" Id: {}", cur_id); + println!(" Version: {}", instance.game_version); + println!(" Location: {}", home.join(Settings::get_instance_path(cur_id)).display()); } }, - ProfileCommand::Create(args) => { - if settings.profiles.contains_key(&args.name) { - eprintln!("A profile with that name already exists."); + InstanceCommand::Create(args) => { + if args.name.is_empty() { + eprintln!("The instance must not have an empty name."); return Ok(ExitCode::FAILURE); } - if let Err(e) = Profile::check_name(&args.name) { - eprintln!("The profile name is invalid: {e}"); - return Ok(ExitCode::FAILURE); - } - - let mut profile = if let Some(ref src) = args.clone { - if let Some(profile) = settings.get_profiles().get(src) { - profile.clone() + let mut inst = if args.clone { + if let Some(selected_inst) = settings.selected_instance.and_then(|i| settings.instances.get(&i)) { + let mut inst = selected_inst.clone(); + inst.name.replace_range(.., &args.name); + inst } else { - eprintln!("Unknown profile `{src}'."); + eprintln!("You do not have an instance selected."); return Ok(ExitCode::FAILURE); } } else { - let inst_name = args.instance.as_ref().unwrap_or(&args.name); - if let Err(e) = Instance::check_name(inst_name) { - eprintln!("The profile name is invalid for an instance: {e}"); - eprintln!("Please specify an instance name manually with --instance."); + Instance::new(&args.name) + }; + + if let Some(ref ver_name) = args.settings.version { + // FIXME: don't hardcode "versions" path + let versions = VersionList::new(home.join("versions"), !cli.offline).await?; + if matches!(versions.get_version_lazy(ver_name), VersionResult::None) { + eprintln!("The version `{}' could not be found.", ver_name); return Ok(ExitCode::FAILURE); } + } + + args.settings.apply_to(&mut inst); + + let new_id = Uuid::new_v4(); + settings.instances.insert(new_id, inst); + + if !args.no_select { + settings.selected_instance = Some(new_id); + } - Profile::new(inst_name) - }; - - // creating a new profile from scratch - todo!() + settings.save().await?; }, - ProfileCommand::Select(args) => { - let ver = VersionList::new(home.join("versions"), !cli.offline).await?; - - match ver.get_version_lazy(&args.profile) { - VersionResult::None => { - println!("Unknown version"); - }, - VersionResult::Remote(v) => { - println!("Remote version: {v:?}"); - }, - VersionResult::Complete(v) => { - println!("Complete version: {v:?}"); + InstanceCommand::Select(args) => { + if let Ok(uuid) = args.instance.parse::() { + if settings.instances.get(&uuid).is_none() { + eprintln!("No instances were found by that UUID."); + return Ok(ExitCode::FAILURE); } + + settings.selected_instance = Some(uuid); + settings.save().await?; + + return Ok(ExitCode::SUCCESS); } + + let search_norm = args.instance.to_lowercase(); + + let found: Vec<_> = settings.instances.iter() + .filter(|(_, inst)| { + // FIXME: find a better way of doing this matching + inst.name.to_lowercase().starts_with(&search_norm) + }).collect(); + + if found.is_empty() { + eprintln!("No instances were found."); + return Ok(ExitCode::FAILURE); + } + + if found.len() > 1 { + eprintln!("Ambiguous argument. Found {} instances:", found.len()); + for (id, inst) in found { + eprintln!("- {} ({id})", inst.name); + } + + return Ok(ExitCode::FAILURE); + } + + let (found_id, found_inst) = found.first().unwrap(); + println!("Selected instance {} ({found_id}).", found_inst.name); + + settings.selected_instance = Some(**found_id); + settings.save().await?; } - _ => todo!() }, RootCommand::Launch => { settings.save().await?; - let launcher = Launcher::new(&home, !cli.offline).await?; +/* let launcher = Launcher::new(&home, !cli.offline).await?; let profile = settings.get_profiles().get("default").unwrap(); let launch = launcher.prepare_launch(profile, settings.get_instances().get(profile.get_instance_name()).unwrap(), settings.get_client_id()).await.map_err(|e| { @@ -105,7 +139,7 @@ async fn main_inner(cli: Cli) -> Result> { dbg!(&launch); info!("ok"); - ozone::launcher::run_the_game(&launch)?; + ozone::launcher::run_the_game(&launch)?;*/ } _ => todo!() } -- cgit v1.2.3-70-g09d2