diff --git a/CHANGELOG.md b/CHANGELOG.md index fcbba0b..407e4cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,48 @@ # Changelog for Ferium -## `v4.8.0` -### - +## `v5.0.0` +### TBD +- **Features** +- **Bug Fixes** +- **Internal Changes** ## `v4.7.1` ### 17.09.2024 -- Implement change to libium, mainly removal of async functions - - Fixes [#422](https://github.com/gorilla-devs/ferium/issues/422) -- Also remove unnecessary async from code here -- Consistently use `FuturesUnordered` for parallelising code +- **Internal Changes** + - Implement changes to libium, mainly the removal of async functions + - [#422](https://github.com/gorilla-devs/ferium/issues/422) has been fixed + - Also remove unnecessary async from code here + - Consistently use `FuturesUnordered` for parallelising code ## `v4.7.0` ### 11.06.2024 -- Features +- **Features** - Scan a directory (the profile's output directory by default) to automatically add the mods - Uses a maximum of 4 network requests! Unfortunately file hashing and searching for the file on the server take some time so it's not instant, especially with a large number of mods. -- Internal Changes +- **Internal Changes** - Move code for displaying successes and failures to the `add` module ## `v4.6.0` ### 10.06.2024 -#### You can now add multiple mods at once! ([#175](https://github.com/gorilla-devs/ferium/issues/175)) +*You can now add multiple mods at once! ([#175](https://github.com/gorilla-devs/ferium/issues/175))* -- Features +- **Features** - Adding mods has been significantly improved - No matter how many mods you add, there will only be a maximum of 3 network requests (for each platform) - The `--ignore-game-version` and `--ignore-mod-loader` flags will only work when adding a single mod, since these are meant for fine-tuning -- Bug Fixes +- **Bug Fixes** - Fix `No such file or directory (os error 2)` when downloading some specific modpacks ([#402](https://github.com/gorilla-devs/ferium/issues/402)) - Fix issues with the active profile/modpack index changing when deleting profiles/modpacks - Fix `--ignore-mod-loader` not working with curseforge mods ([#417](https://github.com/gorilla-devs/ferium/issues/417)) - Fix stack overflows on the Windows GNU build ([#377](https://github.com/gorilla-devs/ferium/issues/377)) -- Internal Changes +- **Internal Changes** - Use `.eq_ignore_ascii_case()` where appropriate - Use `anyhow::ensure!()` where appropriate - Use `.ok_or_else()` where appropriate diff --git a/Cargo.lock b/Cargo.lock index 36080e9..c1847c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,9 +266,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" @@ -297,6 +297,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -372,9 +378,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.21" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0" dependencies = [ "jobserver", "libc", @@ -566,6 +572,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -603,16 +634,24 @@ dependencies = [ ] [[package]] -name = "dialoguer" -version = "0.11.0" +name = "derive_more" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "console", - "shell-words", - "tempfile", - "thiserror", - "zeroize", + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", ] [[package]] @@ -643,6 +682,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.13.0" @@ -727,9 +772,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "ferinth" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e02ddb7dbc6a683bf0e53786279a3a621977609df68eedb3311d05a8de807eb" +checksum = "052d5d4f1b538e87a25b9d092da20f176658c790f12e78bd133914d2d05a6d7c" dependencies = [ "chrono", "lazy-regex", @@ -743,19 +788,18 @@ dependencies = [ [[package]] name = "ferium" -version = "4.7.2" +version = "5.0.0" dependencies = [ "anyhow", "clap", "clap_complete", "colored", - "dialoguer", "ferinth", "fs_extra", "furse", "futures", "indicatif", - "itertools", + "inquire", "libium", "octocrab", "rand", @@ -767,9 +811,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -914,6 +958,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1204,6 +1266,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.6.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "instant" version = "0.1.13" @@ -1221,9 +1300,9 @@ checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "iri-string" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c25163201be6ded9e686703e85532f8f852ea1f92ba625cb3c51f7fe6d07a4a" +checksum = "44bd7eced44cfe2cebc674adb2a7124a754a4b5269288d22e9f39f8fada3562d" dependencies = [ "memchr", "serde", @@ -1235,15 +1314,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -1320,16 +1390,18 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libium" -version = "1.30.0" -source = "git+https://github.com/gorilla-devs/libium?rev=69e58a154be5e80e5b79586fd980a29945d1b689#69e58a154be5e80e5b79586fd980a29945d1b689" +version = "1.32.0" +source = "git+https://github.com/gorilla-devs/libium?rev=b80b16412018e11ae7adde0727eb32065a32ddc7#b80b16412018e11ae7adde0727eb32065a32ddc7" dependencies = [ "clap", - "dialoguer", + "derive_more", "ferinth", "furse", "futures-util", "home", + "inquire", "octocrab", + "regex", "reqwest", "rfd", "serde", @@ -1347,6 +1419,16 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "lockfree-object-pool" version = "0.1.6" @@ -1408,6 +1490,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.2" @@ -1426,13 +1520,22 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb585ade2549a017db2e35978b77c319214fa4b37cede841e27954dd6e8f3ca8" +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", @@ -1558,9 +1661,12 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] [[package]] name = "openssl-probe" @@ -1584,6 +1690,29 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -1682,9 +1811,9 @@ checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" [[package]] name = "portable-atomic" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" [[package]] name = "powerfmt" @@ -1812,11 +1941,20 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -1826,9 +1964,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -1837,9 +1975,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -1941,7 +2079,7 @@ version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -2001,9 +2139,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" @@ -2031,6 +2169,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "secrecy" version = "0.8.0" @@ -2046,7 +2190,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -2139,18 +2283,33 @@ dependencies = [ "digest", ] -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2256,9 +2415,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -2280,7 +2439,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] @@ -2297,9 +2456,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -2328,6 +2487,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.36" @@ -2383,7 +2552,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.2", "pin-project-lite", "signal-hook-registry", "socket2", @@ -2478,7 +2647,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bytes", "futures-util", "http", @@ -2580,12 +2749,24 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -2934,9 +3115,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index f4d55c0..9f940d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ferium" -version = "4.7.2" +version = "5.0.0" repository = "https://github.com/gorilla-devs/ferium" description = "Fast CLI program for managing Minecraft mods and modpacks from Modrinth, CurseForge, and Github Releases" authors = [ @@ -51,17 +51,16 @@ tokio = { version = "1.40", default-features = false, features = [ clap = { version = "4.5", features = ["derive"] } clap_complete = "4.5" serde_json = "1.0" -dialoguer = "0.11" indicatif = "0.17" -itertools = "0.13" octocrab = "0.40" fs_extra = "1.3" ferinth = "2.11" colored = "2.1" futures = "0.3" -libium = { git = "https://github.com/gorilla-devs/libium", rev = "69e58a154be5e80e5b79586fd980a29945d1b689" } +inquire = "0.7" +libium = { git = "https://github.com/gorilla-devs/libium", rev = "b80b16412018e11ae7adde0727eb32065a32ddc7" } # libium = { path = "../libium" } -# libium = "1.31" +# libium = "1.32" anyhow = "1.0" furse = "1.5" size = "0.4" diff --git a/src/add.rs b/src/add.rs index ca72bb2..3c3808f 100644 --- a/src/add.rs +++ b/src/add.rs @@ -1,15 +1,14 @@ use std::collections::HashMap; use colored::Colorize as _; -use itertools::Itertools as _; -use libium::add::Error; +use libium::{add::Error, iter_ext::IterExt as _}; pub fn display_successes_failures(successes: &[String], failures: Vec<(String, Error)>) -> bool { if !successes.is_empty() { println!( "{} {}", "Successfully added".green(), - successes.iter().map(|s| s.bold()).format(", ") + successes.iter().map(|s| s.bold()).display(", ") ); } @@ -40,7 +39,7 @@ pub fn display_successes_failures(successes: &[String], failures: Vec<(String, E exit_error = true; err.red() }, - ids.iter().map(|s| s.italic()).format(", ") + ids.iter().map(|s| s.italic()).display(", ") ); } diff --git a/src/cli.rs b/src/cli.rs index de9cc93..1225086 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,7 +2,7 @@ use clap::{Parser, Subcommand, ValueEnum, ValueHint}; use clap_complete::Shell; -use libium::config::structs::ModLoader; +use libium::config::{filters::ReleaseChannel, structs::ModLoader}; use std::path::PathBuf; #[derive(Parser)] @@ -33,6 +33,10 @@ pub struct Ferium { #[derive(Subcommand)] pub enum SubCommands { + /* TODO: + Use this for filter arguments: + https://docs.rs/clap/latest/clap/_derive/_tutorial/chapter_3/index.html#argument-relations + */ /// Add mods to the profile Add { /// The identifier(s) of the mod/project/repository @@ -43,17 +47,26 @@ pub enum SubCommands { /// The GitHub identifier is the repository's full name, e.g. `gorilla-devs/ferium`. #[clap(required = true)] identifiers: Vec, + /// Temporarily ignore game version and mod loader checks and add the mod anyway #[clap(long, short, visible_alias = "override")] force: bool, - /// The game version will not be checked for this mod. - /// Only works when adding a single mod. - #[clap(long, short = 'V', alias = "dont-check-game-version")] - ignore_game_version: bool, - /// The mod loader will not be checked for this mod. - /// Only works when adding a single mod. - #[clap(long, short = 'M', alias = "dont-check-mod-loader")] - ignore_mod_loader: bool, + + #[clap(long, short = 'l', group = "loader")] + mod_loader_prefer: Vec, + #[clap(long, group = "loader")] + mod_loader_any: Vec, + + #[clap(long, short = 'v', group = "version")] + game_version_strict: Vec, + #[clap(long, group = "version")] + game_version_minor: Vec, + + #[clap(long, short = 'c')] + release_channel: Option, + + #[clap(long, short = 'n')] + file_name: Option, }, /// Scan the profile's output directory (or the specified directory) for mods and add them to the profile Scan { @@ -123,13 +136,13 @@ pub enum ProfileSubCommands { /// Optionally, provide the settings to change as arguments. #[clap(visible_aliases = ["config", "conf"])] Configure { - /// The Minecraft version to check compatibility for + /// The Minecraft version(s) to consider as compatible #[clap(long, short = 'v')] - game_version: Option, - /// The mod loader to check compatibility for - #[clap(long, short)] + game_versions: Vec, + /// The mod loader(s) to consider as compatible + #[clap(long, short = 'l')] #[clap(value_enum)] - mod_loader: Option, + mod_loaders: Vec, /// The name of the profile #[clap(long, short)] name: Option, @@ -146,11 +159,11 @@ pub enum ProfileSubCommands { /// Copy over the mods from an existing profile. /// Optionally, provide the name of the profile to import mods from. #[clap(long, short, visible_aliases = ["copy", "duplicate"])] - #[allow(clippy::option_option)] + #[expect(clippy::option_option)] import: Option>, /// The Minecraft version to check compatibility for #[clap(long, short = 'v')] - game_version: Option, + game_version: Vec, /// The mod loader to check compatibility for #[clap(long, short)] #[clap(value_enum)] diff --git a/src/download.rs b/src/download.rs index 71001c1..00d8802 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,19 +1,15 @@ -// Allow `expect()`s for mutex poisons -#![allow(clippy::expect_used)] +#![expect(clippy::expect_used, reason = "For mutex poisons")] use crate::{STYLE_BYTE, TICK}; use anyhow::{anyhow, bail, Error, Result}; -use colored::Colorize; +use colored::Colorize as _; use fs_extra::{ dir::{copy as copy_dir, CopyOptions as DirCopyOptions}, file::{move_file, CopyOptions as FileCopyOptions}, }; use futures::{stream::FuturesUnordered, StreamExt as _}; use indicatif::ProgressBar; -use itertools::Itertools; -use libium::upgrade::DownloadFile; -use reqwest::Client; -use size::Size; +use libium::{iter_ext::IterExt as _, upgrade::DownloadFile}; use std::{ ffi::OsString, fs::{copy, create_dir_all, read_dir, remove_file}, @@ -43,7 +39,7 @@ pub async fn clean( dupes .into_iter() .map(|i| to_download.swap_remove(i).filename()) - .format(", ") + .display(", ") ) .yellow() .bold() @@ -118,7 +114,7 @@ pub async fn download( .enable_steady_tick(Duration::from_millis(100)); let mut tasks = FuturesUnordered::new(); let semaphore = Arc::new(Semaphore::new(75)); - let client = Arc::new(Client::new()); + let client = Arc::new(reqwest::Client::new()); for downloadable in to_download { let permit = Arc::clone(&semaphore).acquire_owned().await?; @@ -142,7 +138,7 @@ pub async fn download( .println(format!( "{} Downloaded {:>7} {}", &*TICK, - Size::from_bytes(length) + size::Size::from_bytes(length) .format() .with_base(size::Base::Base10) .to_string(), diff --git a/src/main.rs b/src/main.rs index 05224dd..94e5f65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,9 +10,10 @@ clippy::unwrap_used, clippy::expect_used, // use anyhow::Context instead clippy::correctness, + clippy::allow_attributes, )] #![warn(clippy::dbg_macro)] -#![allow(clippy::multiple_crate_versions, clippy::too_many_lines)] +#![expect(clippy::multiple_crate_versions, clippy::too_many_lines)] mod add; mod cli; @@ -23,16 +24,15 @@ use anyhow::{anyhow, bail, ensure, Result}; use clap::{CommandFactory, Parser}; use cli::{Ferium, ModpackSubCommands, ProfileSubCommands, SubCommands}; use colored::{ColoredString, Colorize}; -use dialoguer::theme::ColorfulTheme; use indicatif::ProgressStyle; -use itertools::Itertools; use libium::{ config::{ self, + filters::ProfileParameters as _, structs::{Config, ModIdentifier, Modpack, Profile}, DEFAULT_CONFIG_PATH, }, - read_wrapper, + iter_ext::IterExt as _, }; use std::{ env::{set_var, var_os}, @@ -43,18 +43,15 @@ use std::{ const CROSS: &str = "×"; static TICK: LazyLock = LazyLock::new(|| "✓".green()); -/// Dialoguer theme -static THEME: LazyLock = LazyLock::new(Default::default); - /// Indicatif themes -#[allow(clippy::expect_used)] +#[expect(clippy::expect_used)] pub static STYLE_NO: LazyLock = LazyLock::new(|| { ProgressStyle::default_bar() .template("{spinner} {elapsed} [{wide_bar:.cyan/blue}] {pos:.cyan}/{len:.blue}") .expect("Progress bar template parse failure") .progress_chars("#>-") }); -#[allow(clippy::expect_used)] +#[expect(clippy::expect_used)] pub static STYLE_BYTE: LazyLock = LazyLock::new(|| { ProgressStyle::default_bar() .template( @@ -68,7 +65,7 @@ fn main() -> ExitCode { #[cfg(windows)] // Enable colours on conhost (command prompt or powershell) { - #[allow(clippy::unwrap_used)] // There is actually no error + #[expect(clippy::unwrap_used)] // There is actually no error colored::control::set_virtual_terminal(true).unwrap(); } @@ -80,7 +77,7 @@ fn main() -> ExitCode { if let Some(threads) = cli.threads { builder.worker_threads(threads); } - #[allow(clippy::expect_used)] // No error handling yet + #[expect(clippy::expect_used)] // No error handling yet let runtime = builder.build().expect("Could not initialise Tokio runtime"); if let Err(err) = runtime.block_on(actual_main(cli)) { @@ -147,7 +144,7 @@ async fn actual_main(mut cli_app: Ferium) -> Result<()> { .or_else(|| var_os("FERIUM_CONFIG_FILE").map(Into::into)) .unwrap_or(DEFAULT_CONFIG_PATH.clone()), )?; - let mut config = config::deserialise(&read_wrapper(&mut config_file)?)?; + let mut config = config::deserialise(&libium::read_wrapper(&mut config_file)?)?; let mut did_add_fail = false; @@ -195,7 +192,7 @@ async fn actual_main(mut cli_app: Ferium) -> Result<()> { } } - let (successes, failures) = libium::add(profile, send_ids, !force, true, true).await?; + let (successes, failures) = libium::add(profile, send_ids, !force).await?; spinner.finish_and_clear(); did_add_fail = add::display_successes_failures(&successes, failures); @@ -203,13 +200,24 @@ async fn actual_main(mut cli_app: Ferium) -> Result<()> { SubCommands::Add { identifiers, force, - ignore_game_version, - ignore_mod_loader, + mod_loader_prefer, + mod_loader_any, + game_version_strict, + game_version_minor, + release_channel, + file_name, } => { let profile = get_active_profile(&mut config)?; - if identifiers.len() > 1 && (ignore_game_version || ignore_mod_loader) { - bail!("Only use the ignore flags when adding a single mod!") + if identifiers.len() > 1 + && (!mod_loader_prefer.is_empty() + || !mod_loader_any.is_empty() + || !game_version_strict.is_empty() + || !game_version_minor.is_empty() + || release_channel.is_some() + || file_name.is_some()) + { + bail!("Only configure filters when adding a single mod!") } let (successes, failures) = libium::add( @@ -219,8 +227,6 @@ async fn actual_main(mut cli_app: Ferium) -> Result<()> { .map(libium::add::parse_id) .collect_vec(), !force, - !ignore_game_version, - !ignore_mod_loader, ) .await?; @@ -237,8 +243,19 @@ async fn actual_main(mut cli_app: Ferium) -> Result<()> { "{} {} on {} {}\n", profile.name.bold(), format!("({} mods)", profile.mods.len()).yellow(), - format!("{:?}", profile.mod_loader).purple(), - profile.game_version.green(), + profile + .filters + .mod_loader() + .map(ToString::to_string) + .unwrap_or_default() + .purple(), + profile + .filters + .game_versions() + .unwrap_or(&vec![]) + .iter() + .display(", ") + .green(), ); for mod_ in &profile.mods { println!( @@ -344,15 +361,15 @@ async fn actual_main(mut cli_app: Ferium) -> Result<()> { }); match subcommand { ProfileSubCommands::Configure { - game_version, - mod_loader, + game_versions, + mod_loaders, name, output_dir, } => { subcommands::profile::configure( get_active_profile(&mut config)?, - game_version, - mod_loader, + game_versions, + mod_loaders, name, output_dir, ) @@ -368,7 +385,11 @@ async fn actual_main(mut cli_app: Ferium) -> Result<()> { subcommands::profile::create( &mut config, import, - game_version, + if game_version.is_empty() { + None + } else { + Some(game_version) + }, mod_loader, name, output_dir, diff --git a/src/subcommands/list.rs b/src/subcommands/list.rs index 914545d..e2d4ce1 100644 --- a/src/subcommands/list.rs +++ b/src/subcommands/list.rs @@ -1,14 +1,12 @@ -#![allow(clippy::unwrap_used)] - use crate::TICK; -use anyhow::{Context, Result}; -use colored::Colorize; +use anyhow::{Context as _, Result}; +use colored::Colorize as _; use ferinth::structures::{project::Project, user::TeamMember}; use furse::structures::mod_structs::Mod; use futures::{stream::FuturesUnordered, StreamExt as _}; -use itertools::{izip, Itertools}; use libium::{ config::structs::{ModIdentifier, Profile}, + iter_ext::IterExt as _, CURSEFORGE_API, GITHUB_API, MODRINTH_API, }; use octocrab::models::{repos::Release, Repository}; @@ -27,6 +25,7 @@ impl Metadata { } } + #[expect(clippy::unwrap_used)] fn id(&self) -> ModIdentifier { match self { Metadata::CF(p) => ModIdentifier::CurseForgeProject(p.id), @@ -66,26 +65,29 @@ pub async fn verbose(profile: &mut Profile, markdown: bool) -> Result<()> { } } - let mr_projects = MODRINTH_API - .get_multiple_projects(&mr_ids.iter().map(|s| &**s).collect::>()) - .await?; - let mr_teams_members = MODRINTH_API - .list_multiple_teams_members( - &mr_projects - .iter() - .map(|p| &p.team as &str) - .collect::>(), - ) - .await?; + let mr_projects = if mr_ids.is_empty() { + vec![] + } else { + MODRINTH_API + .get_multiple_projects(&mr_ids.iter().map(AsRef::as_ref).collect_vec()) + .await? + }; + let mr_teams_members = if mr_projects.is_empty() { + vec![] + } else { + MODRINTH_API + .list_multiple_teams_members(&mr_projects.iter().map(|p| p.team.as_ref()).collect_vec()) + .await? + }; let cf_projects = if cf_ids.is_empty() { - Vec::new() + vec![] } else { CURSEFORGE_API.get_mods(cf_ids).await? }; let mut metadata = Vec::new(); - for (project, members) in izip!(mr_projects, mr_teams_members) { + for (project, members) in mr_projects.into_iter().zip(mr_teams_members) { metadata.push(Metadata::MD(project, members)); } for project in cf_projects { @@ -158,14 +160,14 @@ pub fn curseforge(project: &Mod) { .authors .iter() .map(|author| &author.name) - .format(", ") + .display(", ") .to_string() .cyan(), project .categories .iter() .map(|category| &category.name) - .format(", ") + .display(", ") .to_string() .magenta(), ); @@ -198,10 +200,15 @@ pub fn modrinth(project: &Project, team_members: &[TeamMember]) { team_members .iter() .map(|member| &member.user.username) - .format(", ") + .display(", ") .to_string() .cyan(), - project.categories.iter().format(", ").to_string().magenta(), + project + .categories + .iter() + .display(", ") + .to_string() + .magenta(), { if project.license.name.is_empty() { "Custom" @@ -215,6 +222,7 @@ pub fn modrinth(project: &Project, team_members: &[TeamMember]) { ); } +#[expect(clippy::unwrap_used)] pub fn github(repo: &Repository, releases: &[Release]) { // Calculate number of downloads let mut downloads = 0; @@ -255,7 +263,7 @@ pub fn github(repo: &Repository, releases: &[Release]) { repo.owner.as_ref().unwrap().login.cyan(), repo.topics.as_ref().map_or("".into(), |topics| topics .iter() - .format(", ") + .display(", ") .to_string() .magenta()), repo.license @@ -295,12 +303,12 @@ _{}_ .authors .iter() .map(|author| format!("[{}]({})", author.name, author.url)) - .format(", "), + .display(", "), project .categories .iter() .map(|category| &category.name) - .format(", "), + .display(", "), ); } @@ -330,11 +338,12 @@ _{}_ "[{}](https://modrinth.com/user/{})", member.user.username, member.user.id )) - .format(", "), - project.categories.iter().format(", "), + .display(", "), + project.categories.iter().display(", "), ); } +#[expect(clippy::unwrap_used)] pub fn github_md(repo: &Repository) { println!( " @@ -357,7 +366,7 @@ pub fn github_md(repo: &Repository) { repo.owner.as_ref().unwrap().html_url, repo.topics.as_ref().map_or(String::new(), |topics| format!( "\n| Topics | {} |", - topics.iter().format(", ") + topics.iter().display(", ") )), ); } diff --git a/src/subcommands/modpack/add.rs b/src/subcommands/modpack/add.rs index 26c6805..798a0b0 100644 --- a/src/subcommands/modpack/add.rs +++ b/src/subcommands/modpack/add.rs @@ -1,13 +1,13 @@ use super::check_output_directory; -use crate::{THEME, TICK}; +use crate::TICK; use anyhow::{anyhow, Result}; -use colored::Colorize; -use dialoguer::Confirm; -use itertools::Itertools; +use colored::Colorize as _; +use inquire::Confirm; use libium::{ config::structs::{Config, Modpack, ModpackIdentifier}, file_picker::pick_folder, get_minecraft_dir, + iter_ext::IterExt as _, modpack::add, }; use std::path::PathBuf; @@ -34,10 +34,9 @@ pub async fn curseforge( check_output_directory(&output_dir)?; let install_overrides = match install_overrides { Some(some) => some, - None => Confirm::with_theme(&*THEME) - .default(true) - .with_prompt("Should overrides be installed?") - .interact()?, + None => Confirm::new("Should overrides be installed?") + .with_default(true) + .prompt()?, }; if install_overrides { println!( @@ -80,10 +79,9 @@ pub async fn modrinth( check_output_directory(&output_dir)?; let install_overrides = match install_overrides { Some(some) => some, - None => Confirm::with_theme(&*THEME) - .default(true) - .with_prompt("Should overrides be installed?") - .interact()?, + None => Confirm::new("Should overrides be installed?") + .with_default(true) + .prompt()?, }; if install_overrides { println!( @@ -104,7 +102,7 @@ pub async fn modrinth( this.platform.bold(), this.url.to_string().blue().underline() )) - .format(", ") + .display(", ") ); } config.modpacks.push(Modpack { diff --git a/src/subcommands/modpack/configure.rs b/src/subcommands/modpack/configure.rs index 6c5430f..bec0d49 100644 --- a/src/subcommands/modpack/configure.rs +++ b/src/subcommands/modpack/configure.rs @@ -1,8 +1,7 @@ use super::check_output_directory; -use crate::THEME; use anyhow::Result; -use colored::Colorize; -use dialoguer::Confirm; +use colored::Colorize as _; +use inquire::Confirm; use libium::{config::structs::Modpack, file_picker::pick_folder}; use std::path::PathBuf; @@ -30,10 +29,9 @@ pub fn configure( modpack.install_overrides = if let Some(install_overrides) = install_overrides { install_overrides } else { - let install_overrides = Confirm::with_theme(&*THEME) - .default(modpack.install_overrides) - .with_prompt("Should overrides be installed?") - .interact()?; + let install_overrides = Confirm::new("Should overrides be installed?") + .with_default(modpack.install_overrides) + .prompt()?; if install_overrides { println!( "{}", diff --git a/src/subcommands/modpack/delete.rs b/src/subcommands/modpack/delete.rs index 281a68a..9b89993 100644 --- a/src/subcommands/modpack/delete.rs +++ b/src/subcommands/modpack/delete.rs @@ -1,11 +1,13 @@ use std::cmp::Ordering; use super::switch; -use crate::THEME; use anyhow::{anyhow, Result}; -use colored::Colorize; -use dialoguer::Select; -use libium::config::structs::{Config, ModpackIdentifier}; +use colored::Colorize as _; +use inquire::Select; +use libium::{ + config::structs::{Config, ModpackIdentifier}, + iter_ext::IterExt as _, +}; pub fn delete( config: &mut Config, @@ -35,18 +37,12 @@ pub fn delete( modpack.name.bold(), ) }) - .collect::>(); + .collect_vec(); - let selection = Select::with_theme(&*THEME) - .with_prompt("Select which modpack to delete") - .items(&modpack_names) - .default(config.active_modpack) - .interact_opt()?; - if let Some(selection) = selection { - selection - } else { - return Ok(()); - } + Select::new("Select which modpack to delete", modpack_names) + .with_starting_cursor(config.active_modpack) + .raw_prompt()? + .index }; config.modpacks.remove(selection); diff --git a/src/subcommands/modpack/info.rs b/src/subcommands/modpack/info.rs index e0967f7..74e1683 100644 --- a/src/subcommands/modpack/info.rs +++ b/src/subcommands/modpack/info.rs @@ -1,4 +1,4 @@ -use colored::Colorize; +use colored::Colorize as _; use libium::config::structs::{Modpack, ModpackIdentifier}; pub fn info(modpack: &Modpack, active: bool) { diff --git a/src/subcommands/modpack/mod.rs b/src/subcommands/modpack/mod.rs index 7242cf1..d0c59fe 100644 --- a/src/subcommands/modpack/mod.rs +++ b/src/subcommands/modpack/mod.rs @@ -10,14 +10,12 @@ pub use info::info; pub use switch::switch; pub use upgrade::upgrade; -use crate::THEME; -use anyhow::{anyhow, ensure, Context, Result}; -use dialoguer::Confirm; +use anyhow::{anyhow, ensure, Context as _, Result}; use fs_extra::dir::{copy, CopyOptions}; +use inquire::Confirm; use libium::{file_picker::pick_folder, HOME}; use std::{fs::read_dir, path::Path}; -#[allow(clippy::expect_used)] pub fn check_output_directory(output_dir: &Path) -> Result<()> { ensure!( output_dir.is_absolute(), @@ -40,10 +38,7 @@ pub fn check_output_directory(output_dir: &Path) -> Result<()> { "There are files in the {} folder in your output directory, these will be deleted when you upgrade.", check_dir.file_name().context("Unable to get folder name")?.to_string_lossy() ); - if Confirm::with_theme(&*THEME) - .with_prompt("Would like to create a backup?") - .interact()? - { + if Confirm::new("Would like to create a backup?").prompt()? { let backup_dir = pick_folder( &*HOME, "Where should the backup be made?", diff --git a/src/subcommands/modpack/switch.rs b/src/subcommands/modpack/switch.rs index 4b99b1c..ca1d261 100644 --- a/src/subcommands/modpack/switch.rs +++ b/src/subcommands/modpack/switch.rs @@ -1,8 +1,10 @@ -use crate::THEME; use anyhow::{anyhow, Result}; -use colored::Colorize; -use dialoguer::Select; -use libium::config::structs::{Config, ModpackIdentifier}; +use colored::Colorize as _; +use inquire::Select; +use libium::{ + config::structs::{Config, ModpackIdentifier}, + iter_ext::IterExt as _, +}; pub fn switch(config: &mut Config, modpack_name: Option) -> Result<()> { if config.modpacks.len() <= 1 { @@ -36,14 +38,12 @@ pub fn switch(config: &mut Config, modpack_name: Option) -> Result<()> { modpack.name.bold(), ) }) - .collect::>(); + .collect_vec(); - let selection = Select::with_theme(&*THEME) - .with_prompt("Select which modpack to switch to") - .items(&modpack_info) - .default(config.active_modpack) - .interact()?; - config.active_modpack = selection; + config.active_modpack = Select::new("Select which modpack to switch to", modpack_info) + .with_starting_cursor(config.active_modpack) + .raw_prompt()? + .index; Ok(()) } } diff --git a/src/subcommands/modpack/upgrade.rs b/src/subcommands/modpack/upgrade.rs index cffa8f6..afc2dba 100644 --- a/src/subcommands/modpack/upgrade.rs +++ b/src/subcommands/modpack/upgrade.rs @@ -2,15 +2,16 @@ use crate::{ download::{clean, download, read_overrides}, STYLE_BYTE, TICK, }; -use anyhow::{Context, Result}; -use colored::Colorize; +use anyhow::{Context as _, Result}; +use colored::Colorize as _; use futures::{stream::FuturesUnordered, StreamExt as _}; use indicatif::ProgressBar; -use itertools::Itertools; use libium::{ config::structs::{Modpack, ModpackIdentifier}, + iter_ext::IterExt as _, modpack::{ - curseforge::structs::Manifest, modrinth::structs::Metadata, read_file_from_zip, zip_extract, + curseforge::structs::Manifest as CFManifest, modrinth::structs::Metadata as MRMetadata, + read_file_from_zip, zip_extract, }, upgrade::{DistributionDeniedError, DownloadFile}, CURSEFORGE_API, HOME, @@ -46,7 +47,7 @@ pub async fn upgrade(modpack: &'_ Modpack) -> Result<()> { match &modpack.identifier { ModpackIdentifier::CurseForgeModpack(_) => { - let manifest: Manifest = serde_json::from_str( + let manifest: CFManifest = serde_json::from_str( &read_file_from_zip(BufReader::new(modpack_file), "manifest.json")? .context("Does not contain manifest")?, )?; @@ -108,7 +109,7 @@ pub async fn upgrade(modpack: &'_ Modpack) -> Result<()> { .mod_loaders .iter() .map(|this| &this.id) - .format(", or ") + .display(", ") ); if modpack.install_overrides { @@ -122,7 +123,7 @@ pub async fn upgrade(modpack: &'_ Modpack) -> Result<()> { } } ModpackIdentifier::ModrinthModpack(_) => { - let metadata: Metadata = serde_json::from_str( + let metadata: MRMetadata = serde_json::from_str( &read_file_from_zip(BufReader::new(modpack_file), "modrinth.index.json")? .context("Does not contain metadata file")?, )?; @@ -137,7 +138,7 @@ pub async fn upgrade(modpack: &'_ Modpack) -> Result<()> { .dependencies .iter() .map(|this| format!("{:?} {}", this.0, this.1)) - .format("\n") + .display("\n") ); if modpack.install_overrides { diff --git a/src/subcommands/profile/configure.rs b/src/subcommands/profile/configure.rs index 603dcba..f2ff05f 100644 --- a/src/subcommands/profile/configure.rs +++ b/src/subcommands/profile/configure.rs @@ -1,8 +1,8 @@ -use super::{check_output_directory, pick_minecraft_version, pick_mod_loader}; -use crate::THEME; -use anyhow::Result; -use dialoguer::{Input, Select}; +use super::{check_output_directory, pick_minecraft_versions, pick_mod_loader}; +use anyhow::{Context as _, Result}; +use inquire::{Select, Text}; use libium::{ + config::filters::ProfileParameters as _, config::structs::{ModLoader, Profile}, file_picker::pick_folder, }; @@ -10,19 +10,27 @@ use std::path::PathBuf; pub async fn configure( profile: &mut Profile, - game_version: Option, - mod_loader: Option, + game_versions: Vec, + mod_loaders: Vec, name: Option, output_dir: Option, ) -> Result<()> { let mut interactive = true; - if let Some(game_version) = game_version { - profile.game_version = game_version; + if !game_versions.is_empty() { + *profile + .filters + .game_versions_mut() + .context("Active profile does not filter by game version")? = game_versions; + interactive = false; } - if let Some(mod_loader) = mod_loader { - profile.mod_loader = mod_loader; + if !mod_loaders.is_empty() { + *profile + .filters + .mod_loaders_mut() + .context("Active profile does not filter mod loader")? = mod_loaders; + interactive = false; } if let Some(name) = name { @@ -49,10 +57,11 @@ pub async fn configure( ]; loop { - let selection = Select::with_theme(&*THEME) - .with_prompt("Which setting would you like to change") - .items(&items) - .interact_opt()?; + // TODO: raw_prompt_skippable + let selection = Select::new("Which setting would you like to change", items.clone()) + .raw_prompt() + .ok() + .map(|x| x.index); if let Some(index) = selection { match index { @@ -66,14 +75,33 @@ pub async fn configure( profile.output_dir = dir; } } - 1 => profile.game_version = pick_minecraft_version().await?, - 2 => profile.mod_loader = pick_mod_loader(Some(&profile.mod_loader))?, + 1 => { + let Some(versions) = profile.filters.game_versions_mut() else { + println!("Active profile does not filter by game version"); + continue; + }; + + *versions = pick_minecraft_versions().await?; + } + 2 => { + let Some(loaders) = profile.filters.mod_loaders_mut() else { + println!("Active profile does not filter mod loader"); + continue; + }; + *loaders = match pick_mod_loader(loaders.first())? { + ModLoader::Quilt => vec![ModLoader::Quilt, ModLoader::Fabric], + loader => vec![loader], + } + } 3 => { - let name = Input::with_theme(&*THEME) - .with_prompt("Change the profile's name") - .default(profile.name.clone()) - .interact_text()?; - profile.name = name; + if let Ok(new_name) = Text::new("Change the profile's name") + .with_default(&profile.name) + .prompt() + { + profile.name = new_name; + } else { + continue; + } } 4 => break, _ => unreachable!(), diff --git a/src/subcommands/profile/create.rs b/src/subcommands/profile/create.rs index aa451d3..0f0db78 100644 --- a/src/subcommands/profile/create.rs +++ b/src/subcommands/profile/create.rs @@ -1,40 +1,42 @@ -use super::{check_output_directory, check_profile_name, pick_minecraft_version}; -use crate::THEME; -use anyhow::{anyhow, bail, ensure, Result}; -use colored::Colorize; -use dialoguer::{Confirm, Input, Select}; +use super::{check_output_directory, pick_minecraft_versions, pick_mod_loader}; +use anyhow::{bail, ensure, Context as _, Result}; +use colored::Colorize as _; +use inquire::{ + validator::{ErrorMessage, Validation}, + Confirm, Select, Text, +}; use libium::{ config::structs::{Config, ModLoader, Profile}, file_picker::pick_folder, get_minecraft_dir, + iter_ext::IterExt as _, }; use std::path::PathBuf; -#[allow(clippy::option_option)] +#[expect(clippy::option_option)] pub async fn create( config: &mut Config, import: Option>, - game_version: Option, + game_versions: Option>, mod_loader: Option, name: Option, output_dir: Option, ) -> Result<()> { - let mut profile = match (game_version, mod_loader, name, output_dir) { - (Some(game_version), Some(mod_loader), Some(name), output_dir) => { - check_profile_name(config, &name)?; + let mut profile = match (game_versions, mod_loader, name, output_dir) { + (Some(game_versions), Some(mod_loader), Some(name), output_dir) => { + for profile in &config.profiles { + ensure!( + !profile.name.eq_ignore_ascii_case(&name), + "A profile with name {name} already exists" + ); + } let output_dir = output_dir.unwrap_or_else(|| get_minecraft_dir().join("mods")); ensure!( output_dir.is_absolute(), "The provided output directory is not absolute, i.e. it is a relative path" ); - Profile { - name, - output_dir, - game_version, - mod_loader, - mods: Vec::new(), - } + Profile::new(name, output_dir, game_versions, mod_loader) } (None, None, None, None) => { let mut selected_mods_dir = get_minecraft_dir().join("mods"); @@ -42,9 +44,9 @@ pub async fn create( "The default mods directory is {}", selected_mods_dir.display() ); - if Confirm::with_theme(&*THEME) - .with_prompt("Would you like to specify a custom mods directory?") - .interact()? + if Confirm::new("Would you like to specify a custom mods directory?") + .prompt() + .unwrap_or(false) { if let Some(dir) = pick_folder( &selected_mods_dir, @@ -56,35 +58,28 @@ pub async fn create( }; } - let name = loop { - let name: String = Input::with_theme(&*THEME) - .with_prompt("What should this profile be called?") - .interact_text()?; - - if check_profile_name(config, &name).is_ok() { - break name; - } + let profiles = config.profiles.clone(); + let name = Text::new("What should this profile be called") + .with_validator(move |s: &str| { + Ok(if profiles.iter().any(|p| p.name.eq_ignore_ascii_case(s)) { + Validation::Invalid(ErrorMessage::Custom( + "A profile with that name already exists".to_owned(), + )) + } else { + Validation::Valid + }) + }) + .prompt()?; - println!( - "{}", - "Please provide a name that is not already being used" - .red() - .bold() - ); - }; - - let selected_version = pick_minecraft_version().await?; - - Profile { + Profile::new( name, - output_dir: selected_mods_dir, - mods: Vec::new(), - game_version: selected_version, - mod_loader: super::pick_mod_loader(None)?, - } + selected_mods_dir, + pick_minecraft_versions().await?, + pick_mod_loader(None)?, + ) } _ => { - bail!("Provide at least the name, game version, and mod loader options to create a profile") + bail!("Provide the name, game version, mod loader, and output directory options to create a profile") } }; @@ -95,25 +90,29 @@ pub async fn create( ); // If the profile name has been provided as an option - let selection = if let Some(profile_name) = from { - config + if let Some(profile_name) = from { + let selection = config .profiles .iter() - .position(|profile| profile.name == profile_name) - .ok_or_else(|| anyhow!("The profile name provided does not exist"))? + .position(|profile| profile.name.eq_ignore_ascii_case(&profile_name)) + .context("The profile name provided does not exist")?; + profile.mods.clone_from(&config.profiles[selection].mods); } else { let profile_names = config .profiles .iter() .map(|profile| &profile.name) - .collect::>(); - Select::with_theme(&*THEME) - .with_prompt("Select which profile to import mods from") - .items(&profile_names) - .default(config.active_profile) - .interact()? + .collect_vec(); + if let Ok(selection) = + Select::new("Select which profile to import mods from", profile_names) + .with_starting_cursor(config.active_profile) + .raw_prompt() + { + profile + .mods + .clone_from(&config.profiles[selection.index].mods); + } }; - profile.mods.clone_from(&config.profiles[selection].mods); } println!( diff --git a/src/subcommands/profile/delete.rs b/src/subcommands/profile/delete.rs index b3904a9..3b37d43 100644 --- a/src/subcommands/profile/delete.rs +++ b/src/subcommands/profile/delete.rs @@ -1,11 +1,13 @@ use std::cmp::Ordering; use super::switch; -use crate::THEME; -use anyhow::{anyhow, Result}; -use colored::Colorize; -use dialoguer::Select; -use libium::config::structs::Config; +use anyhow::{Context as _, Result}; +use colored::Colorize as _; +use inquire::Select; +use libium::{ + config::{filters::ProfileParameters as _, structs::Config}, + iter_ext::IterExt as _, +}; pub fn delete( config: &mut Config, @@ -17,8 +19,8 @@ pub fn delete( config .profiles .iter() - .position(|profile| profile.name == profile_name) - .ok_or_else(|| anyhow!("The profile name provided does not exist"))? + .position(|profile| profile.name.eq_ignore_ascii_case(&profile_name)) + .context("The profile name provided does not exist")? } else { let profile_names = config .profiles @@ -26,21 +28,29 @@ pub fn delete( .map(|profile| { format!( "{:6} {:7} {} {}", - format!("{:?}", profile.mod_loader).purple(), - profile.game_version.green(), + profile + .filters + .mod_loader() + .map(ToString::to_string) + .unwrap_or_default() + .purple(), + profile + .filters + .game_versions() + .map(|v| v.iter().display(", ")) + .unwrap_or_default() + .green(), profile.name.bold(), format!("({} mods)", profile.mods.len()).yellow(), ) }) - .collect::>(); + .collect_vec(); - let selection = Select::with_theme(&*THEME) - .with_prompt("Select which profile to delete") - .items(&profile_names) - .default(config.active_profile) - .interact_opt()?; - if let Some(selection) = selection { - selection + if let Ok(selection) = Select::new("Select which profile to delete", profile_names) + .with_starting_cursor(config.active_profile) + .raw_prompt() + { + selection.index } else { return Ok(()); } diff --git a/src/subcommands/profile/info.rs b/src/subcommands/profile/info.rs index de2594e..da7324e 100644 --- a/src/subcommands/profile/info.rs +++ b/src/subcommands/profile/info.rs @@ -1,18 +1,33 @@ use colored::Colorize; -use libium::config::structs::Profile; +use libium::{ + config::{filters::ProfileParameters as _, structs::Profile}, + iter_ext::IterExt as _, +}; pub fn info(profile: &Profile, active: bool) { println!( "{}{} - \r Output directory: {} - \r Minecraft Version: {} - \r Mod Loader: {} + \r Output directory: {}{}{} \r Mods: {}\n", profile.name.bold(), if active { " *" } else { "" }, profile.output_dir.display().to_string().blue().underline(), - profile.game_version.green(), - format!("{:?}", profile.mod_loader).purple(), + profile + .filters + .game_versions() + .map(|v| format!( + "\n Minecraft Version: {}", + v.iter() + .map(AsRef::as_ref) + .map(Colorize::green) + .display(", ") + )) + .unwrap_or_default(), + profile + .filters + .mod_loader() + .map(|l| format!("\n Mod Loader: {}", l.to_string().purple())) + .unwrap_or_default(), profile.mods.len().to_string().yellow(), ); } diff --git a/src/subcommands/profile/mod.rs b/src/subcommands/profile/mod.rs index 795483f..8646f5b 100644 --- a/src/subcommands/profile/mod.rs +++ b/src/subcommands/profile/mod.rs @@ -9,83 +9,69 @@ pub use delete::delete; pub use info::info; pub use switch::switch; -use crate::THEME; use anyhow::{anyhow, ensure, Result}; -use colored::Colorize; -use dialoguer::{Confirm, Select}; -use ferinth::{structures::tag::GameVersionType, Ferinth}; +use colored::Colorize as _; +use ferinth::Ferinth; use fs_extra::dir::{copy, CopyOptions}; -use libium::{ - config::structs::{Config, ModLoader}, - file_picker::pick_folder, - HOME, +use inquire::{Confirm, MultiSelect, Select}; +use libium::{config::structs::ModLoader, file_picker::pick_folder, iter_ext::IterExt as _, HOME}; +use std::{ + fs::{create_dir_all, read_dir}, + path::PathBuf, }; -use std::{fs::{create_dir_all, read_dir}, path::PathBuf}; +#[expect(clippy::unwrap_used, reason = "All variants are present")] pub fn pick_mod_loader(default: Option<&ModLoader>) -> Result { - let mut picker = Select::with_theme(&*THEME) - .with_prompt("Which mod loader do you use?") - .items(&["Quilt", "Fabric", "Forge", "NeoForge"]); + let options = vec![ + ModLoader::Fabric, + ModLoader::Quilt, + ModLoader::NeoForge, + ModLoader::Forge, + ]; + let mut picker = Select::new("Which mod loader do you use?", options.clone()); if let Some(default) = default { - picker = picker.default(match default { - ModLoader::Quilt => 0, - ModLoader::Fabric => 1, - ModLoader::Forge => 2, - ModLoader::NeoForge => 3, - }); - } - match picker.interact()? { - 0 => Ok(ModLoader::Quilt), - 1 => Ok(ModLoader::Fabric), - 2 => Ok(ModLoader::Forge), - 3 => Ok(ModLoader::NeoForge), - _ => unreachable!(), + picker.starting_cursor = options.iter().position(|l| l == default).unwrap(); } + Ok(picker.prompt()?) } -pub async fn pick_minecraft_version() -> Result { - let versions = Ferinth::default().list_game_versions().await?; - let mut major_versions = ["Show all", "Show release"] // Prepend additional options - .into_iter() - .chain( - versions - .iter() - .filter(|v| v.major) // Only get major versions - .map(|v| v.version.as_ref()) - .collect::>(), - ) - .collect::>(); - let selected_version = Select::with_theme(&*THEME) - .with_prompt("Which version of Minecraft do you play?") - .items(&major_versions) - .default(2) - .interact()?; - match selected_version { - 0 | 1 => { - let mut versions = versions - .into_iter() - .filter(|v| selected_version == 0 || v.version_type == GameVersionType::Release) - .map(|v| v.version) - .collect::>(); - let selected_version = Select::with_theme(&*THEME) - .with_prompt("Which version of Minecraft do you play?") - .items(&versions) - .interact()?; - Ok(versions.swap_remove(selected_version)) - } - _ => Ok(major_versions.swap_remove(selected_version).to_owned()), - } -} +pub async fn pick_minecraft_versions() -> Result> { + let mut versions = Ferinth::default().list_game_versions().await?; + versions.sort_by(|a, b| { + // Sort by release type (release > snapshot > beta > alpha) then in reverse chronological order + a.version_type + .cmp(&b.version_type) + .then(b.date.cmp(&a.date)) + }); + let display_versions = versions + .iter() + .map(|v| { + if v.major { + v.version.bold() + } else { + v.version.clone().into() + } + }) + .collect_vec(); -/// Check that there isn't already a profile with the same name -pub fn check_profile_name(config: &Config, name: &str) -> Result<()> { - for profile in &config.profiles { - ensure!( - profile.name != name, - "A profile with name {name} already exists" - ); - } - Ok(()) + let selected_versions = + MultiSelect::new("Which version of Minecraft do you play?", display_versions) + .raw_prompt()? + .into_iter() + .map(|s| s.index) + .collect_vec(); + + Ok(versions + .into_iter() + .enumerate() + .filter_map(|(i, v)| { + if selected_versions.contains(&i) { + Some(v.version) + } else { + None + } + }) + .collect_vec()) } pub async fn check_output_directory(output_dir: &PathBuf) -> Result<()> { @@ -111,10 +97,7 @@ pub async fn check_output_directory(output_dir: &PathBuf) -> Result<()> { println!( "There are files in your output directory, these will be deleted when you upgrade." ); - if Confirm::with_theme(&*THEME) - .with_prompt("Would like to create a backup?") - .interact()? - { + if Confirm::new("Would like to create a backup?").prompt()? { let backup_dir = pick_folder( &*HOME, "Where should the backup be made?", diff --git a/src/subcommands/profile/switch.rs b/src/subcommands/profile/switch.rs index cc188f7..3ca95a8 100644 --- a/src/subcommands/profile/switch.rs +++ b/src/subcommands/profile/switch.rs @@ -1,8 +1,10 @@ -use crate::THEME; use anyhow::{anyhow, Result}; -use colored::Colorize; -use dialoguer::Select; -use libium::config::structs::Config; +use colored::Colorize as _; +use inquire::Select; +use libium::{ + config::{filters::ProfileParameters as _, structs::Config}, + iter_ext::IterExt as _, +}; pub fn switch(config: &mut Config, profile_name: Option) -> Result<()> { if config.profiles.len() <= 1 { @@ -25,21 +27,29 @@ pub fn switch(config: &mut Config, profile_name: Option) -> Result<()> { .iter() .map(|profile| { format!( - "{:6} {:7} {} {}", - format!("{:?}", profile.mod_loader).purple(), - profile.game_version.green(), + "{:8} {:7} {} {}", + profile + .filters + .mod_loader() + .map(|l| l.to_string().purple()) + .unwrap_or_default(), + profile + .filters + .game_versions() + .map(|v| v[0].green()) + .unwrap_or_default(), profile.name.bold(), format!("({} mods)", profile.mods.len()).yellow(), ) }) - .collect::>(); + .collect_vec(); - let selection = Select::with_theme(&*THEME) - .with_prompt("Select which profile to switch to") - .items(&profile_info) - .default(config.active_profile) - .interact()?; - config.active_profile = selection; + if let Ok(selection) = Select::new("Select which profile to switch to", profile_info) + .with_starting_cursor(config.active_profile) + .raw_prompt() + { + config.active_profile = selection.index; + } Ok(()) } } diff --git a/src/subcommands/remove.rs b/src/subcommands/remove.rs index ab18133..631d61e 100644 --- a/src/subcommands/remove.rs +++ b/src/subcommands/remove.rs @@ -1,9 +1,10 @@ -use crate::THEME; use anyhow::{bail, Result}; -use colored::Colorize; -use dialoguer::MultiSelect; -use itertools::Itertools; -use libium::config::structs::{ModIdentifier, Profile}; +use colored::Colorize as _; +use inquire::MultiSelect; +use libium::{ + config::structs::{ModIdentifier, Profile}, + iter_ext::IterExt as _, +}; /// If `to_remove` is empty, display a list of projects in the profile to select from and remove selected ones /// @@ -25,13 +26,10 @@ pub fn remove(profile: &mut Profile, to_remove: Vec) -> Result<()> { }, ) }); - match MultiSelect::with_theme(&*THEME) - .with_prompt("Select mods to remove") - .items(&mod_info.collect::>()) - .report(false) - .interact_opt()? + match MultiSelect::new("Select mods to remove", mod_info.collect_vec()) + .raw_prompt_skippable()? { - Some(items_to_remove) => items_to_remove, + Some(items_to_remove) => items_to_remove.iter().map(|o| o.index).collect_vec(), None => return Ok(()), // Exit if the user cancelled } } else { @@ -66,7 +64,7 @@ pub fn remove(profile: &mut Profile, to_remove: Vec) -> Result<()> { println!( "Removed {}", - removed.iter().map(|txt| txt.bold()).format(", ") + removed.iter().map(|txt| txt.bold()).display(", ") ); Ok(()) diff --git a/src/subcommands/upgrade.rs b/src/subcommands/upgrade.rs index a34ccce..1e9b6a6 100644 --- a/src/subcommands/upgrade.rs +++ b/src/subcommands/upgrade.rs @@ -1,21 +1,20 @@ -// Allow `expect()`s for mutex poisons -#![allow(clippy::expect_used)] +#![expect(clippy::expect_used, reason = "For mutex poisons")] use crate::{ download::{clean, download}, CROSS, STYLE_NO, TICK, }; use anyhow::{anyhow, bail, Result}; -use colored::Colorize; -use futures::{stream::FuturesUnordered, StreamExt}; +use colored::Colorize as _; +use futures::{stream::FuturesUnordered, StreamExt as _, TryFutureExt as _}; use indicatif::ProgressBar; use libium::{ - config::structs::{ModLoader, Profile}, - upgrade::{ - check, - mod_downloadable::{self}, - DownloadFile, + config::{ + filters::ProfileParameters as _, + structs::{ModLoader, Profile}, }, + iter_ext::IterExt as _, + upgrade::{check, mod_downloadable, DownloadFile}, }; use std::{ fs::read_dir, @@ -52,46 +51,45 @@ pub async fn get_platform_downloadables(profile: &Profile) -> Result<(Vec { - if let Some(download_file) = check::select_latest( - download_files, - profile.get_version(mod_.check_game_version), - profile.get_loader(mod_.check_mod_loader), - ) { - progress_bar.println(format!( - "{} {:pad_len$} {}", - TICK.clone(), - mod_.name, - download_file.filename().dimmed() - )); - to_download - .lock() - .expect("Mutex poisoned") - .push(download_file); - Ok(true) - } else { - progress_bar.println(format!( - "{}", - format!( - "{CROSS} {:pad_len$} No compatible file was found", - mod_.name - ) - .red() - )); - Ok(false) - } + Ok(download_file) => { + progress_bar.println(format!( + "{} {:pad_len$} {}", + TICK.clone(), + mod_.name, + download_file.filename().dimmed() + )); + to_download + .lock() + .expect("Mutex poisoned") + .push(download_file); + Ok(true) } Err(err) => { - if let mod_downloadable::Error::ModrinthError( + if let Some(mod_downloadable::Error::ModrinthError( ferinth::Error::RateLimitExceeded(_), - ) = err + )) = err.downcast_ref() { // Immediately fail if the rate limit has been exceeded progress_bar.finish_and_clear(); @@ -127,7 +125,9 @@ pub async fn get_platform_downloadables(profile: &Profile) -> Result<(Vec Result<()> { let (mut to_download, error) = get_platform_downloadables(profile).await?; let mut to_install = Vec::new(); - if profile.output_dir.join("user").exists() && profile.mod_loader != ModLoader::Quilt { + if profile.output_dir.join("user").exists() + && profile.filters.mod_loader() != Some(&ModLoader::Quilt) + { for file in read_dir(profile.output_dir.join("user"))? { let file = file?; let path = file.path();