portablemc-cli 5.0.3

Cross platform command line utility for launching Minecraft quickly and reliably with included support for Mojang versions and popular mod loaders.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
//! Implementation of the command line parser, using clap struct derivation.

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;


// ================= //
//    MAIN COMMAND   //
// ================= //

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;

/// Command line utility for launching Minecraft quickly and reliably with included 
/// support for Mojang versions and popular mod loaders.
#[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,
    /// Verbose output, use multiple flag to increase verbosity, like -vv.
    #[arg(short, global = true, env = "PMC_VERBOSE", action = clap::ArgAction::Count, display_order = ORDER_VERBOSE)]
    pub verbose: u8,
    /// Change the output format on stdout.
    #[arg(long, global = true, env = "PMC_OUTPUT", default_value = "human", display_order = ORDER_OUTPUT)]
    pub output: CliOutput,
    /// Set the directory where versions, libraries, assets, JVM and where the game's run.
    /// 
    /// If left unspecified, this argument defaults to the standard Minecraft directory
    /// for your system: in '%USERPROFILE%/AppData/Roaming' on Windows, 
    /// '$HOME/Library/Application Support/minecraft' on macOS and '$HOME/.minecraft' on
    /// other systems. If the launcher fails to find the default directory then it will
    /// abort any command exit with a failure telling you to specify it.
    /// 
    /// This argument might not always be used by a command, you can specify it through
    /// environment variables if more practical.
    #[arg(long, global = true, env = "PMC_MAIN_DIR", value_name = "PATH", display_order = ORDER_MAIN_DIR)]
    pub main_dir: Option<PathBuf>,
    /// Path to the Microsoft Authentication database for caching session tokens.
    /// 
    /// When unspecified, this argument is derived from the '--main-dir' path: 
    /// '<main-dir>/portablemc_msa.json'. This file uses a JSON human-readable format.
    /// 
    /// This argument might not always be used by a command, you can specify it through
    /// environment variables if more practical.
    #[arg(long, global = true, env = "PMC_MSA_DB_FILE", value_name = "PATH", display_order = ORDER_MSA)]
    pub msa_db_file: Option<PathBuf>,
    /// Change the default Azure application ID used by the launcher.
    /// 
    /// The Azure application ID is used for interacting with the Microsoft authentication
    /// API used for authentication of Minecraft accounts. When not specified, the default
    /// (and hidden) launcher's application ID is used.
    #[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 readable output, it depends on the actual command being used and is not
    /// guaranteed to be stable across releases, for that you should prefer using
    /// 'tabular' output for example. With this format, the verbosity is used to
    /// show more informative data.
    Human,
    /// Machine output mode to allow parsing by other programs, using tab ('\t', 0x09) 
    /// separated values where the first value defines which kind of data to follow on 
    /// the line, a line return ('\n', 0x0A) is used to split every line. If any line 
    /// return or tab is encoded into a value within the line, it is escaped with the 
    /// two characters '\n' (for line return) or '\t' (for tab), these are the only two
    /// escapes used. This mode is always verbose, and verbosity will not have any effect 
    /// on it. If the launcher exit with a failure code, you should expect finding a log 
    /// message prefixed with `error_`, describing the error(s) causing the exit.
    Machine,
}

// ================= //
//   START COMMAND   //
// ================= //

/// Start the game.
/// 
/// This command is the main entrypoint for installing and then launching the game,
/// it works with many different versions, this includes official Mojang versions 
/// but also popular mod loaders, such as Fabric, Quilt, Forge, NeoForge and 
/// LegacyFabric. It ensures that the version is properly installed prior to launching
/// it.
#[derive(Debug, Args)]
pub struct StartArgs {
    /// The version to launch (see more with '--help').
    /// 
    /// You can provide this argument with colon-separated ':' syntax, in such case the
    /// first part defines the kind of installer, supported values are: mojang, fabric,
    /// quilt, forge, neoforge, legacyfabric and babric. 
    /// When not using the colon-separated syntax, this will defaults to the 'mojang' 
    /// installer. Below are detailed each installer.
    /// 
    /// - mojang:[release|snapshot|<version>] => use 'release' (default if absent) or 
    /// 'snapshot' to install and launch the latest version of that type, or you can 
    /// use any valid version id provided by Mojang (you can search for them using the 
    /// 'portablemc search' command). 
    /// This also supports any local version that is already installed, with support 
    /// for inheriting other versions: a generic rule of the Mojang installer is that
    /// each version in the hierarchy that is known by the Mojang's version manifest 
    /// will be checked for validity (file hash) and fetched if needed.
    /// You can manually exclude versions from this rule using '--exclude-fetch' with
    /// each version you don't want to fetch (see this argument's help). The Mojang's
    /// version manifest is only accessed for resolving 'release', 'snapshot' or a
    /// non-excluded version, if it's not yet cached this will require internet.
    /// 
    /// - fabric:[<game-version>[:[<loader-version>]]] => install and launch a given 
    /// Mojang version with the Fabric mod loader. Both versions can be omitted (empty) 
    /// to use the latest stable versions available, but you can also manually specify 
    /// 'stable' or 'unstable', which are equivalent as Mojang release and snapshot
    /// but at the discretion of the Fabric API. If the version is not yet installed, 
    /// it will requires internet to access the Fabric API. See https://fabricmc.net/.
    /// 
    /// - quilt:[<game-version>[:[<loader-version>]]] => same as 'fabric' installer, 
    /// but using the Quilt API for missing versions. See https://quiltmc.org/.
    /// 
    /// - legacyfabric:[<game-version>[:[<loader-version>]]] => same as 'fabric',
    /// but using the LegacyFabric API for missing versions. This installer can be
    /// used for using the Fabric mod loader on Mojang versions prior to 1.14. 
    /// See https://legacyfabric.net/.
    /// 
    /// - babric:[:[<loader-version>]] => same as 'fabric', but using the Babric API 
    /// for missing versions. This mod loader is specifically made to support Fabric
    /// only on Mojang b1.7.3, so it's useless to specify the game version like 
    /// other Fabric-like loaders, both 'stable' and 'unstable' would be equivalent 
    /// to 'b1.7.3'. See https://babric.github.io/.
    /// 
    /// - forge::<loader-version> | forge:[<game-version>][:stable|unstable] => the 
    /// syntax is a bit cumbersome because you can either specify the full loader version
    /// such as '1.21.4-54.0.12' but you must leave the first parameter empty, or you 
    /// can specify a Mojang game version with optional second parameter that specifies
    /// if you target the latest 'stable' (default) or 'unstable' loader version.
    /// See https://minecraftforge.net/.
    /// 
    /// - neoforge::<loader-version> | neoforge:[<game-version>][:stable|unstable] => same
    /// as 'forge', but using the NeoForge repository. See https://neoforged.net/.
    #[arg(default_value = "release")]
    pub version: StartVersion,
    /// Only ensures that the game is installed but don't launch the game. 
    /// 
    /// This can be used to debug installation paths while using verbose output.
    #[arg(long, display_order = ORDER_COMMON)]
    pub dry: bool,
    /// Set the directory where the game is run from
    /// 
    /// The game will use this directory to put options, saves, screenshots and access 
    /// texture or resource packs and any other user related stuff.
    /// 
    /// When unspecified, this argument is equal to the '--main-dir' path.
    #[arg(long, env = "PMC_MC_DIR", value_name = "PATH", display_order = ORDER_MC_DIR)]
    pub mc_dir: Option<PathBuf>,
    /// Set the binaries directory where binary objects are extracted before running.
    /// 
    /// A sub-directory is created inside this directory that is uniquely named after a 
    /// hash of the version's libraries.
    /// 
    /// When unspecified, this argument is derived from the '--mc-dir' path: 
    /// '<main-dir>/bin/'.
    #[arg(long, env = "PMC_BIN_DIR", value_name = "PATH", display_order = ORDER_BIN_DIR)]
    pub bin_dir: Option<PathBuf>,
    /// Disable the multiplayer buttons (>= 1.16).
    #[arg(long, display_order = ORDER_SWITCH)]
    pub disable_multiplayer: bool,
    /// Disable the online chat (>= 1.16).
    #[arg(long, display_order = ORDER_SWITCH)]
    pub disable_chat: bool,
    /// Enable demo mode for the game.
    #[arg(long, display_order = ORDER_SWITCH)]
    pub demo: bool,
    /// Change the resolution of the game window (<width>x<height>, >= 1.6).
    #[arg(long, display_order = ORDER_SWITCH)]
    pub resolution: Option<StartResolution>,
    /// Disable the legacy quick play fix for older versions without Quick Play support.
    /// 
    /// When starting versions older than 1.20 (23w14a) where Quick Play was not supported
    /// by the client, this fix tries to use legacy arguments instead, such as --server
    /// and --port, this is enabled by default.
    #[arg(long, display_order = ORDER_FIX)]
    pub no_fix_legacy_quick_play: bool,
    /// Disable the legacy proxy fix to old online resources.
    /// 
    /// When starting older alpha, beta and release up to 1.5, this allows legacy online
    /// resources such as skins to be properly requested. The implementation is currently 
    /// using `betacraft.uk` proxies, this is enabled by default.
    #[arg(long, display_order = ORDER_FIX)]
    pub no_fix_legacy_proxy: bool,
    /// Disable the legacy merge sort fix on really old versions.
    /// 
    /// When starting older alpha and beta versions, this adds a JVM argument to use the
    /// legacy merge sort `java.util.Arrays.useLegacyMergeSort=true`, this is required on
    /// some old versions to avoid crashes, this is enabled by default.
    #[arg(long, display_order = ORDER_FIX)]
    pub no_fix_legacy_merge_sort: bool,
    /// Disable the legacy resolution fix on older versions without resolution arguments.
    /// 
    /// When starting older versions that don't support modern resolution arguments, this
    /// fix will add arguments to force resolution of the initial window, this is enabled 
    /// by default.
    #[arg(long, display_order = ORDER_FIX)]
    pub no_fix_legacy_resolution: bool,
    /// Disable the broken AuthLib fix on 1.16.4 and 1.16.5.
    /// 
    /// Versions 1.16.4 and 1.16.5 uses authlib:2.1.28 which cause multiplayer button
    /// (and probably in-game chat) to be disabled, this can be fixed by switching to
    /// version 2.2.30 of authlib, this is enabled by default.
    #[arg(long, display_order = ORDER_FIX)]
    pub no_fix_broken_authlib: bool,
    /// Change the LWJGL version used by the game (LWJGL >= 3.2.3).
    /// 
    /// This argument will cause all LWJGL libraries of the game to be changed to the
    /// given version, this applies to natives as well. In addition to simply changing
    /// the versions, this will also add natives that are missing, such as ARM.
    /// 
    /// It's not guaranteed to work with every version of Minecraft and downgrading 
    /// LWJGL version is not recommended.
    #[arg(long, value_name = "VERSION", display_order = ORDER_FIX)]
    pub fix_lwjgl: Option<String>,
    /// Exclude the given version from validity check and fetching.
    /// 
    /// This is used by the Mojang installer and all installers relying on it to exclude
    /// version from being validated and fetched from the Mojang's version manifest, as
    /// described in 'VERSION' help. You can use --fetch-exclude-all to exclude all 
    /// versions and therefore prevent any fetching of the Mojang's manifest.
    /// 
    /// This argument can be specified multiple times.
    #[arg(long, value_name = "VERSION", display_order = ORDER_FETCH)]
    pub fetch_exclude: Vec<String>,
    /// Exclude all versions from validity check and fetching.
    /// 
    /// See --fetch-exclude, note that this is incompatible with --fetch-exclude.
    #[arg(long, conflicts_with = "fetch_exclude", display_order = ORDER_FETCH)]
    pub fetch_exclude_all: bool,
    /// Use a filter to exclude Java libraries from the installation.
    /// 
    /// The filter is checked against each GAV (Group-Artifact-Version) of each library
    /// resolved in the version metadata and remove each library matching the filter.
    /// It's using the following syntax, this is almost the same as a standard GAV but
    /// it allows having an asterisk '*' as a placeholder for any of the 'group', 
    /// 'artifact' or 'version': <group>:<artifact>:<version>[:<classifier>][@<extension>].
    /// 
    /// A typical use case for this argument would be to exclude some natives-providing
    /// library (such as LWJGL libraries with 'natives' classifier) and then provide 
    /// those natives manually using '--include-bin' argument. Known usage of this 
    /// argument has been for supporting MUSL-only systems, because LWJGL binaries are
    /// only provided for glibc (see #110 and #112 on GitHub).
    /// 
    /// This argument can be specified multiple times.
    #[arg(long, value_name = "FILTER", display_order = ORDER_LIB)]
    pub exclude_lib: Vec<StartExcludeLibPattern>,
    /// Include a natives file in the binaries directory, usually shared objects or 
    /// archives.
    /// 
    /// When an archive is specified (ZIP or JAR), the shared objects inside it are used.
    /// Those files are symlinked (or copied if not possible) to the binaries directory 
    /// where the game will check for natives to load. The main use case is for including
    /// shared objects (.so, .dll, .dylib), in case of versioned .so files like we can
    /// see on UNIX systems, the version is discarded when linked or copied to the bin
    /// directory (/usr/lib/foo.so.1.22.2 -> foo.so).
    /// 
    /// Read the help message of '--exclude-lib' for a typical use case.
    /// 
    /// This argument can be specified multiple times.
    #[arg(long, value_name = "PATH", display_order = ORDER_LIB)]
    pub include_natives: Vec<PathBuf>,
    /// Include a class file in the class path of the launching game, this should usually
    /// be a JAR archive.
    /// 
    /// This argument can be specified multiple times.
    #[arg(long, value_name = "PATH", display_order = ORDER_LIB)]
    pub include_class: Vec<PathBuf>,
    /// The path to the JVM executable, 'java' (or 'javaw.exe' on Windows).
    /// 
    /// This is used to launch the game, it has a special use-case with Forge and NeoForge
    /// loader versions where that JVM executable is also used to run the installer 
    /// processors.
    /// 
    /// Note that when this argument is specified, you cannot specify the '--jvm-policy'
    /// argument.
    #[arg(long, value_name = "PATH", display_order = ORDER_JVM)]
    pub jvm: Option<String>,
    /// The policy for finding or installing the JVM executable.
    #[arg(long, value_name = "POLICY", conflicts_with = "jvm", default_value = "system-then-mojang", display_order = ORDER_JVM)]
    pub jvm_policy: StartJvmPolicy,
    /// Add more arguments to the JVM command line.
    /// 
    /// You can specify multiple arguments after the '--jvm-arg' option, using commas ',',
    /// for example 'start --jvm-arg=-Xms256m,-Xmx2048m'.
    /// 
    /// Most of the time you should prefer using the '--jvm-arg=' form because JVM 
    /// arguments also starts with a dash, which would be ambiguous if using 
    /// '--jvm-arg -X...' (NOT WORKING) for example.
    #[arg(long, value_name = "ARG", value_delimiter(','), display_order = ORDER_JVM)]
    pub jvm_arg: Vec<String>,
    /// Automatically join the given singleplayer world after game has been launched.
    /// 
    /// Note that this may not work on older version that did not support the "Quick Play"
    /// feature. 
    /// 
    /// This is incompatible with other Quick Play modes.
    #[arg(long, value_name = "WORLD_NAME", conflicts_with = "join_server", conflicts_with = "join_realms", display_order = ORDER_SWITCH)]
    pub join_world: Option<String>,
    /// Automatically join the given server after game has been launched.
    /// 
    /// Note that this may not work on older version that did not support the "Quick Play"
    /// feature nor the legacy game's `--server` argument.
    /// 
    /// This is incompatible with other Quick Play modes.
    #[arg(long, value_name = "HOST", conflicts_with = "join_world", conflicts_with = "join_realms", display_order = ORDER_SWITCH)]
    pub join_server: Option<String>,
    /// Complement to the `--join-server` argument to specify the server port.
    #[arg(long, value_name = "PORT", requires = "join_server", default_value_t = 25565, display_order = ORDER_SWITCH)]
    pub join_server_port: u16,
    /// Automatically join a Realms server from its id after game has been launched.
    /// 
    /// Note that this may not work on older version that did not support the "Quick Play"
    /// feature.
    /// 
    /// This is incompatible with other Quick Play modes.
    #[arg(long, value_name = "ID", conflicts_with = "join_server", conflicts_with = "join_world", display_order = ORDER_SWITCH)]
    pub join_realms: Option<String>,
    /// Change the default username of the player.
    /// 
    /// When the '--auth' (-a) flag is enabled, this argument is used, after the 
    /// '--uuid' (-i) one, to find the authenticated account to start the game with.
    #[arg(short = 'u', long, value_name = "NAME", display_order = ORDER_IDENTITY)]
    pub username: Option<String>,
    /// Change the default UUID of the player.
    /// 
    /// When the '--auth' (-a) flag is enabled, this argument is used, before the 
    /// '--username' (-u) one, to find the authenticated account to start the game with.
    #[arg(short = 'i', long, display_order = ORDER_IDENTITY)]
    pub uuid: Option<Uuid>,
    /// Enable authentication for the username or UUID.
    /// 
    /// When enabled, the launcher will look for specified '--uuid', or '--username' as
    /// a fallback, it will then pick the matching account and start the game with it,
    /// the account is refreshed if needed. IT MEANS that you must first login into
    /// your account using the 'portablemc auth login' command before starting the game 
    /// with the account.
    /// 
    /// If the account is not found, the launcher won't start the game and will show an
    /// error.
    /// 
    /// Note that '--username' (-u) argument is completely ignored if the '--uuid' (-i)
    /// is specified, only one of them can be used at the same time with this flag. 
    /// You can combine this flag with one of these argument, for example '-au <username>'
    /// or '-ai <uuid>'.
    #[arg(short = 'a', long, display_order = ORDER_AUTH)]
    pub auth: bool,
}

/// Represent all possible version the launcher can start.
#[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>,  // None for targeting "release"
        stable: bool,
    }
}

impl FromStr for StartVersion {

    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        
        // Extract the kind (defaults to mojang) and the parameters.
        let (kind, rest) = s.split_once(':')
            .unwrap_or(("mojang", s));

        // Then split the rest into all parts.
        let parts = rest.split(':').collect::<Vec<_>>();
        debug_assert!(!parts.is_empty());

        // Compute max parts count and immediately discard 
        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 {
    /// The installer will try to find a suitable JVM executable in the path, searching
    /// a `java` (or `javaw.exe` on Windows) executable. On operating systems where it's
    /// supported, this will also check for known directories (on Arch for example).
    /// If the version needs a specific JVM major version, each candidate executable is 
    /// checked and a warning is triggered to notify that the version is not suited.
    /// The install fails if none of those versions is valid.
    System,
    /// The installer will try to find a suitable JVM to install from Mojang-provided
    /// distributions, if no JVM is available for the platform and for the required 
    /// distribution then the install fails.
    Mojang,
    /// The installer search system and then mojang as a fallback.
    SystemThenMojang,
    /// The installer search Mojang and then system as a fallback.
    MojangThenSystem,
}

/// Represent an optional initial resolution for the game window.
#[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}"))?,
        })

    }

}

/// Represent a pattern for excluding library.
#[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
    }

    /// Return true if that pattern matches the given GAV.
    pub fn matches(&self, gav: &Gav) -> bool {

        /// Internal function to match a haystack against a pattern that may contain an
        /// asterisk '*' that allows wildcard matching. Only the first asterisk is used.
        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;  // Match everything
            }

            if !left.is_empty() {
                if haystack.starts_with(left) {
                    // Strip of the left part from the haystack.
                    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

    }

}

// ================= //
//  SEARCH COMMAND   //
// ================= //

/// Search for versions.
/// 
/// By default this command will search for official Mojang version but you can change 
/// this behavior and search for local or mod loaders versions with the -k (--kind) 
/// argument. Note that the displayed table layout depends on the kind. How the
/// query string is interpreted depends on the kind.
#[derive(Debug, Args)]
pub struct SearchArgs {
    /// The search filter string.
    /// 
    /// You can give multiple filters that will apply to various texts depending on the 
    /// search king. In general this will apply to the leftmost column, so the version
    /// name in most of the cases.
    pub filter: Vec<String>,
    /// Select the target of the search query.
    #[arg(short, long, default_value = "mojang", display_order = ORDER_COMMON)]
    pub kind: SearchKind,
    /// Limit the number of rows of results.
    /// 
    /// Because search results are sorted by descending versions, this will keep only the
    /// given number of most recent versions. One exception to this are local versions,
    /// which are in the same order as your OS give them when listing their directories.
    #[arg(short, long, default_value_t = usize::MAX, hide_default_value = true, display_order = ORDER_COMMON)]
    pub limit: usize,
    /// Only keep versions of given channel.
    /// 
    /// This argument can be given multiple times to specify multiple channels to match
    /// in an OR logic.
    /// 
    /// [supported search kinds: mojang, forge, neoforge]
    #[arg(long, display_order = ORDER_COMMON + 1)]
    pub channel: Vec<SearchChannel>,
    /// Only show the latest version of the given channel.
    /// 
    /// This argument can be specified only once and is incompatible with any other
    /// filters, it also don't work with any channel, some channels have no information 
    /// about their "latest" version as it doesn't make sense, like the latest Mojang's 
    /// beta was released 13 years ago, this cannot be described as the "latest" version
    /// of the game.
    /// 
    /// [supported search kinds: mojang]
    #[arg(long, conflicts_with_all = ["filter", "channel"], display_order = ORDER_COMMON + 2)]
    pub latest: Option<SearchLatestChannel>,
    /// Only keep loader versions that targets the given game version. 
    /// 
    /// [supported search kinds: forge, neoforge]
    #[arg(long, display_order = ORDER_COMMON + 3)]
    pub game_version: Vec<String>,
}

impl SearchArgs {

    /// Return true if the given haystack contains one of the string filters. Return true
    /// if no string filter.
    pub fn match_filter(&self, haystack: &str) -> bool {
        self.filter.is_empty() || self.filter.iter().any(|s| haystack.contains(s))
    }

    /// Return true if the given search channel is selected. Return true if no filter.
    pub fn match_channel(&self, channel: SearchChannel) -> bool {
        self.channel.is_empty() || self.channel.contains(&channel)
    }

    /// Return true if the given game version is present, exactly, in one of the filter.
    /// Return true if no filter.
    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 {
    /// Search for official versions released by Mojang, including release and snapshots.
    Mojang,
    /// Search for locally installed versions, located in the versions directory.
    Local,
    /// Search for Fabric loader versions.
    Fabric,
    /// Search for Fabric supported game versions.
    FabricGame,
    /// Search for Quilt loader versions.
    Quilt,
    /// Search for Quilt supported game versions.
    QuiltGame,
    /// Search for LegacyFabric loader versions.
    Legacyfabric,
    /// Search for LegacyFabric supported game versions.
    LegacyfabricGame,
    /// Search for Babric loader versions.
    Babric,
    /// Search for Babric supported game versions.
    BabricGame,
    /// Search for Forge loader versions.
    Forge,
    /// Search for NeoForge loader versions.
    #[value(name = "neoforge")]
    NeoForge,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SearchChannel {
    /// Filter versions by release channel (only for mojang).
    Release,
    /// Filter versions by snapshot channel (only for mojang).
    Snapshot,
    /// Filter versions by beta channel (only for mojang).
    Beta,
    /// Filter versions by alpha channel (only for mojang).
    Alpha,
    /// Filter versions by stable channel (only for mod loaders).
    Stable,
    /// Filter versions by unstable channel (only for mod loaders).
    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 {
    /// Select the latest release version.
    Release,
    /// Select the latest snapshot version.
    Snapshot,
}

// ================= //
//   AUTH COMMAND    //
// ================= //

/// Manage the authentication sessions.
/// 
/// By default, this command will start a new authentication flow with the Microsoft
/// authentication service, when completed this will add the newly authenticated session
/// to the authentication database (specified with '--msa-db-file' argument).
/// 
/// If this command fails to load and/or store the database, its exit code is 1 (failure).
#[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),
}

/// Login and register a new authenticated session.
#[derive(Debug, Args)]
pub struct AuthLoginArgs {
    /// Prevent the launcher from opening your system's web browser with the 
    /// authentication page.
    /// 
    /// When the '--output' mode is 'human', the launcher will try to open your system's
    /// web browser with the Microsoft authentication page, this flag disables this 
    /// behavior.
    #[arg(long, display_order = ORDER_COMMON)]
    pub no_browser: bool,
}

/// List all currently authenticated sessions.
/// 
/// By username and UUID, that can be used with the start command to authenticate.
#[derive(Debug, Args)]
pub struct AuthListArgs { }

/// Refresh an authenticated session.
/// 
/// This updates the username if it has been modified.
/// 
/// Note that this procedure is automatically done on game's start, so you don't need 
/// to run this before starting the game with an account. You may want to use this 
/// in order to update the database and list the updated accounts.
#[derive(Debug, Args)]
pub struct AuthRefreshArgs {
    /// The UUID of the account or the username as a fallback.
    pub account: String,
}

/// Forget an authenticated session.
/// 
/// You'll no longer be able to authenticate with this session when starting the
/// game, you'll have to authenticate again. If not account is matching the given
/// UUID or username, then the database is not rewritten, and a warning message is
/// issued, but the exit code is always 0 (success).
#[derive(Debug, Args)]
pub struct AuthForgetArgs {
    /// The UUID of the account or the username as a fallback.
    pub account: String,
}

// ================= //
//    GEN COMMAND    //
// ================= //

/// Internal tool to generate derived resources from this tool.
/// 
/// This subcommand is currently intentionally hidden and unstable, made for distributors.
#[derive(Debug, Args)]
#[command(hide = true)]
pub struct GenArgs {
    #[command(subcommand)]
    pub cmd: GenCmd,
}

#[derive(Debug, Subcommand)]
pub enum GenCmd {
    Man(GenManArgs),
}

/// Generate a ROFF manpage from the command line.
#[derive(Debug, Args)]
pub struct GenManArgs {
    /// The directory where to generate all the manual pages.
    pub dir: PathBuf,
}