use std::path::PathBuf;
use std::str::FromStr;
use clap::{Args, Parser, Subcommand, ValueEnum};
use uuid::Uuid;
use portablemc::{fabric, forge};
use portablemc::maven::Gav;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const VERSION_LONG: &str = match option_env!("PMC_VERSION_LONG") {
Some(version) => version,
None => VERSION,
};
const ORDER_MAIN_DIR: usize = 00;
const ORDER_MC_DIR: usize = 01;
const ORDER_BIN_DIR: usize = 02;
const ORDER_MSA: usize = 05;
const ORDER_OUTPUT: usize = 06;
const ORDER_VERBOSE: usize = 07;
const ORDER_COMMON: usize = 10;
const ORDER_SWITCH: usize = 20;
const ORDER_FIX: usize = 30;
const ORDER_FETCH: usize = 40;
const ORDER_LIB: usize = 50;
const ORDER_JVM: usize = 60;
const ORDER_IDENTITY: usize = 70;
const ORDER_AUTH: usize = 80;
#[derive(Debug, Parser)]
#[command(name = "portablemc", author, disable_help_subcommand = true, max_term_width = 140)]
#[command(version = VERSION)]
#[command(long_version = VERSION_LONG)]
pub struct CliArgs {
#[command(subcommand)]
pub cmd: CliCmd,
#[arg(short, global = true, env = "PMC_VERBOSE", action = clap::ArgAction::Count, display_order = ORDER_VERBOSE)]
pub verbose: u8,
#[arg(long, global = true, env = "PMC_OUTPUT", default_value = "human", display_order = ORDER_OUTPUT)]
pub output: CliOutput,
#[arg(long, global = true, env = "PMC_MAIN_DIR", value_name = "PATH", display_order = ORDER_MAIN_DIR)]
pub main_dir: Option<PathBuf>,
#[arg(long, global = true, env = "PMC_MSA_DB_FILE", value_name = "PATH", display_order = ORDER_MSA)]
pub msa_db_file: Option<PathBuf>,
#[arg(long, global = true, env = "PMC_MSA_AZURE_APP_ID", value_name = "APP_ID", display_order = ORDER_MSA)]
pub msa_azure_app_id: Option<String>,
}
#[derive(Debug, Subcommand)]
pub enum CliCmd {
Start(StartArgs),
Search(SearchArgs),
Auth(AuthArgs),
Gen(GenArgs),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CliOutput {
Human,
Machine,
}
#[derive(Debug, Args)]
pub struct StartArgs {
#[arg(default_value = "release")]
pub version: StartVersion,
#[arg(long, display_order = ORDER_COMMON)]
pub dry: bool,
#[arg(long, env = "PMC_MC_DIR", value_name = "PATH", display_order = ORDER_MC_DIR)]
pub mc_dir: Option<PathBuf>,
#[arg(long, env = "PMC_BIN_DIR", value_name = "PATH", display_order = ORDER_BIN_DIR)]
pub bin_dir: Option<PathBuf>,
#[arg(long, display_order = ORDER_SWITCH)]
pub disable_multiplayer: bool,
#[arg(long, display_order = ORDER_SWITCH)]
pub disable_chat: bool,
#[arg(long, display_order = ORDER_SWITCH)]
pub demo: bool,
#[arg(long, display_order = ORDER_SWITCH)]
pub resolution: Option<StartResolution>,
#[arg(long, display_order = ORDER_FIX)]
pub no_fix_legacy_quick_play: bool,
#[arg(long, display_order = ORDER_FIX)]
pub no_fix_legacy_proxy: bool,
#[arg(long, display_order = ORDER_FIX)]
pub no_fix_legacy_merge_sort: bool,
#[arg(long, display_order = ORDER_FIX)]
pub no_fix_legacy_resolution: bool,
#[arg(long, display_order = ORDER_FIX)]
pub no_fix_broken_authlib: bool,
#[arg(long, value_name = "VERSION", display_order = ORDER_FIX)]
pub fix_lwjgl: Option<String>,
#[arg(long, value_name = "VERSION", display_order = ORDER_FETCH)]
pub fetch_exclude: Vec<String>,
#[arg(long, conflicts_with = "fetch_exclude", display_order = ORDER_FETCH)]
pub fetch_exclude_all: bool,
#[arg(long, value_name = "FILTER", display_order = ORDER_LIB)]
pub exclude_lib: Vec<StartExcludeLibPattern>,
#[arg(long, value_name = "PATH", display_order = ORDER_LIB)]
pub include_natives: Vec<PathBuf>,
#[arg(long, value_name = "PATH", display_order = ORDER_LIB)]
pub include_class: Vec<PathBuf>,
#[arg(long, value_name = "PATH", display_order = ORDER_JVM)]
pub jvm: Option<String>,
#[arg(long, value_name = "POLICY", conflicts_with = "jvm", default_value = "system-then-mojang", display_order = ORDER_JVM)]
pub jvm_policy: StartJvmPolicy,
#[arg(long, value_name = "ARG", value_delimiter(','), display_order = ORDER_JVM)]
pub jvm_arg: Vec<String>,
#[arg(long, value_name = "WORLD_NAME", conflicts_with = "join_server", conflicts_with = "join_realms", display_order = ORDER_SWITCH)]
pub join_world: Option<String>,
#[arg(long, value_name = "HOST", conflicts_with = "join_world", conflicts_with = "join_realms", display_order = ORDER_SWITCH)]
pub join_server: Option<String>,
#[arg(long, value_name = "PORT", requires = "join_server", default_value_t = 25565, display_order = ORDER_SWITCH)]
pub join_server_port: u16,
#[arg(long, value_name = "ID", conflicts_with = "join_server", conflicts_with = "join_world", display_order = ORDER_SWITCH)]
pub join_realms: Option<String>,
#[arg(short = 'u', long, value_name = "NAME", display_order = ORDER_IDENTITY)]
pub username: Option<String>,
#[arg(short = 'i', long, display_order = ORDER_IDENTITY)]
pub uuid: Option<Uuid>,
#[arg(short = 'a', long, display_order = ORDER_AUTH)]
pub auth: bool,
}
#[derive(Debug, Clone)]
pub enum StartVersion {
Mojang {
version: String,
},
MojangRelease,
MojangSnapshot,
Fabric {
loader: fabric::Loader,
game_version: fabric::GameVersion,
loader_version: fabric::LoaderVersion,
},
Forge {
loader: forge::Loader,
version: String,
},
ForgeLatest {
loader: forge::Loader,
game_version: Option<String>, stable: bool,
}
}
impl FromStr for StartVersion {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (kind, rest) = s.split_once(':')
.unwrap_or(("mojang", s));
let parts = rest.split(':').collect::<Vec<_>>();
debug_assert!(!parts.is_empty());
let max_parts = match kind {
"raw" => 1,
"mojang" => 1,
"fabric" | "quilt" | "legacyfabric" | "babric" => 2,
"forge" | "neoforge" => 2,
_ => return Err(format!("unknown installer kind: {kind}")),
};
if parts.len() > max_parts {
return Err(format!("too much parameters for this installer kind"));
}
let version = match kind {
"mojang" => {
match parts[0] {
"" |
"release" => Self::MojangRelease { },
"snapshot" => Self::MojangSnapshot { },
version => Self::Mojang { version: version.to_string() },
}
}
"fabric" | "quilt" | "legacyfabric" | "babric" => {
Self::Fabric {
loader: match kind {
"fabric" => fabric::Loader::Fabric,
"quilt" => fabric::Loader::Quilt,
"legacyfabric" => fabric::Loader::LegacyFabric,
"babric" => fabric::Loader::Babric,
_ => unreachable!(),
},
game_version: match parts[0] {
"" |
"stable" => fabric::GameVersion::Stable,
"unstable" => fabric::GameVersion::Unstable,
id => fabric::GameVersion::Name(id.to_string()),
},
loader_version: match parts.get(1).copied() {
None | Some("" | "stable") => fabric::LoaderVersion::Stable,
Some("unstable") => fabric::LoaderVersion::Unstable,
Some(id) => fabric::LoaderVersion::Name(id.to_string()),
},
}
}
"forge" | "neoforge" => {
let loader = match kind {
"forge" => forge::Loader::Forge,
"neoforge" => forge::Loader::NeoForge,
_ => unreachable!(),
};
match parts.get(1).copied() {
None |
Some("" | "stable" | "unstable") => {
Self::ForgeLatest {
loader,
game_version: match parts[0] {
"" | "release" => None,
id => Some(id.to_string()),
},
stable: match parts.get(1).copied() {
None | Some("" | "stable") => true,
Some("unstable") => false,
_ => unreachable!(),
},
}
}
Some(other) => {
if !parts[0].is_empty() {
return Err(format!("first parameter should be empty when specifying full loader version"));
}
Self::Forge {
loader,
version: other.to_string(),
}
}
}
}
_ => unreachable!()
};
Ok(version)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum StartJvmPolicy {
System,
Mojang,
SystemThenMojang,
MojangThenSystem,
}
#[derive(Debug, Clone, Copy)]
pub struct StartResolution {
pub width: u16,
pub height: u16,
}
impl FromStr for StartResolution {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((width, height)) = s.split_once('x') else {
return Err(format!("invalid resolution syntax, expecting <width>x<height>"))
};
Ok(Self {
width: width.parse().map_err(|e| format!("invalid resolution width: {e}"))?,
height: height.parse().map_err(|e| format!("invalid resolution height: {e}"))?,
})
}
}
#[derive(Debug, Clone)]
pub struct StartExcludeLibPattern(Gav);
impl FromStr for StartExcludeLibPattern {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Gav::from_str(s)
.map_err(|()| format!("invalid exclude lib pattern, expected <group>:<artifact>:<version>[:<classifier>][@<extension>]"))
.map(Self)
}
}
impl StartExcludeLibPattern {
#[inline]
pub fn inner(&self) -> &Gav {
&self.0
}
pub fn matches(&self, gav: &Gav) -> bool {
fn match_wildcard(pattern: &str, mut haystack: &str) -> bool {
let Some((left, right)) = pattern.split_once('*') else {
return pattern == haystack;
};
if left.is_empty() && right.is_empty() {
return true; }
if !left.is_empty() {
if haystack.starts_with(left) {
haystack = &haystack[left.len()..];
} else {
return false;
}
}
right.is_empty() || haystack.ends_with(right)
}
if !match_wildcard(self.0.group(), gav.group()) {
return false;
}
if !match_wildcard(self.0.artifact(), gav.artifact()) {
return false;
}
if !match_wildcard(self.0.version(), gav.version()) {
return false;
}
if !match_wildcard(self.0.extension(), gav.extension()) {
return false;
}
match (self.0.classifier(), gav.classifier()) {
(Some(pattern), Some(haystack)) if !match_wildcard(pattern, haystack) => return false,
(Some(_), None) |
(None, Some(_)) => return false,
_ => (),
}
true
}
}
#[derive(Debug, Args)]
pub struct SearchArgs {
pub filter: Vec<String>,
#[arg(short, long, default_value = "mojang", display_order = ORDER_COMMON)]
pub kind: SearchKind,
#[arg(short, long, default_value_t = usize::MAX, hide_default_value = true, display_order = ORDER_COMMON)]
pub limit: usize,
#[arg(long, display_order = ORDER_COMMON + 1)]
pub channel: Vec<SearchChannel>,
#[arg(long, conflicts_with_all = ["filter", "channel"], display_order = ORDER_COMMON + 2)]
pub latest: Option<SearchLatestChannel>,
#[arg(long, display_order = ORDER_COMMON + 3)]
pub game_version: Vec<String>,
}
impl SearchArgs {
pub fn match_filter(&self, haystack: &str) -> bool {
self.filter.is_empty() || self.filter.iter().any(|s| haystack.contains(s))
}
pub fn match_channel(&self, channel: SearchChannel) -> bool {
self.channel.is_empty() || self.channel.contains(&channel)
}
pub fn match_game_version(&self, game_version: &str) -> bool {
self.game_version.is_empty() || self.game_version.iter().any(|v| v == game_version)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum SearchKind {
Mojang,
Local,
Fabric,
FabricGame,
Quilt,
QuiltGame,
Legacyfabric,
LegacyfabricGame,
Babric,
BabricGame,
Forge,
#[value(name = "neoforge")]
NeoForge,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SearchChannel {
Release,
Snapshot,
Beta,
Alpha,
Stable,
Unstable,
}
impl SearchChannel {
pub fn new_stable_or_unstable(stable: bool) -> Self {
if stable {
SearchChannel::Stable
} else {
SearchChannel::Unstable
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SearchLatestChannel {
Release,
Snapshot,
}
#[derive(Debug, Args)]
pub struct AuthArgs {
#[command(subcommand)]
pub cmd: AuthCmd,
}
#[derive(Debug, Subcommand)]
pub enum AuthCmd {
Login(AuthLoginArgs),
List(AuthListArgs),
Refresh(AuthRefreshArgs),
Forget(AuthForgetArgs),
}
#[derive(Debug, Args)]
pub struct AuthLoginArgs {
#[arg(long, display_order = ORDER_COMMON)]
pub no_browser: bool,
}
#[derive(Debug, Args)]
pub struct AuthListArgs { }
#[derive(Debug, Args)]
pub struct AuthRefreshArgs {
pub account: String,
}
#[derive(Debug, Args)]
pub struct AuthForgetArgs {
pub account: String,
}
#[derive(Debug, Args)]
#[command(hide = true)]
pub struct GenArgs {
#[command(subcommand)]
pub cmd: GenCmd,
}
#[derive(Debug, Subcommand)]
pub enum GenCmd {
Man(GenManArgs),
}
#[derive(Debug, Args)]
pub struct GenManArgs {
pub dir: PathBuf,
}