Initial draft of profile metadata format & CLI (#17)
* Initial draft of profile metadata format * Remove records, add Clippy to Nix, fix Clippy error * Work on profile definition * BREAKING: Make global settings consistent with profile settings * Add builder methods & format * Integrate launching with profiles * Add profile loading * Launching via profile, API tweaks, and yak shaving * Incremental update, committing everything due to personal system maintainance * Prepare for review cycle * Remove reminents of experimental work * CLI: allow people to override the non-empty directory check * Fix mistake in previous commit * Handle trailing whitespace and newlines in prompts * Revamp prompt to use dialoguer and support defaults * Make requested changes
This commit is contained in:
parent
98aa66f9d8
commit
d1070ca213
6
.gitignore
vendored
6
.gitignore
vendored
@ -3,3 +3,9 @@ node_modules/
|
|||||||
.svelte-kit/
|
.svelte-kit/
|
||||||
theseus_gui/build/
|
theseus_gui/build/
|
||||||
WixTools
|
WixTools
|
||||||
|
|
||||||
|
[#]*[#]
|
||||||
|
|
||||||
|
# TEMPORARY: ignore my test instance and metadata
|
||||||
|
theseus_cli/launcher
|
||||||
|
theseus_cli/foo
|
||||||
|
|||||||
176
Cargo.lock
generated
176
Cargo.lock
generated
@ -529,6 +529,21 @@ dependencies = [
|
|||||||
"cache-padded",
|
"cache-padded",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
|
||||||
|
dependencies = [
|
||||||
|
"encode_unicode",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"terminal_size",
|
||||||
|
"unicode-width",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const_format"
|
name = "const_format"
|
||||||
version = "0.2.22"
|
version = "0.2.22"
|
||||||
@ -847,6 +862,23 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dialoguer"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "349d6b4fabcd9e97e1df1ae15395ac7e49fb144946a0d453959dc2696273b9da"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"tempfile",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@ -878,6 +910,15 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-next"
|
name = "dirs-next"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@ -888,6 +929,17 @@ dependencies = [
|
|||||||
"dirs-sys-next",
|
"dirs-sys-next",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_users 0.4.0",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-sys-next"
|
name = "dirs-sys-next"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@ -938,6 +990,12 @@ version = "1.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.29"
|
version = "0.8.29"
|
||||||
@ -974,6 +1032,16 @@ version = "2.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
|
checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "eyre"
|
||||||
|
version = "0.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9289ed2c0440a6536e65119725cf91fc2c6b5e513bfd2e36e1134d7cca6ca12f"
|
||||||
|
dependencies = [
|
||||||
|
"indenter",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fake-simd"
|
name = "fake-simd"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@ -1694,6 +1762,12 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indenter"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
@ -1898,7 +1972,7 @@ checksum = "3dfb6b71a9a89cd38b395d994214297447e8e63b1ba5708a9a2b0b1048ceda76"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs",
|
"dirs 1.0.5",
|
||||||
"objc-foundation",
|
"objc-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2313,6 +2387,15 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "output_vt100"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.15.6"
|
version = "0.15.6"
|
||||||
@ -2338,6 +2421,21 @@ dependencies = [
|
|||||||
"system-deps 6.0.2",
|
"system-deps 6.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "papergrid"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c2b5b9ea36abb56e4f8a29889e67f878ebc4ab43c918abe32e85a012d27b819"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paris"
|
||||||
|
version = "1.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c69d19a208bba8b94bd27d4b7a06ad153cddc6b88cb2149a668e23ce7bdb67d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@ -2624,6 +2722,18 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c038cb5319b9c704bf9c227c261d275bfec0ad438118a2787ce47944fb228b"
|
||||||
|
dependencies = [
|
||||||
|
"ansi_term",
|
||||||
|
"ctor",
|
||||||
|
"diff",
|
||||||
|
"output_vt100",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@ -3465,6 +3575,27 @@ dependencies = [
|
|||||||
"version-compare 0.1.0",
|
"version-compare 0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tabled"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85fda0a52a50c85604747c6584cc79cfe6c7b6ee2349ffed082f965bdbef77ef"
|
||||||
|
dependencies = [
|
||||||
|
"papergrid",
|
||||||
|
"tabled_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tabled_derive"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a224735cbc8c30f06e52dc3891dc4b8eed07e5d4c8fb6f4cb6a839458e5a6465"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tao"
|
name = "tao"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@ -3715,6 +3846,16 @@ dependencies = [
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal_size"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus"
|
name = "theseus"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -3731,6 +3872,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"path-clean",
|
"path-clean",
|
||||||
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3748,9 +3890,18 @@ dependencies = [
|
|||||||
name = "theseus_cli"
|
name = "theseus_cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argh",
|
||||||
|
"daedalus",
|
||||||
|
"dialoguer",
|
||||||
|
"dirs 4.0.0",
|
||||||
|
"eyre",
|
||||||
"futures",
|
"futures",
|
||||||
|
"paris",
|
||||||
|
"tabled",
|
||||||
"theseus",
|
"theseus",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3864,6 +4015,17 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-stream"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.6.9"
|
version = "0.6.9"
|
||||||
@ -4002,6 +4164,12 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@ -4628,6 +4796,12 @@ dependencies = [
|
|||||||
"zvariant",
|
"zvariant",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7eb5728b8afd3f280a869ce1d4c554ffaed35f45c231fc41bfbd0381bef50317"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "0.5.13"
|
version = "0.5.13"
|
||||||
|
|||||||
103
flake.lock
generated
Normal file
103
flake.lock
generated
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1646893503,
|
||||||
|
"narHash": "sha256-N4Wn8FUXUC1h1DkL8X9I7VMvIv0fLLLjeJX3uFyzvRQ=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "aad7f0a3e44ecfc9e2c5f1a45387d193c1c51aa6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"naersk": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1639947939,
|
||||||
|
"narHash": "sha256-pGsM8haJadVP80GFq4xhnSpNitYNQpaXk4cnA796Cso=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"rev": "2fc8ce9d3c025d59fee349c1f80be9785049d653",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1646497237,
|
||||||
|
"narHash": "sha256-Ccpot1h/rV8MgcngDp5OrdmLTMaUTbStZTR5/sI7zW0=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "062a0c5437b68f950b081bbfc8a699d57a4ee026",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"fenix": "fenix",
|
||||||
|
"naersk": "naersk",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"utils": "utils"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1646862342,
|
||||||
|
"narHash": "sha256-zXd3qsIcQFDFMB6p8bSpkOKjTuBTvYuM4GkPYxEfQdA=",
|
||||||
|
"owner": "rust-analyzer",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "5b51cb835a356cf79cba00cf5c65d51cadeea7f1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-analyzer",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1644229661,
|
||||||
|
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
66
flake.nix
Normal file
66
flake.nix
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
description = "The official Modrinth launcher";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
utils.url = "github:numtide/flake-utils";
|
||||||
|
naersk = {
|
||||||
|
url = "github:nix-community/naersk";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = inputs:
|
||||||
|
inputs.utils.lib.eachDefaultSystem (system: let
|
||||||
|
pkgs = import inputs.nixpkgs { inherit system; };
|
||||||
|
fenix = inputs.fenix.packages.${system};
|
||||||
|
utils = inputs.utils.lib;
|
||||||
|
|
||||||
|
toolchain = with fenix;
|
||||||
|
combine [
|
||||||
|
minimal.rustc minimal.cargo
|
||||||
|
];
|
||||||
|
|
||||||
|
naersk = inputs.naersk.lib.${system}.override {
|
||||||
|
rustc = toolchain;
|
||||||
|
cargo = toolchain;
|
||||||
|
};
|
||||||
|
|
||||||
|
deps = with pkgs; {
|
||||||
|
global = [
|
||||||
|
openssl pkg-config
|
||||||
|
];
|
||||||
|
gui = [
|
||||||
|
gtk4 gdk-pixbuf atk webkitgtk
|
||||||
|
];
|
||||||
|
shell = [
|
||||||
|
toolchain fenix.default.clippy git
|
||||||
|
jdk17 jdk8
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
packages = {
|
||||||
|
theseus-cli = naersk.buildPackage {
|
||||||
|
pname = "theseus_cli";
|
||||||
|
src = ./.;
|
||||||
|
buildInputs = deps.global;
|
||||||
|
cargoBuildOptions = x: x ++ ["-p" "theseus_cli"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
apps = {
|
||||||
|
theseus-cli = utils.mkApp {
|
||||||
|
drv = inputs.self.packages.${system}.theseus-cli;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = with deps;
|
||||||
|
global ++ gui ++ shell;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
edition = "2018"
|
||||||
|
max_width = 80
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
pkgs.mkShell {
|
pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
rustc cargo openssl pkg-config
|
rustc cargo clippy openssl pkg-config
|
||||||
|
gtk4 gdk-pixbuf atk webkitgtk
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ once_cell = "1.9.0"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
argh = "0.1.6"
|
argh = "0.1.6"
|
||||||
|
pretty_assertions = "1.1.0"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "download-pack"
|
name = "download-pack"
|
||||||
|
|||||||
@ -45,7 +45,9 @@ pub async fn main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
fetch_modpack(&args.url, args.hash.as_deref(), &dest, ModpackSide::Client).await.unwrap();
|
fetch_modpack(&args.url, args.hash.as_deref(), &dest, ModpackSide::Client)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let end = start.elapsed();
|
let end = start.elapsed();
|
||||||
|
|
||||||
println!("Download completed in {} seconds", end.as_secs_f32());
|
println!("Download completed in {} seconds", end.as_secs_f32());
|
||||||
|
|||||||
@ -22,7 +22,8 @@ impl Metadata {
|
|||||||
let meta_path = Path::new(LAUNCHER_WORK_DIR).join(META_FILE);
|
let meta_path = Path::new(LAUNCHER_WORK_DIR).join(META_FILE);
|
||||||
|
|
||||||
if meta_path.exists() {
|
if meta_path.exists() {
|
||||||
let meta_data = std::fs::read_to_string(meta_path).ok()
|
let meta_data = std::fs::read_to_string(meta_path)
|
||||||
|
.ok()
|
||||||
.and_then(|x| serde_json::from_str::<Metadata>(&x).ok());
|
.and_then(|x| serde_json::from_str::<Metadata>(&x).ok());
|
||||||
|
|
||||||
if let Some(metadata) = meta_data {
|
if let Some(metadata) = meta_data {
|
||||||
@ -77,8 +78,14 @@ impl Metadata {
|
|||||||
"{}/minecraft/v0/manifest.json",
|
"{}/minecraft/v0/manifest.json",
|
||||||
META_URL
|
META_URL
|
||||||
))),
|
))),
|
||||||
daedalus::modded::fetch_manifest(&format!("{}/forge/v0/manifest.json", META_URL)),
|
daedalus::modded::fetch_manifest(&format!(
|
||||||
daedalus::modded::fetch_manifest(&format!("{}/fabric/v0/manifest.json", META_URL)),
|
"{}/forge/v0/manifest.json",
|
||||||
|
META_URL
|
||||||
|
)),
|
||||||
|
daedalus::modded::fetch_manifest(&format!(
|
||||||
|
"{}/fabric/v0/manifest.json",
|
||||||
|
META_URL
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@ -90,10 +97,12 @@ impl Metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
||||||
Ok(METADATA
|
let res = METADATA
|
||||||
.get()
|
.get()
|
||||||
.ok_or_else(|| DataError::InitializedError("metadata".to_string()))?
|
.ok_or_else(|| DataError::InitializedError("metadata".to_string()))?
|
||||||
.read()
|
.read()
|
||||||
.await)
|
.await;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
pub use meta::Metadata;
|
pub use meta::Metadata;
|
||||||
|
pub use profiles::{Profile, Profiles};
|
||||||
pub use settings::Settings;
|
pub use settings::Settings;
|
||||||
|
|
||||||
mod meta;
|
mod meta;
|
||||||
|
pub mod profiles;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@ -14,6 +16,9 @@ pub enum DataError {
|
|||||||
#[error("Daedalus error: {0}")]
|
#[error("Daedalus error: {0}")]
|
||||||
DaedalusError(#[from] daedalus::Error),
|
DaedalusError(#[from] daedalus::Error),
|
||||||
|
|
||||||
|
#[error("Data format error: {0}")]
|
||||||
|
FormatError(String),
|
||||||
|
|
||||||
#[error("Attempted to access {0} without initializing it!")]
|
#[error("Attempted to access {0} without initializing it!")]
|
||||||
InitializedError(String),
|
InitializedError(String),
|
||||||
|
|
||||||
|
|||||||
502
theseus/src/data/profiles.rs
Normal file
502
theseus/src/data/profiles.rs
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
use super::DataError;
|
||||||
|
use crate::launcher::ModLoader;
|
||||||
|
use daedalus::modded::LoaderVersion;
|
||||||
|
use futures::*;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
fs,
|
||||||
|
process::{Child, Command},
|
||||||
|
sync::{Mutex, RwLock, RwLockReadGuard},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PROFILES: OnceCell<RwLock<Profiles>> = OnceCell::new();
|
||||||
|
pub const PROFILE_JSON_PATH: &str = "profile.json";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Profiles(pub HashMap<PathBuf, Profile>);
|
||||||
|
|
||||||
|
// TODO: possibly add defaults to some of these values
|
||||||
|
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||||
|
pub const SUPPORTED_ICON_FORMATS: &[&'static str] = &[
|
||||||
|
"bmp", "gif", "jpeg", "jpg", "jpe", "png", "svg", "svgz", "webp", "rgb",
|
||||||
|
"mp4",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Profile {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub metadata: Metadata,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub java: Option<JavaSettings>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub memory: Option<MemorySettings>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub resolution: Option<WindowSize>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub hooks: Option<ProfileHooks>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Metadata {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub icon: Option<PathBuf>,
|
||||||
|
pub game_version: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub loader: ModLoader,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub loader_version: Option<LoaderVersion>,
|
||||||
|
pub format_version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct JavaSettings {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub install: Option<PathBuf>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub extra_arguments: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub struct MemorySettings {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub minimum: Option<u32>,
|
||||||
|
pub maximum: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MemorySettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
minimum: None,
|
||||||
|
maximum: 2048,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub struct WindowSize(pub u16, pub u16);
|
||||||
|
|
||||||
|
impl Default for WindowSize {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(854, 480)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ProfileHooks {
|
||||||
|
#[serde(skip_serializing_if = "HashSet::is_empty", default)]
|
||||||
|
pub pre_launch: HashSet<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub wrapper: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "HashSet::is_empty", default)]
|
||||||
|
pub post_exit: HashSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProfileHooks {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pre_launch: HashSet::<String>::new(),
|
||||||
|
wrapper: None,
|
||||||
|
post_exit: HashSet::<String>::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Profile {
|
||||||
|
pub async fn new(
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
path: PathBuf,
|
||||||
|
) -> Result<Self, DataError> {
|
||||||
|
if name.trim().is_empty() {
|
||||||
|
return Err(DataError::FormatError(String::from(
|
||||||
|
"Empty name for instance!",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
path: path.canonicalize()?,
|
||||||
|
metadata: Metadata {
|
||||||
|
name,
|
||||||
|
icon: None,
|
||||||
|
game_version: version,
|
||||||
|
loader: ModLoader::Vanilla,
|
||||||
|
loader_version: None,
|
||||||
|
format_version: CURRENT_FORMAT_VERSION,
|
||||||
|
},
|
||||||
|
java: None,
|
||||||
|
memory: None,
|
||||||
|
resolution: None,
|
||||||
|
hooks: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
credentials: &crate::launcher::Credentials,
|
||||||
|
) -> Result<Child, crate::launcher::LauncherError> {
|
||||||
|
use crate::launcher::LauncherError;
|
||||||
|
let (settings, version_info) = tokio::try_join! {
|
||||||
|
super::Settings::get(),
|
||||||
|
super::Metadata::get()
|
||||||
|
.and_then(|manifest| async move {
|
||||||
|
let version = manifest
|
||||||
|
.minecraft
|
||||||
|
.versions
|
||||||
|
.iter()
|
||||||
|
.find(|it| it.id == self.metadata.game_version.as_ref())
|
||||||
|
.ok_or_else(|| DataError::FormatError(format!(
|
||||||
|
"invalid or unknown version: {}",
|
||||||
|
self.metadata.game_version
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
Ok(daedalus::minecraft::fetch_version_info(version)
|
||||||
|
.await?)
|
||||||
|
})
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let ref pre_launch_hooks =
|
||||||
|
self.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
|
||||||
|
for hook in pre_launch_hooks.iter() {
|
||||||
|
// TODO: hook parameters
|
||||||
|
let mut cmd = hook.split(' ');
|
||||||
|
let result = Command::new(cmd.next().unwrap())
|
||||||
|
.args(&cmd.collect::<Vec<&str>>())
|
||||||
|
.current_dir(&self.path)
|
||||||
|
.spawn()?
|
||||||
|
.wait()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !result.success() {
|
||||||
|
return Err(LauncherError::ExitError(
|
||||||
|
result.code().unwrap_or(-1),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let java_install = match self.java {
|
||||||
|
Some(JavaSettings {
|
||||||
|
install: Some(ref install),
|
||||||
|
..
|
||||||
|
}) => install,
|
||||||
|
_ => if version_info
|
||||||
|
.java_version
|
||||||
|
.as_ref()
|
||||||
|
.filter(|it| it.major_version >= 16)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
settings.java_17_path.as_ref()
|
||||||
|
} else {
|
||||||
|
settings.java_8_path.as_ref()
|
||||||
|
}
|
||||||
|
.ok_or_else(|| {
|
||||||
|
LauncherError::JavaError(format!(
|
||||||
|
"No Java installed for version {}",
|
||||||
|
version_info.java_version.map_or(8, |it| it.major_version),
|
||||||
|
))
|
||||||
|
})?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !java_install.exists() {
|
||||||
|
return Err(LauncherError::JavaError(format!(
|
||||||
|
"Could not find java install: {}",
|
||||||
|
java_install.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let java_args = &self
|
||||||
|
.java
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|it| it.extra_arguments.as_ref())
|
||||||
|
.unwrap_or(&settings.custom_java_args);
|
||||||
|
|
||||||
|
let wrapper = self
|
||||||
|
.hooks
|
||||||
|
.as_ref()
|
||||||
|
.map_or(&settings.hooks.wrapper, |it| &it.wrapper);
|
||||||
|
|
||||||
|
let ref memory = self.memory.unwrap_or(settings.memory);
|
||||||
|
let ref resolution =
|
||||||
|
self.resolution.unwrap_or(settings.game_resolution);
|
||||||
|
|
||||||
|
crate::launcher::launch_minecraft(
|
||||||
|
&self.metadata.game_version,
|
||||||
|
&self.metadata.loader_version,
|
||||||
|
&self.path,
|
||||||
|
&java_install,
|
||||||
|
&java_args,
|
||||||
|
&wrapper,
|
||||||
|
memory,
|
||||||
|
resolution,
|
||||||
|
credentials,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn kill(
|
||||||
|
&self,
|
||||||
|
running: &mut Child,
|
||||||
|
) -> Result<(), crate::launcher::LauncherError> {
|
||||||
|
running.kill().await?;
|
||||||
|
self.wait_for(running).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for(
|
||||||
|
&self,
|
||||||
|
running: &mut Child,
|
||||||
|
) -> Result<(), crate::launcher::LauncherError> {
|
||||||
|
let result = running.wait().await.map_err(|err| {
|
||||||
|
crate::launcher::LauncherError::ProcessError {
|
||||||
|
inner: err,
|
||||||
|
process: String::from("minecraft"),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match result.success() {
|
||||||
|
false => Err(crate::launcher::LauncherError::ExitError(
|
||||||
|
result.code().unwrap_or(-1),
|
||||||
|
)),
|
||||||
|
true => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: deduplicate these builder methods
|
||||||
|
// They are flat like this in order to allow builder-style usage
|
||||||
|
pub fn with_name(&mut self, name: String) -> &mut Self {
|
||||||
|
self.metadata.name = name;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn with_icon(
|
||||||
|
&mut self,
|
||||||
|
icon: &Path,
|
||||||
|
) -> Result<&mut Self, DataError> {
|
||||||
|
let ext = icon
|
||||||
|
.extension()
|
||||||
|
.and_then(std::ffi::OsStr::to_str)
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
if SUPPORTED_ICON_FORMATS.contains(&ext) {
|
||||||
|
let file_name = format!("icon.{ext}");
|
||||||
|
fs::copy(icon, &self.path.join(&file_name)).await?;
|
||||||
|
self.metadata.icon =
|
||||||
|
Some(Path::new(&format!("./{file_name}")).to_owned());
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
} else {
|
||||||
|
Err(DataError::FormatError(format!(
|
||||||
|
"Unsupported image type: {ext}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
||||||
|
self.metadata.game_version = version;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_loader(
|
||||||
|
&mut self,
|
||||||
|
loader: ModLoader,
|
||||||
|
version: Option<LoaderVersion>,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.metadata.loader = loader;
|
||||||
|
self.metadata.loader_version = version;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_java_settings(
|
||||||
|
&mut self,
|
||||||
|
settings: Option<JavaSettings>,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.java = settings;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_memory(
|
||||||
|
&mut self,
|
||||||
|
settings: Option<MemorySettings>,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.memory = settings;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_resolution(
|
||||||
|
&mut self,
|
||||||
|
resolution: Option<WindowSize>,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.resolution = resolution;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_hooks(&mut self, hooks: Option<ProfileHooks>) -> &mut Self {
|
||||||
|
self.hooks = hooks;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Profiles {
|
||||||
|
pub async fn init() -> Result<(), DataError> {
|
||||||
|
let settings = super::Settings::get().await?;
|
||||||
|
let profiles = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
|
let futures = settings.profiles.clone().into_iter().map(|path| async {
|
||||||
|
let profiles = Arc::clone(&profiles);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// TODO: handle missing profiles
|
||||||
|
let mut profiles = profiles.lock().await;
|
||||||
|
let profile = Self::read_profile_from_dir(path.clone()).await?;
|
||||||
|
|
||||||
|
profiles.insert(path, profile);
|
||||||
|
Ok(()) as Result<_, DataError>
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
futures::future::try_join_all(futures).await?;
|
||||||
|
|
||||||
|
PROFILES.get_or_init(|| {
|
||||||
|
RwLock::new(Profiles(
|
||||||
|
Arc::try_unwrap(profiles).unwrap().into_inner(),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(profile: Profile) -> Result<(), DataError> {
|
||||||
|
let mut profiles = PROFILES
|
||||||
|
.get()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
DataError::InitializedError(String::from("profiles"))
|
||||||
|
})?
|
||||||
|
.write()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
super::Settings::get_mut()
|
||||||
|
.await?
|
||||||
|
.profiles
|
||||||
|
.insert(profile.path.clone());
|
||||||
|
profiles.0.insert(profile.path.clone(), profile);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_from(path: PathBuf) -> Result<(), DataError> {
|
||||||
|
Self::read_profile_from_dir(path)
|
||||||
|
.and_then(Self::insert)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove(path: &Path) -> Result<Option<Profile>, DataError> {
|
||||||
|
let path = path.canonicalize()?;
|
||||||
|
let mut profiles = PROFILES.get().unwrap().write().await;
|
||||||
|
super::Settings::get_mut().await?.profiles.remove(&path);
|
||||||
|
Ok(profiles.0.remove(&path))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save() -> Result<(), DataError> {
|
||||||
|
let profiles = Self::get().await?;
|
||||||
|
|
||||||
|
let futures = profiles.0.clone().into_iter().map(|(path, profile)| {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let json = tokio::task::spawn_blocking(move || {
|
||||||
|
serde_json::to_vec_pretty(&profile)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()?;
|
||||||
|
|
||||||
|
let profile_json_path = path.join(PROFILE_JSON_PATH);
|
||||||
|
fs::write(profile_json_path, json).await?;
|
||||||
|
Ok(()) as Result<(), DataError>
|
||||||
|
})
|
||||||
|
});
|
||||||
|
futures::future::try_join_all(futures)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<_, DataError>>()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
||||||
|
Ok(PROFILES
|
||||||
|
.get()
|
||||||
|
.ok_or_else(|| DataError::InitializedError("profiles".to_string()))?
|
||||||
|
.read()
|
||||||
|
.await)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_profile_from_dir(
|
||||||
|
path: PathBuf,
|
||||||
|
) -> Result<Profile, DataError> {
|
||||||
|
let json = fs::read(path.join(PROFILE_JSON_PATH)).await?;
|
||||||
|
let mut profile = serde_json::from_slice::<Profile>(&json)?;
|
||||||
|
profile.path = path.clone();
|
||||||
|
Ok(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profile_test() -> Result<(), serde_json::Error> {
|
||||||
|
let profile = Profile {
|
||||||
|
path: PathBuf::from("/tmp/nunya/beeswax"),
|
||||||
|
metadata: Metadata {
|
||||||
|
name: String::from("Example Pack"),
|
||||||
|
icon: None,
|
||||||
|
game_version: String::from("1.18.2"),
|
||||||
|
loader: ModLoader::Vanilla,
|
||||||
|
loader_version: None,
|
||||||
|
format_version: CURRENT_FORMAT_VERSION,
|
||||||
|
},
|
||||||
|
java: JavaSettings {
|
||||||
|
install: PathBuf::from("/usr/bin/java"),
|
||||||
|
extra_arguments: Vec::new(),
|
||||||
|
},
|
||||||
|
memory: MemorySettings {
|
||||||
|
minimum: None,
|
||||||
|
maximum: 8192,
|
||||||
|
},
|
||||||
|
resolution: WindowSize(1920, 1080),
|
||||||
|
hooks: ProfileHooks {
|
||||||
|
pre_launch: HashSet::new(),
|
||||||
|
wrapper: None,
|
||||||
|
post_exit: HashSet::new(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"path": "/tmp/nunya/beeswax",
|
||||||
|
"metadata": {
|
||||||
|
"name": "Example Pack",
|
||||||
|
"game_version": "1.18.2",
|
||||||
|
"format_version": 1u32,
|
||||||
|
},
|
||||||
|
"java": {
|
||||||
|
"install": "/usr/bin/java",
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"maximum": 8192u32,
|
||||||
|
},
|
||||||
|
"resolution": (1920u16, 1080u16),
|
||||||
|
"hooks": {},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(serde_json::to_value(profile.clone())?, json.clone());
|
||||||
|
assert_str_eq!(
|
||||||
|
format!("{:?}", serde_json::from_value::<Profile>(json)?),
|
||||||
|
format!("{:?}", profile),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,33 +1,51 @@
|
|||||||
use std::path::Path;
|
use super::profiles::*;
|
||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{data::DataError, LAUNCHER_WORK_DIR};
|
use crate::{data::DataError, LAUNCHER_WORK_DIR};
|
||||||
use once_cell::sync;
|
use once_cell::sync;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
|
||||||
const SETTINGS_FILE: &str = "settings.json";
|
const SETTINGS_FILE: &str = "settings.json";
|
||||||
|
const ICONS_PATH: &str = "icons";
|
||||||
|
const METADATA_DIR: &str = "meta";
|
||||||
|
|
||||||
static SETTINGS: sync::OnceCell<RwLock<Settings>> = sync::OnceCell::new();
|
static SETTINGS: sync::OnceCell<RwLock<Settings>> = sync::OnceCell::new();
|
||||||
|
pub const FORMAT_VERSION: u32 = 1;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub memory: i32,
|
pub memory: MemorySettings,
|
||||||
pub game_resolution: (i32, i32),
|
pub game_resolution: WindowSize,
|
||||||
pub custom_java_args: String,
|
pub custom_java_args: Vec<String>,
|
||||||
pub java_8_path: Option<String>,
|
pub java_8_path: Option<PathBuf>,
|
||||||
pub java_17_path: Option<String>,
|
pub java_17_path: Option<PathBuf>,
|
||||||
pub wrapper_command: Option<String>,
|
pub hooks: ProfileHooks,
|
||||||
|
pub icon_path: PathBuf,
|
||||||
|
pub metadata_dir: PathBuf,
|
||||||
|
pub profiles: HashSet<PathBuf>,
|
||||||
|
pub max_concurrent_downloads: usize,
|
||||||
|
pub version: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
memory: 2048,
|
memory: MemorySettings::default(),
|
||||||
game_resolution: (854, 480),
|
game_resolution: WindowSize::default(),
|
||||||
custom_java_args: "".to_string(),
|
custom_java_args: Vec::new(),
|
||||||
java_8_path: None,
|
java_8_path: None,
|
||||||
java_17_path: None,
|
java_17_path: None,
|
||||||
wrapper_command: None,
|
hooks: ProfileHooks::default(),
|
||||||
|
icon_path: Path::new(LAUNCHER_WORK_DIR).join(ICONS_PATH),
|
||||||
|
metadata_dir: Path::new(LAUNCHER_WORK_DIR).join(METADATA_DIR),
|
||||||
|
profiles: HashSet::new(),
|
||||||
|
max_concurrent_downloads: 32,
|
||||||
|
version: FORMAT_VERSION,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,10 +68,14 @@ impl Settings {
|
|||||||
if SETTINGS.get().is_none() {
|
if SETTINGS.get().is_none() {
|
||||||
let new = Self::default();
|
let new = Self::default();
|
||||||
|
|
||||||
std::fs::write(
|
tokio::fs::rename(SETTINGS_FILE, format!("{SETTINGS_FILE}.bak"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tokio::fs::write(
|
||||||
Path::new(LAUNCHER_WORK_DIR).join(SETTINGS_FILE),
|
Path::new(LAUNCHER_WORK_DIR).join(SETTINGS_FILE),
|
||||||
&serde_json::to_string(&new)?,
|
&serde_json::to_string(&new)?,
|
||||||
)?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
SETTINGS.get_or_init(|| RwLock::new(new));
|
SETTINGS.get_or_init(|| RwLock::new(new));
|
||||||
}
|
}
|
||||||
@ -66,7 +88,7 @@ impl Settings {
|
|||||||
Path::new(LAUNCHER_WORK_DIR).join(SETTINGS_FILE),
|
Path::new(LAUNCHER_WORK_DIR).join(SETTINGS_FILE),
|
||||||
)?)?;
|
)?)?;
|
||||||
|
|
||||||
let write = &mut *SETTINGS
|
let mut write = SETTINGS
|
||||||
.get()
|
.get()
|
||||||
.ok_or_else(|| DataError::InitializedError("settings".to_string()))?
|
.ok_or_else(|| DataError::InitializedError("settings".to_string()))?
|
||||||
.write()
|
.write()
|
||||||
@ -82,17 +104,24 @@ impl Settings {
|
|||||||
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
Path::new(LAUNCHER_WORK_DIR).join(SETTINGS_FILE),
|
Path::new(LAUNCHER_WORK_DIR).join(SETTINGS_FILE),
|
||||||
&serde_json::to_string(&*settings)?,
|
&serde_json::to_string_pretty(&*settings)?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
pub async fn get<'a>() -> Result<RwLockReadGuard<'a, Self>, DataError> {
|
||||||
Ok(SETTINGS
|
Ok(Self::get_or_uninit::<'a>()?.read().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_mut<'a>() -> Result<RwLockWriteGuard<'a, Self>, DataError>
|
||||||
|
{
|
||||||
|
Ok(Self::get_or_uninit::<'a>()?.write().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_or_uninit<'a>() -> Result<&'a RwLock<Self>, DataError> {
|
||||||
|
SETTINGS
|
||||||
.get()
|
.get()
|
||||||
.ok_or_else(|| DataError::InitializedError("settings".to_string()))?
|
.ok_or_else(|| DataError::InitializedError("settings".to_string()))
|
||||||
.read()
|
|
||||||
.await)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::data::profiles::*;
|
||||||
use crate::launcher::auth::provider::Credentials;
|
use crate::launcher::auth::provider::Credentials;
|
||||||
use crate::launcher::rules::parse_rules;
|
use crate::launcher::rules::parse_rules;
|
||||||
use crate::launcher::LauncherError;
|
use crate::launcher::LauncherError;
|
||||||
@ -21,19 +22,22 @@ pub fn get_class_paths(
|
|||||||
libraries: &[Library],
|
libraries: &[Library],
|
||||||
client_path: &Path,
|
client_path: &Path,
|
||||||
) -> Result<String, LauncherError> {
|
) -> Result<String, LauncherError> {
|
||||||
let mut class_paths = libraries.iter().filter_map(|library| {
|
let mut class_paths = libraries
|
||||||
if let Some(rules) = &library.rules {
|
.iter()
|
||||||
if !super::rules::parse_rules(rules.as_slice()) {
|
.filter_map(|library| {
|
||||||
|
if let Some(rules) = &library.rules {
|
||||||
|
if !super::rules::parse_rules(rules.as_slice()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !library.include_in_classpath {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !library.include_in_classpath {
|
Some(get_lib_path(libraries_path, &library.name))
|
||||||
return None;
|
})
|
||||||
}
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
Some(get_lib_path(libraries_path, &library.name))
|
|
||||||
}).collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
class_paths.push(
|
class_paths.push(
|
||||||
crate::util::absolute_path(&client_path)
|
crate::util::absolute_path(&client_path)
|
||||||
@ -54,9 +58,10 @@ pub fn get_class_paths_jar<T: AsRef<str>>(
|
|||||||
libraries_path: &Path,
|
libraries_path: &Path,
|
||||||
libraries: &[T],
|
libraries: &[T],
|
||||||
) -> Result<String, LauncherError> {
|
) -> Result<String, LauncherError> {
|
||||||
let class_paths = libraries.iter().map(|library| {
|
let class_paths = libraries
|
||||||
get_lib_path(libraries_path, library.as_ref())
|
.iter()
|
||||||
}).collect::<Result<Vec<_>, _>>()?;
|
.map(|library| get_lib_path(libraries_path, library.as_ref()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
Ok(class_paths.join(get_cp_separator()))
|
Ok(class_paths.join(get_cp_separator()))
|
||||||
}
|
}
|
||||||
@ -90,7 +95,7 @@ pub fn get_jvm_arguments(
|
|||||||
libraries_path: &Path,
|
libraries_path: &Path,
|
||||||
class_paths: &str,
|
class_paths: &str,
|
||||||
version_name: &str,
|
version_name: &str,
|
||||||
memory: i32,
|
memory: MemorySettings,
|
||||||
custom_args: Vec<String>,
|
custom_args: Vec<String>,
|
||||||
) -> Result<Vec<String>, LauncherError> {
|
) -> Result<Vec<String>, LauncherError> {
|
||||||
let mut parsed_arguments = Vec::new();
|
let mut parsed_arguments = Vec::new();
|
||||||
@ -120,7 +125,10 @@ pub fn get_jvm_arguments(
|
|||||||
parsed_arguments.push(class_paths.to_string());
|
parsed_arguments.push(class_paths.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed_arguments.push(format!("-Xmx{}M", memory));
|
if let Some(minimum) = memory.minimum {
|
||||||
|
parsed_arguments.push(format!("-Xms{minimum}M"));
|
||||||
|
}
|
||||||
|
parsed_arguments.push(format!("-Xmx{}M", memory.maximum));
|
||||||
for arg in custom_args {
|
for arg in custom_args {
|
||||||
if !arg.is_empty() {
|
if !arg.is_empty() {
|
||||||
parsed_arguments.push(arg);
|
parsed_arguments.push(arg);
|
||||||
@ -148,8 +156,7 @@ fn parse_jvm_argument(
|
|||||||
natives_path.to_string_lossy()
|
natives_path.to_string_lossy()
|
||||||
))
|
))
|
||||||
})?
|
})?
|
||||||
.to_string_lossy()
|
.to_string_lossy(),
|
||||||
.to_string(),
|
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"${library_directory}",
|
"${library_directory}",
|
||||||
@ -180,7 +187,7 @@ pub fn get_minecraft_arguments(
|
|||||||
game_directory: &Path,
|
game_directory: &Path,
|
||||||
assets_directory: &Path,
|
assets_directory: &Path,
|
||||||
version_type: &VersionType,
|
version_type: &VersionType,
|
||||||
resolution: (i32, i32),
|
resolution: WindowSize,
|
||||||
) -> Result<Vec<String>, LauncherError> {
|
) -> Result<Vec<String>, LauncherError> {
|
||||||
if let Some(arguments) = arguments {
|
if let Some(arguments) = arguments {
|
||||||
let mut parsed_arguments = Vec::new();
|
let mut parsed_arguments = Vec::new();
|
||||||
@ -234,7 +241,7 @@ fn parse_minecraft_argument(
|
|||||||
game_directory: &Path,
|
game_directory: &Path,
|
||||||
assets_directory: &Path,
|
assets_directory: &Path,
|
||||||
version_type: &VersionType,
|
version_type: &VersionType,
|
||||||
resolution: (i32, i32),
|
resolution: WindowSize,
|
||||||
) -> Result<String, LauncherError> {
|
) -> Result<String, LauncherError> {
|
||||||
Ok(argument
|
Ok(argument
|
||||||
.replace("${auth_access_token}", access_token)
|
.replace("${auth_access_token}", access_token)
|
||||||
@ -255,7 +262,7 @@ fn parse_minecraft_argument(
|
|||||||
))
|
))
|
||||||
})?
|
})?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"${assets_root}",
|
"${assets_root}",
|
||||||
@ -267,7 +274,7 @@ fn parse_minecraft_argument(
|
|||||||
))
|
))
|
||||||
})?
|
})?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"${game_assets}",
|
"${game_assets}",
|
||||||
@ -279,7 +286,7 @@ fn parse_minecraft_argument(
|
|||||||
))
|
))
|
||||||
})?
|
})?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.replace("${version_type}", version_type.as_str())
|
.replace("${version_type}", version_type.as_str())
|
||||||
.replace("${resolution_width}", &resolution.0.to_string())
|
.replace("${resolution_width}", &resolution.0.to_string())
|
||||||
|
|||||||
@ -1,15 +1,38 @@
|
|||||||
use crate::launcher::LauncherError;
|
use crate::{
|
||||||
|
data::{DataError, Settings},
|
||||||
|
launcher::LauncherError,
|
||||||
|
};
|
||||||
use daedalus::get_path_from_artifact;
|
use daedalus::get_path_from_artifact;
|
||||||
use daedalus::minecraft::{
|
use daedalus::minecraft::{
|
||||||
fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType, Library, Os, Version,
|
fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType,
|
||||||
VersionInfo,
|
Library, Os, Version, VersionInfo,
|
||||||
|
};
|
||||||
|
use daedalus::modded::{
|
||||||
|
fetch_partial_version, merge_partial_version, LoaderVersion,
|
||||||
};
|
};
|
||||||
use daedalus::modded::{fetch_partial_version, merge_partial_version, LoaderVersion};
|
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::{
|
||||||
|
fs::File,
|
||||||
|
io::AsyncWriteExt,
|
||||||
|
sync::{OnceCell, Semaphore},
|
||||||
|
};
|
||||||
|
|
||||||
|
static DOWNLOADS_SEMAPHORE: OnceCell<Semaphore> = OnceCell::const_new();
|
||||||
|
|
||||||
|
pub async fn init() -> Result<(), DataError> {
|
||||||
|
DOWNLOADS_SEMAPHORE
|
||||||
|
.get_or_try_init(|| async {
|
||||||
|
let settings = Settings::get().await?;
|
||||||
|
Ok::<_, DataError>(Semaphore::new(
|
||||||
|
settings.max_concurrent_downloads,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn download_version_info(
|
pub async fn download_version_info(
|
||||||
client_path: &Path,
|
client_path: &Path,
|
||||||
@ -22,8 +45,7 @@ pub async fn download_version_info(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut path = client_path.join(id);
|
let mut path = client_path.join(id);
|
||||||
path.push(id);
|
path.push(&format!("{id}.json"));
|
||||||
path.set_extension("json");
|
|
||||||
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let contents = std::fs::read_to_string(path)?;
|
let contents = std::fs::read_to_string(path)?;
|
||||||
@ -37,7 +59,7 @@ pub async fn download_version_info(
|
|||||||
info.id = loader_version.id.clone();
|
info.id = loader_version.id.clone();
|
||||||
}
|
}
|
||||||
let info_s = serde_json::to_string(&info)?;
|
let info_s = serde_json::to_string(&info)?;
|
||||||
save_file(&path, &bytes::Bytes::from(info_s))?;
|
save_file(&path, &bytes::Bytes::from(info_s)).await?;
|
||||||
|
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
@ -58,10 +80,14 @@ pub async fn download_client(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut path = client_path.join(version);
|
let mut path = client_path.join(version);
|
||||||
path.push(version);
|
path.push(&format!("{version}.jar"));
|
||||||
path.set_extension("jar");
|
|
||||||
|
|
||||||
save_and_download_file(&path, &client_download.url, Some(&client_download.sha1)).await?;
|
save_and_download_file(
|
||||||
|
&path,
|
||||||
|
&client_download.url,
|
||||||
|
Some(&client_download.sha1),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +95,8 @@ pub async fn download_assets_index(
|
|||||||
assets_path: &Path,
|
assets_path: &Path,
|
||||||
version: &VersionInfo,
|
version: &VersionInfo,
|
||||||
) -> Result<AssetsIndex, LauncherError> {
|
) -> Result<AssetsIndex, LauncherError> {
|
||||||
let path = assets_path.join(format!("indexes/{}.json", &version.asset_index.id));
|
let path =
|
||||||
|
assets_path.join(format!("indexes/{}.json", &version.asset_index.id));
|
||||||
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
let content = std::fs::read_to_string(path)?;
|
let content = std::fs::read_to_string(path)?;
|
||||||
@ -77,7 +104,8 @@ pub async fn download_assets_index(
|
|||||||
} else {
|
} else {
|
||||||
let index = fetch_assets_index(version).await?;
|
let index = fetch_assets_index(version).await?;
|
||||||
|
|
||||||
save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))?;
|
save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(index)
|
Ok(index)
|
||||||
}
|
}
|
||||||
@ -88,12 +116,9 @@ pub async fn download_assets(
|
|||||||
legacy_path: Option<&Path>,
|
legacy_path: Option<&Path>,
|
||||||
index: &AssetsIndex,
|
index: &AssetsIndex,
|
||||||
) -> Result<(), LauncherError> {
|
) -> Result<(), LauncherError> {
|
||||||
future::join_all(
|
future::join_all(index.objects.iter().map(|(name, asset)| {
|
||||||
index
|
download_asset(assets_path, legacy_path, name, asset)
|
||||||
.objects
|
}))
|
||||||
.iter()
|
|
||||||
.map(|(name, asset)| download_asset(assets_path, legacy_path, name, asset)),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Result<Vec<()>, LauncherError>>()?;
|
.collect::<Result<Vec<()>, LauncherError>>()?;
|
||||||
@ -114,14 +139,16 @@ async fn download_asset(
|
|||||||
resource_path.push(sub_hash);
|
resource_path.push(sub_hash);
|
||||||
resource_path.push(hash);
|
resource_path.push(hash);
|
||||||
|
|
||||||
let url = format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
|
let url =
|
||||||
|
format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
|
||||||
|
|
||||||
let resource = save_and_download_file(&resource_path, &url, Some(hash)).await?;
|
let resource =
|
||||||
|
save_and_download_file(&resource_path, &url, Some(hash)).await?;
|
||||||
|
|
||||||
if let Some(legacy_path) = legacy_path {
|
if let Some(legacy_path) = legacy_path {
|
||||||
let resource_path =
|
let resource_path = legacy_path
|
||||||
legacy_path.join(name.replace('/', &std::path::MAIN_SEPARATOR.to_string()));
|
.join(name.replace('/', &std::path::MAIN_SEPARATOR.to_string()));
|
||||||
save_file(resource_path.as_path(), &resource)?;
|
save_file(resource_path.as_path(), &resource).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -132,11 +159,9 @@ pub async fn download_libraries(
|
|||||||
natives_path: &Path,
|
natives_path: &Path,
|
||||||
libraries: &[Library],
|
libraries: &[Library],
|
||||||
) -> Result<(), LauncherError> {
|
) -> Result<(), LauncherError> {
|
||||||
future::join_all(
|
future::join_all(libraries.iter().map(|library| {
|
||||||
libraries
|
download_library(libraries_path, natives_path, library)
|
||||||
.iter()
|
}))
|
||||||
.map(|library| download_library(libraries_path, natives_path, library)),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Result<Vec<()>, LauncherError>>()?;
|
.collect::<Result<Vec<()>, LauncherError>>()?;
|
||||||
@ -173,7 +198,8 @@ async fn download_library_jar(
|
|||||||
|
|
||||||
if let Some(downloads) = &library.downloads {
|
if let Some(downloads) = &library.downloads {
|
||||||
if let Some(library) = &downloads.artifact {
|
if let Some(library) = &downloads.artifact {
|
||||||
save_and_download_file(&path, &library.url, Some(&library.sha1)).await?;
|
save_and_download_file(&path, &library.url, Some(&library.sha1))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
@ -189,16 +215,21 @@ async fn download_library_jar(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_native(natives_path: &Path, library: &Library) -> Result<(), LauncherError> {
|
async fn download_native(
|
||||||
|
natives_path: &Path,
|
||||||
|
library: &Library,
|
||||||
|
) -> Result<(), LauncherError> {
|
||||||
use daedalus::minecraft::LibraryDownload;
|
use daedalus::minecraft::LibraryDownload;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// Try blocks in stable Rust when?
|
// Try blocks in stable Rust when?
|
||||||
let optional_cascade = || -> Option<(&String, &HashMap<String, LibraryDownload>)> {
|
let optional_cascade =
|
||||||
let os_key = library.natives.as_ref()?.get(&get_os())?;
|
|| -> Option<(&String, &HashMap<String, LibraryDownload>)> {
|
||||||
let classifiers = library.downloads.as_ref()?.classifiers.as_ref()?;
|
let os_key = library.natives.as_ref()?.get(&get_os())?;
|
||||||
Some((os_key, classifiers))
|
let classifiers =
|
||||||
};
|
library.downloads.as_ref()?.classifiers.as_ref()?;
|
||||||
|
Some((os_key, classifiers))
|
||||||
|
};
|
||||||
|
|
||||||
if let Some((os_key, classifiers)) = optional_cascade() {
|
if let Some((os_key, classifiers)) = optional_cascade() {
|
||||||
#[cfg(target_pointer_width = "64")]
|
#[cfg(target_pointer_width = "64")]
|
||||||
@ -227,19 +258,25 @@ async fn save_and_download_file(
|
|||||||
Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
|
Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let file = download_file(url, sha1).await?;
|
let file = download_file(url, sha1).await?;
|
||||||
save_file(path, &file)?;
|
save_file(path, &file).await?;
|
||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_file(path: &Path, bytes: &bytes::Bytes) -> std::io::Result<()> {
|
async fn save_file(path: &Path, bytes: &bytes::Bytes) -> std::io::Result<()> {
|
||||||
|
let _save_permit = DOWNLOADS_SEMAPHORE
|
||||||
|
.get()
|
||||||
|
.expect("File operation semaphore not initialized!")
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent)?;
|
tokio::fs::create_dir_all(parent).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut file = File::create(path)?;
|
let mut file = File::create(path).await?;
|
||||||
file.write_all(bytes)?;
|
file.write_all(bytes).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +289,17 @@ pub fn get_os() -> Os {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes, LauncherError> {
|
pub async fn download_file(
|
||||||
|
url: &str,
|
||||||
|
sha1: Option<&str>,
|
||||||
|
) -> Result<bytes::Bytes, LauncherError> {
|
||||||
|
let _download_permit = DOWNLOADS_SEMAPHORE
|
||||||
|
.get()
|
||||||
|
.expect("File operation semaphore not initialized!")
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.tcp_keepalive(Some(Duration::from_secs(10)))
|
.tcp_keepalive(Some(Duration::from_secs(10)))
|
||||||
.build()
|
.build()
|
||||||
@ -307,7 +354,9 @@ pub async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes
|
|||||||
|
|
||||||
/// Computes a checksum of the input bytes
|
/// Computes a checksum of the input bytes
|
||||||
async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
|
async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
|
||||||
let hash = tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest()).await?;
|
let hash =
|
||||||
|
tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest())
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(hash)
|
Ok(hash)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
use crate::launcher::LauncherError;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
|
|
||||||
pub fn check_java() -> Result<String, LauncherError> {
|
|
||||||
let child = Command::new("java")
|
|
||||||
.arg("-version")
|
|
||||||
.output()
|
|
||||||
.map_err(|inner| LauncherError::ProcessError {
|
|
||||||
inner,
|
|
||||||
process: "java".into(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let output = String::from_utf8_lossy(&child.stderr);
|
|
||||||
let output = output.trim_matches('\"');
|
|
||||||
Ok(output.into())
|
|
||||||
}
|
|
||||||
@ -1,20 +1,22 @@
|
|||||||
use daedalus::minecraft::{ArgumentType, VersionInfo};
|
use daedalus::minecraft::{ArgumentType, VersionInfo};
|
||||||
|
use daedalus::modded::LoaderVersion;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
use std::{path::Path, process::Stdio};
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio::process::{Child, Command};
|
||||||
|
|
||||||
pub use crate::launcher::auth::provider::Credentials;
|
pub use crate::launcher::auth::provider::Credentials;
|
||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
mod auth;
|
pub mod auth;
|
||||||
mod download;
|
mod download;
|
||||||
mod java;
|
|
||||||
mod rules;
|
mod rules;
|
||||||
|
|
||||||
|
pub(crate) use download::init as init_download_semaphore;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum LauncherError {
|
pub enum LauncherError {
|
||||||
#[error("Failed to violate file checksum at url {url} with hash {hash} after {tries} tries")]
|
#[error("Failed to validate file checksum at url {url} with hash {hash} after {tries} tries")]
|
||||||
ChecksumFailure {
|
ChecksumFailure {
|
||||||
hash: String,
|
hash: String,
|
||||||
url: String,
|
url: String,
|
||||||
@ -56,8 +58,12 @@ pub enum LauncherError {
|
|||||||
|
|
||||||
#[error("Java error: {0}")]
|
#[error("Java error: {0}")]
|
||||||
JavaError(String),
|
JavaError(String),
|
||||||
|
|
||||||
|
#[error("Command exited with non-zero exit code: {0}")]
|
||||||
|
ExitError(i32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this probably should be in crate::data
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
|
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ModLoader {
|
pub enum ModLoader {
|
||||||
@ -72,80 +78,64 @@ impl Default for ModLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn launch_minecraft(
|
impl std::fmt::Display for ModLoader {
|
||||||
version_name: &str,
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
mod_loader: Option<ModLoader>,
|
let repr = match self {
|
||||||
root_dir: &Path,
|
&Self::Vanilla => "Vanilla",
|
||||||
credentials: &Credentials,
|
&Self::Forge => "Forge",
|
||||||
) -> Result<(), LauncherError> {
|
&Self::Fabric => "Fabric",
|
||||||
let metadata = crate::data::Metadata::get().await?;
|
};
|
||||||
let settings = crate::data::Settings::get().await?;
|
|
||||||
|
|
||||||
let versions_path = crate::util::absolute_path(root_dir.join("versions"))?;
|
f.write_str(repr)
|
||||||
let libraries_path = crate::util::absolute_path(root_dir.join("libraries"))?;
|
}
|
||||||
let assets_path = crate::util::absolute_path(root_dir.join("assets"))?;
|
}
|
||||||
let legacy_assets_path = crate::util::absolute_path(root_dir.join("resources"))?;
|
|
||||||
|
pub async fn launch_minecraft(
|
||||||
|
game_version: &str,
|
||||||
|
loader_version: &Option<LoaderVersion>,
|
||||||
|
root_dir: &Path,
|
||||||
|
java: &Path,
|
||||||
|
java_args: &Vec<String>,
|
||||||
|
wrapper: &Option<String>,
|
||||||
|
memory: &crate::data::profiles::MemorySettings,
|
||||||
|
resolution: &crate::data::profiles::WindowSize,
|
||||||
|
credentials: &Credentials,
|
||||||
|
) -> Result<Child, LauncherError> {
|
||||||
|
let (metadata, settings) = futures::try_join! {
|
||||||
|
crate::data::Metadata::get(),
|
||||||
|
crate::data::Settings::get(),
|
||||||
|
}?;
|
||||||
|
let root_dir = root_dir.canonicalize()?;
|
||||||
|
let metadata_dir = &settings.metadata_dir;
|
||||||
|
|
||||||
|
let (
|
||||||
|
versions_path,
|
||||||
|
libraries_path,
|
||||||
|
assets_path,
|
||||||
|
legacy_assets_path,
|
||||||
|
natives_path,
|
||||||
|
) = (
|
||||||
|
metadata_dir.join("versions"),
|
||||||
|
metadata_dir.join("libraries"),
|
||||||
|
metadata_dir.join("assets"),
|
||||||
|
metadata_dir.join("resources"),
|
||||||
|
metadata_dir.join("natives"),
|
||||||
|
);
|
||||||
|
|
||||||
let version = metadata
|
let version = metadata
|
||||||
.minecraft
|
.minecraft
|
||||||
.versions
|
.versions
|
||||||
.iter()
|
.iter()
|
||||||
.find(|x| x.id == version_name)
|
.find(|it| it.id == game_version)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
LauncherError::InvalidInput(format!("Version {} does not exist", version_name))
|
LauncherError::InvalidInput(format!(
|
||||||
|
"Invalid game version: {game_version}",
|
||||||
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let loader_version = match mod_loader.unwrap_or_default() {
|
let version_jar = loader_version
|
||||||
ModLoader::Vanilla => None,
|
.as_ref()
|
||||||
ModLoader::Forge | ModLoader::Fabric => {
|
.map_or(version.id.clone(), |it| it.id.clone());
|
||||||
let loaders = if mod_loader.unwrap_or_default() == ModLoader::Forge {
|
|
||||||
&metadata
|
|
||||||
.forge
|
|
||||||
.game_versions
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.id == version_name)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
LauncherError::InvalidInput(format!(
|
|
||||||
"Version {} for mod loader Forge does not exist",
|
|
||||||
version_name
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.loaders
|
|
||||||
} else {
|
|
||||||
&metadata
|
|
||||||
.fabric
|
|
||||||
.game_versions
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.id == version_name)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
LauncherError::InvalidInput(format!(
|
|
||||||
"Version {} for mod loader Fabric does not exist",
|
|
||||||
version_name
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.loaders
|
|
||||||
};
|
|
||||||
|
|
||||||
let loader = if let Some(version) = loaders.iter().find(|x| x.stable) {
|
|
||||||
Some(version.clone())
|
|
||||||
} else {
|
|
||||||
loaders.first().cloned()
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(loader.ok_or_else(|| {
|
|
||||||
LauncherError::InvalidInput(format!(
|
|
||||||
"No mod loader version found for version {}",
|
|
||||||
version_name
|
|
||||||
))
|
|
||||||
})?)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let version_jar_name = if let Some(loader) = &loader_version {
|
|
||||||
loader.id.clone()
|
|
||||||
} else {
|
|
||||||
version.id.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut version = download::download_version_info(
|
let mut version = download::download_version_info(
|
||||||
&versions_path,
|
&versions_path,
|
||||||
@ -154,23 +144,10 @@ pub async fn launch_minecraft(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let java_path = if let Some(java) = &version.java_version {
|
let client_path = versions_path
|
||||||
if java.major_version == 17 || java.major_version == 16 {
|
.join(&version.id)
|
||||||
settings.java_17_path.as_deref().ok_or_else(|| LauncherError::JavaError("Please install Java 17 or select your Java 17 installation settings before launching this version!".to_string()))?
|
.join(format!("{}.jar", &version_jar));
|
||||||
} else {
|
let version_natives_path = natives_path.join(&version.id);
|
||||||
&settings.java_8_path.as_deref().ok_or_else(|| LauncherError::JavaError("Please install Java 8 or select your Java 8 installation settings before launching this version!".to_string()))?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
&settings.java_8_path.as_deref().ok_or_else(|| LauncherError::JavaError("Please install Java 8 or select your Java 8 installation settings before launching this version!".to_string()))?
|
|
||||||
};
|
|
||||||
|
|
||||||
let client_path = crate::util::absolute_path(
|
|
||||||
root_dir
|
|
||||||
.join("versions")
|
|
||||||
.join(&version.id)
|
|
||||||
.join(format!("{}.jar", &version.id)),
|
|
||||||
)?;
|
|
||||||
let natives_path = crate::util::absolute_path(root_dir.join("natives").join(&version.id))?;
|
|
||||||
|
|
||||||
download_minecraft(
|
download_minecraft(
|
||||||
&version,
|
&version,
|
||||||
@ -178,7 +155,7 @@ pub async fn launch_minecraft(
|
|||||||
&assets_path,
|
&assets_path,
|
||||||
&legacy_assets_path,
|
&legacy_assets_path,
|
||||||
&libraries_path,
|
&libraries_path,
|
||||||
&natives_path,
|
&version_natives_path,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -201,7 +178,7 @@ pub async fn launch_minecraft(
|
|||||||
data.insert(
|
data.insert(
|
||||||
"MINECRAFT_VERSION".to_string(),
|
"MINECRAFT_VERSION".to_string(),
|
||||||
daedalus::modded::SidedDataEntry {
|
daedalus::modded::SidedDataEntry {
|
||||||
client: version_name.to_string(),
|
client: game_version.to_string(),
|
||||||
server: "".to_string(),
|
server: "".to_string(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -252,6 +229,7 @@ pub async fn launch_minecraft(
|
|||||||
data,
|
data,
|
||||||
)?)
|
)?)
|
||||||
.output()
|
.output()
|
||||||
|
.await
|
||||||
.map_err(|err| LauncherError::ProcessError {
|
.map_err(|err| LauncherError::ProcessError {
|
||||||
inner: err,
|
inner: err,
|
||||||
process: "java".to_string(),
|
process: "java".to_string(),
|
||||||
@ -266,60 +244,50 @@ pub async fn launch_minecraft(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let arguments = version.arguments.unwrap_or_default();
|
let arguments = version.arguments.clone().unwrap_or_default();
|
||||||
|
let mut command = match wrapper {
|
||||||
let mut command = Command::new(if let Some(wrapper) = &settings.wrapper_command {
|
Some(hook) => {
|
||||||
wrapper.clone()
|
let mut cmd = Command::new(hook);
|
||||||
} else {
|
cmd.arg(java);
|
||||||
java_path.to_string()
|
cmd
|
||||||
});
|
}
|
||||||
|
None => Command::new(java.to_string_lossy().to_string()),
|
||||||
if settings.wrapper_command.is_some() {
|
};
|
||||||
command.arg(java_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
command
|
command
|
||||||
.args(args::get_jvm_arguments(
|
.args(args::get_jvm_arguments(
|
||||||
arguments.get(&ArgumentType::Jvm).map(|x| x.as_slice()),
|
arguments.get(&ArgumentType::Jvm).map(|x| x.as_slice()),
|
||||||
&natives_path,
|
&version_natives_path,
|
||||||
&libraries_path,
|
&libraries_path,
|
||||||
&args::get_class_paths(&libraries_path, version.libraries.as_slice(), &client_path)?,
|
&args::get_class_paths(
|
||||||
&version_jar_name,
|
&libraries_path,
|
||||||
settings.memory,
|
version.libraries.as_slice(),
|
||||||
settings
|
&client_path,
|
||||||
.custom_java_args
|
)?,
|
||||||
.split(" ")
|
&version_jar,
|
||||||
.into_iter()
|
*memory,
|
||||||
.map(|x| x.to_string())
|
java_args.clone(),
|
||||||
.collect(),
|
|
||||||
)?)
|
)?)
|
||||||
.arg(version.main_class)
|
.arg(version.main_class.clone())
|
||||||
.args(args::get_minecraft_arguments(
|
.args(args::get_minecraft_arguments(
|
||||||
arguments.get(&ArgumentType::Game).map(|x| x.as_slice()),
|
arguments.get(&ArgumentType::Game).map(|x| x.as_slice()),
|
||||||
version.minecraft_arguments.as_deref(),
|
version.minecraft_arguments.as_deref(),
|
||||||
credentials,
|
credentials,
|
||||||
&version.id,
|
&version.id,
|
||||||
&version.asset_index.id,
|
&version.asset_index.id,
|
||||||
root_dir,
|
&root_dir,
|
||||||
&assets_path,
|
&assets_path,
|
||||||
&version.type_,
|
&version.type_,
|
||||||
settings.game_resolution,
|
*resolution,
|
||||||
)?)
|
)?)
|
||||||
.current_dir(root_dir)
|
.current_dir(root_dir.clone())
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::inherit())
|
||||||
.stderr(Stdio::inherit());
|
.stderr(Stdio::inherit());
|
||||||
|
|
||||||
let mut child = command.spawn().map_err(|err| LauncherError::ProcessError {
|
command.spawn().map_err(|err| LauncherError::ProcessError {
|
||||||
inner: err,
|
inner: err,
|
||||||
process: "minecraft".to_string(),
|
process: format!("minecraft-{} @ {}", &version.id, root_dir.display()),
|
||||||
})?;
|
})
|
||||||
|
|
||||||
child.wait().map_err(|err| LauncherError::ProcessError {
|
|
||||||
inner: err,
|
|
||||||
process: "minecraft".to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_minecraft(
|
pub async fn download_minecraft(
|
||||||
@ -330,7 +298,8 @@ pub async fn download_minecraft(
|
|||||||
libraries_dir: &Path,
|
libraries_dir: &Path,
|
||||||
natives_dir: &Path,
|
natives_dir: &Path,
|
||||||
) -> Result<(), LauncherError> {
|
) -> Result<(), LauncherError> {
|
||||||
let assets_index = download::download_assets_index(assets_dir, version).await?;
|
let assets_index =
|
||||||
|
download::download_assets_index(assets_dir, version).await?;
|
||||||
|
|
||||||
let (a, b, c) = futures::future::join3(
|
let (a, b, c) = futures::future::join3(
|
||||||
download::download_client(versions_dir, version),
|
download::download_client(versions_dir, version),
|
||||||
@ -343,7 +312,11 @@ pub async fn download_minecraft(
|
|||||||
},
|
},
|
||||||
&assets_index,
|
&assets_index,
|
||||||
),
|
),
|
||||||
download::download_libraries(libraries_dir, natives_dir, version.libraries.as_slice()),
|
download::download_libraries(
|
||||||
|
libraries_dir,
|
||||||
|
natives_dir,
|
||||||
|
version.libraries.as_slice(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,11 @@
|
|||||||
//! Theseus is a library which provides utilities for launching minecraft, creating Modrinth mod packs,
|
//! Theseus is a library which provides utilities for launching minecraft, creating Modrinth mod packs,
|
||||||
//! and launching Modrinth mod packs
|
//! and launching Modrinth mod packs
|
||||||
|
|
||||||
#![warn(missing_docs, unused_import_braces, missing_debug_implementations)]
|
#![warn(unused_import_braces, missing_debug_implementations)]
|
||||||
|
|
||||||
static LAUNCHER_WORK_DIR: &'static str = "./launcher";
|
static LAUNCHER_WORK_DIR: &'static str = "./launcher";
|
||||||
|
|
||||||
mod data;
|
pub mod data;
|
||||||
pub mod launcher;
|
pub mod launcher;
|
||||||
pub mod modpack;
|
pub mod modpack;
|
||||||
mod util;
|
mod util;
|
||||||
@ -25,9 +25,29 @@ pub enum Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn init() -> Result<(), Error> {
|
pub async fn init() -> Result<(), Error> {
|
||||||
std::fs::create_dir_all(LAUNCHER_WORK_DIR).expect("Unable to create launcher root directory!");
|
std::fs::create_dir_all(LAUNCHER_WORK_DIR)
|
||||||
crate::data::Metadata::init().await?;
|
.expect("Unable to create launcher root directory!");
|
||||||
crate::data::Settings::init().await?;
|
|
||||||
|
use crate::data::*;
|
||||||
|
Metadata::init().await?;
|
||||||
|
|
||||||
|
Settings::init().await?;
|
||||||
|
|
||||||
|
tokio::try_join! {
|
||||||
|
launcher::init_download_semaphore(),
|
||||||
|
Profiles::init(),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save() -> Result<(), Error> {
|
||||||
|
use crate::data::*;
|
||||||
|
|
||||||
|
tokio::try_join! {
|
||||||
|
Settings::save(),
|
||||||
|
Profiles::save(),
|
||||||
|
}?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,10 +22,12 @@ pub const COMPILED_ZIP: &str = "compiled.mrpack";
|
|||||||
pub const MANIFEST_PATH: &str = "modrinth.index.json";
|
pub const MANIFEST_PATH: &str = "modrinth.index.json";
|
||||||
pub const OVERRIDES_PATH: &str = "overrides/";
|
pub const OVERRIDES_PATH: &str = "overrides/";
|
||||||
pub const PACK_JSON5_PATH: &str = "modpack.json5";
|
pub const PACK_JSON5_PATH: &str = "modpack.json5";
|
||||||
const PACK_GITIGNORE: &'static str = const_format::formatcp!(r#"
|
const PACK_GITIGNORE: &'static str = const_format::formatcp!(
|
||||||
|
r#"
|
||||||
{COMPILED_PATH}
|
{COMPILED_PATH}
|
||||||
{COMPILED_ZIP}
|
{COMPILED_ZIP}
|
||||||
"#);
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum ModpackError {
|
pub enum ModpackError {
|
||||||
|
|||||||
@ -21,7 +21,10 @@ pub trait ModrinthAPI {
|
|||||||
channel: &str,
|
channel: &str,
|
||||||
game: &ModpackGame,
|
game: &ModpackGame,
|
||||||
) -> ModpackResult<HashSet<ModpackFile>>;
|
) -> ModpackResult<HashSet<ModpackFile>>;
|
||||||
async fn get_version(&self, version: &str) -> ModpackResult<HashSet<ModpackFile>>;
|
async fn get_version(
|
||||||
|
&self,
|
||||||
|
version: &str,
|
||||||
|
) -> ModpackResult<HashSet<ModpackFile>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -93,6 +96,8 @@ impl ModrinthAPI for ModrinthV1 {
|
|||||||
String::from("Modrinth V1 does not support vanilla projects"),
|
String::from("Modrinth V1 does not support vanilla projects"),
|
||||||
)),
|
)),
|
||||||
ModpackGame::Minecraft(ref version, ref loader) => Ok((version, loader)),
|
ModpackGame::Minecraft(ref version, ref loader) => Ok((version, loader)),
|
||||||
|
// This guard is here for when Modrinth does support other games.
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
_ => Err(ModpackError::VersionError(String::from(
|
_ => Err(ModpackError::VersionError(String::from(
|
||||||
"Attempted to use Modrinth API V1 to install a non-Minecraft project!",
|
"Attempted to use Modrinth API V1 to install a non-Minecraft project!",
|
||||||
))),
|
))),
|
||||||
@ -131,7 +136,8 @@ impl ModrinthAPI for ModrinthV1 {
|
|||||||
.map(ModpackFile::from)
|
.map(ModpackFile::from)
|
||||||
.collect::<HashSet<ModpackFile>>();
|
.collect::<HashSet<ModpackFile>>();
|
||||||
|
|
||||||
let dep_futures = version.dependencies.iter().map(|it| self.get_version(it));
|
let dep_futures =
|
||||||
|
version.dependencies.iter().map(|it| self.get_version(it));
|
||||||
let deps = try_join_all(dep_futures)
|
let deps = try_join_all(dep_futures)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -148,12 +154,17 @@ impl ModrinthAPI for ModrinthV1 {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_version(&self, version: &str) -> ModpackResult<HashSet<ModpackFile>> {
|
async fn get_version(
|
||||||
|
&self,
|
||||||
|
version: &str,
|
||||||
|
) -> ModpackResult<HashSet<ModpackFile>> {
|
||||||
let domain = &self.0;
|
let domain = &self.0;
|
||||||
let version_json = try_get_json(format!("{domain}/api/v1/version/{version}")).await?;
|
let version_json =
|
||||||
let mut version_deserializer = serde_json::Deserializer::from_slice(&version_json);
|
try_get_json(format!("{domain}/api/v1/version/{version}")).await?;
|
||||||
let version = ModrinthV1ProjectVersion::deserialize(&mut version_deserializer)?;
|
let mut version_deserializer =
|
||||||
let base_path = PathBuf::from("mods/");
|
serde_json::Deserializer::from_slice(&version_json);
|
||||||
|
let version =
|
||||||
|
ModrinthV1ProjectVersion::deserialize(&mut version_deserializer)?;
|
||||||
|
|
||||||
Ok(version
|
Ok(version
|
||||||
.files
|
.files
|
||||||
|
|||||||
@ -159,6 +159,7 @@ pub struct ModpackFile {
|
|||||||
pub downloads: HashSet<String>,
|
pub downloads: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::derive_hash_xor_eq)]
|
||||||
impl Hash for ModpackFile {
|
impl Hash for ModpackFile {
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
if let Some(ref hashes) = self.hashes {
|
if let Some(ref hashes) = self.hashes {
|
||||||
|
|||||||
@ -8,5 +8,14 @@ edition = "2018"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
theseus = { path = "../theseus" }
|
theseus = { path = "../theseus" }
|
||||||
|
daedalus = "0.1.12"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
argh = "0.1"
|
||||||
|
paris = { version = "1.5", features = ["macros", "no_logger"] }
|
||||||
|
dialoguer = "0.10"
|
||||||
|
eyre = "0.6"
|
||||||
|
tabled = "0.5"
|
||||||
|
dirs = "4.0"
|
||||||
|
uuid = {version = "0.8", features = ["v4", "serde"]}
|
||||||
@ -1,2 +1,27 @@
|
|||||||
|
use eyre::Result;
|
||||||
|
use paris::*;
|
||||||
|
|
||||||
|
mod subcommands;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs)]
|
||||||
|
/// The official Modrinth CLI
|
||||||
|
pub struct Args {
|
||||||
|
#[argh(subcommand)]
|
||||||
|
pub subcommand: subcommands::SubCommand,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {}
|
async fn main() -> Result<()> {
|
||||||
|
let args = argh::from_env::<Args>();
|
||||||
|
theseus::init().await?;
|
||||||
|
|
||||||
|
let res = args.dispatch().await;
|
||||||
|
if res.is_err() {
|
||||||
|
error!("An error has occurred!\n");
|
||||||
|
} else {
|
||||||
|
theseus::save().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|||||||
17
theseus_cli/src/subcommands/mod.rs
Normal file
17
theseus_cli/src/subcommands/mod.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use eyre::Result;
|
||||||
|
|
||||||
|
mod profile;
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs)]
|
||||||
|
#[argh(subcommand)]
|
||||||
|
pub enum SubCommand {
|
||||||
|
Profile(profile::ProfileCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::Args {
|
||||||
|
pub async fn dispatch(&self) -> Result<()> {
|
||||||
|
match self.subcommand {
|
||||||
|
SubCommand::Profile(ref cmd) => cmd.dispatch(self).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
418
theseus_cli/src/subcommands/profile.rs
Normal file
418
theseus_cli/src/subcommands/profile.rs
Normal file
@ -0,0 +1,418 @@
|
|||||||
|
//! Profile management subcommand
|
||||||
|
use crate::util::{
|
||||||
|
confirm_async, prompt_async, select_async, table_path_display,
|
||||||
|
};
|
||||||
|
use daedalus::modded::LoaderVersion;
|
||||||
|
use eyre::{ensure, Result};
|
||||||
|
use paris::*;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tabled::{Table, Tabled};
|
||||||
|
use theseus::{
|
||||||
|
data::{profiles::PROFILE_JSON_PATH, Metadata, Profile, Profiles},
|
||||||
|
launcher::ModLoader,
|
||||||
|
};
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio_stream::{wrappers::ReadDirStream, StreamExt};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs)]
|
||||||
|
#[argh(subcommand, name = "profile")]
|
||||||
|
/// profile management
|
||||||
|
pub struct ProfileCommand {
|
||||||
|
#[argh(subcommand)]
|
||||||
|
action: ProfileSubcommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs)]
|
||||||
|
#[argh(subcommand)]
|
||||||
|
pub enum ProfileSubcommand {
|
||||||
|
Add(ProfileAdd),
|
||||||
|
Init(ProfileInit),
|
||||||
|
List(ProfileList),
|
||||||
|
Remove(ProfileRemove),
|
||||||
|
Run(ProfileRun),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs)]
|
||||||
|
#[argh(subcommand, name = "add")]
|
||||||
|
/// add a new profile to Theseus
|
||||||
|
pub struct ProfileAdd {
|
||||||
|
#[argh(positional, default = "std::env::current_dir().unwrap()")]
|
||||||
|
/// the profile to add
|
||||||
|
profile: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileAdd {
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
_args: &crate::Args,
|
||||||
|
_largs: &ProfileCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!(
|
||||||
|
"Adding profile at path '{}' to Theseus",
|
||||||
|
self.profile.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let profile = self.profile.canonicalize()?;
|
||||||
|
let json_path = profile.join(PROFILE_JSON_PATH);
|
||||||
|
ensure!(
|
||||||
|
json_path.exists(),
|
||||||
|
"Profile json does not exist. Perhaps you wanted `profile init` or `profile fetch`?"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
Profiles::get().await.unwrap().0.get(&profile).is_none(),
|
||||||
|
"Profile already managed by Theseus. If the contents of the profile are invalid or missing, the profile can be regenerated using `profile init` or `profile fetch`"
|
||||||
|
);
|
||||||
|
Profiles::insert_from(profile).await?;
|
||||||
|
success!("Profile added!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs)]
|
||||||
|
#[argh(subcommand, name = "init")]
|
||||||
|
/// create a new profile and manage it with Theseus
|
||||||
|
pub struct ProfileInit {
|
||||||
|
#[argh(positional, default = "std::env::current_dir().unwrap()")]
|
||||||
|
/// the path of the newly created profile
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
#[argh(option)]
|
||||||
|
/// the name of the profile
|
||||||
|
name: Option<String>,
|
||||||
|
|
||||||
|
#[argh(option)]
|
||||||
|
/// the game version of the profile
|
||||||
|
game_version: Option<String>,
|
||||||
|
|
||||||
|
#[argh(option)]
|
||||||
|
/// the icon for the profile
|
||||||
|
icon: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[argh(option, from_str_fn(modloader_from_str))]
|
||||||
|
/// the modloader to use
|
||||||
|
modloader: Option<ModLoader>,
|
||||||
|
|
||||||
|
#[argh(option)]
|
||||||
|
/// the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
||||||
|
loader_version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileInit {
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
_args: &crate::Args,
|
||||||
|
_largs: &ProfileCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
// TODO: validate inputs from args early
|
||||||
|
if self.path.exists() {
|
||||||
|
ensure!(
|
||||||
|
self.path.is_dir(),
|
||||||
|
"Attempted to create profile in something other than a folder!"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
!self.path.join(PROFILE_JSON_PATH).exists(),
|
||||||
|
"Profile already exists! Perhaps you want `profile add` instead?"
|
||||||
|
);
|
||||||
|
if ReadDirStream::new(fs::read_dir(&self.path).await?)
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
warn!("You are trying to create a profile in a non-empty directory. If this is an instance from another launcher, please be sure to properly fill the profile.json fields!");
|
||||||
|
if !confirm_async(
|
||||||
|
String::from("Do you wish to continue"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
eyre::bail!("Aborted!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::create_dir_all(&self.path).await?;
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"Creating profile at path {}",
|
||||||
|
&self.path.canonicalize()?.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let metadata = Metadata::get().await?;
|
||||||
|
|
||||||
|
// TODO: abstract default prompting
|
||||||
|
let name = match &self.name {
|
||||||
|
Some(name) => name.clone(),
|
||||||
|
None => {
|
||||||
|
let default = self.path.file_name().unwrap().to_string_lossy();
|
||||||
|
|
||||||
|
prompt_async(
|
||||||
|
String::from("Instance name"),
|
||||||
|
Some(default.into_owned()),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let game_version = match &self.game_version {
|
||||||
|
Some(version) => version.clone(),
|
||||||
|
None => {
|
||||||
|
let default = &metadata.minecraft.latest.release;
|
||||||
|
|
||||||
|
prompt_async(
|
||||||
|
String::from("Game version"),
|
||||||
|
Some(default.clone()),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let loader = match &self.modloader {
|
||||||
|
Some(loader) => *loader,
|
||||||
|
None => {
|
||||||
|
let choice = select_async(
|
||||||
|
"Modloader".to_owned(),
|
||||||
|
&["vanilla", "fabric", "forge"],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match choice {
|
||||||
|
0 => ModLoader::Vanilla,
|
||||||
|
1 => ModLoader::Fabric,
|
||||||
|
2 => ModLoader::Forge,
|
||||||
|
_ => eyre::bail!(
|
||||||
|
"Invalid modloader ID: {choice}. This is a bug in the launcher!"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let loader = if loader != ModLoader::Vanilla {
|
||||||
|
let version = match &self.loader_version {
|
||||||
|
Some(version) => String::from(version),
|
||||||
|
None => prompt_async(
|
||||||
|
String::from(
|
||||||
|
"Modloader version (latest, stable, or a version ID)",
|
||||||
|
),
|
||||||
|
Some(String::from("latest")),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let filter = |it: &LoaderVersion| match version.as_str() {
|
||||||
|
"latest" => true,
|
||||||
|
"stable" => it.stable,
|
||||||
|
id => it.id == String::from(id),
|
||||||
|
};
|
||||||
|
|
||||||
|
let loader_data = match loader {
|
||||||
|
ModLoader::Forge => &metadata.forge,
|
||||||
|
ModLoader::Fabric => &metadata.fabric,
|
||||||
|
_ => eyre::bail!("Could not get manifest for loader {loader}. This is a bug in the CLI!"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ref loaders = loader_data.game_versions
|
||||||
|
.iter()
|
||||||
|
.find(|it| it.id == game_version)
|
||||||
|
.ok_or_else(|| eyre::eyre!("Modloader {loader} unsupported for Minecraft version {game_version}"))?
|
||||||
|
.loaders;
|
||||||
|
|
||||||
|
let loader_version =
|
||||||
|
loaders.iter().cloned().find(filter).ok_or_else(|| {
|
||||||
|
eyre::eyre!(
|
||||||
|
"Invalid version {version} for modloader {loader}"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some((loader_version, loader))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon = match &self.icon {
|
||||||
|
Some(icon) => Some(icon.clone()),
|
||||||
|
None => Some(
|
||||||
|
prompt_async("Icon".to_owned(), Some(String::new())).await?,
|
||||||
|
)
|
||||||
|
.filter(|it| !it.trim().is_empty())
|
||||||
|
.map(PathBuf::from),
|
||||||
|
};
|
||||||
|
|
||||||
|
// We don't really care if the profile already is managed, as getting this far means that the user probably wanted to re-create a profile
|
||||||
|
drop(metadata);
|
||||||
|
let mut profile =
|
||||||
|
Profile::new(name, game_version, self.path.clone()).await?;
|
||||||
|
|
||||||
|
if let Some(ref icon) = icon {
|
||||||
|
profile.with_icon(icon).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((loader_version, loader)) = loader {
|
||||||
|
profile.with_loader(loader, Some(loader_version));
|
||||||
|
}
|
||||||
|
|
||||||
|
Profiles::insert(profile).await?;
|
||||||
|
Profiles::save().await?;
|
||||||
|
|
||||||
|
success!(
|
||||||
|
"Successfully created instance, it is now available to use with Theseus!"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs)]
|
||||||
|
/// list all managed profiles
|
||||||
|
#[argh(subcommand, name = "list")]
|
||||||
|
pub struct ProfileList {}
|
||||||
|
|
||||||
|
#[derive(Tabled)]
|
||||||
|
struct ProfileRow<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
#[field(display_with = "table_path_display")]
|
||||||
|
path: &'a Path,
|
||||||
|
#[header("game version")]
|
||||||
|
game_version: &'a str,
|
||||||
|
loader: &'a ModLoader,
|
||||||
|
#[header("loader version")]
|
||||||
|
loader_version: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Profile> for ProfileRow<'a> {
|
||||||
|
fn from(it: &'a Profile) -> Self {
|
||||||
|
Self {
|
||||||
|
name: &it.metadata.name,
|
||||||
|
path: &it.path,
|
||||||
|
game_version: &it.metadata.game_version,
|
||||||
|
loader: &it.metadata.loader,
|
||||||
|
loader_version: it
|
||||||
|
.metadata
|
||||||
|
.loader_version
|
||||||
|
.as_ref()
|
||||||
|
.map_or("", |it| &it.id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileList {
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
_args: &crate::Args,
|
||||||
|
_largs: &ProfileCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
let profiles = Profiles::get().await?;
|
||||||
|
let profiles = profiles.0.values().map(ProfileRow::from);
|
||||||
|
|
||||||
|
let table = Table::new(profiles).with(tabled::Style::psql()).with(
|
||||||
|
tabled::Modify::new(tabled::Column(1..=1))
|
||||||
|
.with(tabled::MaxWidth::wrapping(40)),
|
||||||
|
);
|
||||||
|
println!("{table}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs)]
|
||||||
|
/// unmanage a profile
|
||||||
|
#[argh(subcommand, name = "remove")]
|
||||||
|
pub struct ProfileRemove {
|
||||||
|
#[argh(positional, default = "std::env::current_dir().unwrap()")]
|
||||||
|
/// the profile to get rid of
|
||||||
|
profile: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileRemove {
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
_args: &crate::Args,
|
||||||
|
_largs: &ProfileCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
let profile = self.profile.canonicalize()?;
|
||||||
|
info!("Removing profile {} from Theseus", self.profile.display());
|
||||||
|
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||||
|
if Profiles::remove(&profile).await?.is_none() {
|
||||||
|
warn!("Profile was not managed by Theseus!");
|
||||||
|
} else {
|
||||||
|
success!("Profile removed!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("Aborted!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs)]
|
||||||
|
/// run a profile
|
||||||
|
#[argh(subcommand, name = "run")]
|
||||||
|
pub struct ProfileRun {
|
||||||
|
#[argh(positional, default = "std::env::current_dir().unwrap()")]
|
||||||
|
/// the profile to run
|
||||||
|
profile: PathBuf,
|
||||||
|
|
||||||
|
// TODO: auth
|
||||||
|
#[argh(option, short = 't')]
|
||||||
|
/// the Minecraft token to use for player login. Should be replaced by auth when that is a thing.
|
||||||
|
token: String,
|
||||||
|
|
||||||
|
#[argh(option, short = 'n')]
|
||||||
|
/// the uername to use for running the game
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[argh(option, short = 'i')]
|
||||||
|
/// the account id to use for running the game
|
||||||
|
id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileRun {
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
_args: &crate::Args,
|
||||||
|
_largs: &ProfileCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("Starting profile at path {}...", self.profile.display());
|
||||||
|
let ref profiles = Profiles::get().await?.0;
|
||||||
|
let path = self.profile.canonicalize()?;
|
||||||
|
let profile = profiles
|
||||||
|
.get(&path)
|
||||||
|
.ok_or(
|
||||||
|
eyre::eyre!(
|
||||||
|
"Profile not managed by Theseus (if it exists, try using `profile add` first!)"
|
||||||
|
)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let credentials = theseus::launcher::Credentials {
|
||||||
|
id: self.id.clone(),
|
||||||
|
username: self.name.clone(),
|
||||||
|
access_token: self.token.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut proc = profile.run(&credentials).await?;
|
||||||
|
profile.wait_for(&mut proc).await?;
|
||||||
|
|
||||||
|
success!("Process exited successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileCommand {
|
||||||
|
pub async fn dispatch(&self, args: &crate::Args) -> Result<()> {
|
||||||
|
match &self.action {
|
||||||
|
ProfileSubcommand::Add(ref cmd) => cmd.run(args, self).await,
|
||||||
|
ProfileSubcommand::Init(ref cmd) => cmd.run(args, self).await,
|
||||||
|
ProfileSubcommand::List(ref cmd) => cmd.run(args, self).await,
|
||||||
|
ProfileSubcommand::Remove(ref cmd) => cmd.run(args, self).await,
|
||||||
|
ProfileSubcommand::Run(ref cmd) => cmd.run(args, self).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modloader_from_str(it: &str) -> core::result::Result<ModLoader, String> {
|
||||||
|
match it {
|
||||||
|
"vanilla" => Ok(ModLoader::Vanilla),
|
||||||
|
"forge" => Ok(ModLoader::Forge),
|
||||||
|
"fabric" => Ok(ModLoader::Fabric),
|
||||||
|
_ => Err(String::from("Invalid modloader: {it}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
76
theseus_cli/src/util.rs
Normal file
76
theseus_cli/src/util.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use dialoguer::{Confirm, Input, Select};
|
||||||
|
use eyre::Result;
|
||||||
|
use std::{borrow::Cow, path::Path};
|
||||||
|
|
||||||
|
// TODO: make primarily async to avoid copies
|
||||||
|
|
||||||
|
// Prompting helpers
|
||||||
|
pub fn prompt(prompt: &str, default: Option<String>) -> Result<String> {
|
||||||
|
let prompt = match default.as_deref() {
|
||||||
|
Some("") => Cow::Owned(format!("{prompt} (optional)")),
|
||||||
|
Some(default) => Cow::Owned(format!("{prompt} (default: {default})")),
|
||||||
|
None => Cow::Borrowed(prompt),
|
||||||
|
};
|
||||||
|
print_prompt(&prompt);
|
||||||
|
|
||||||
|
let mut input = Input::<String>::new();
|
||||||
|
input.with_prompt("").show_default(false);
|
||||||
|
|
||||||
|
if let Some(default) = default {
|
||||||
|
input.default(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(input.interact_text()?.trim().to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prompt_async(
|
||||||
|
text: String,
|
||||||
|
default: Option<String>,
|
||||||
|
) -> Result<String> {
|
||||||
|
tokio::task::spawn_blocking(move || prompt(&text, default)).await?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selection helpers
|
||||||
|
pub fn select(prompt: &str, choices: &[&str]) -> Result<usize> {
|
||||||
|
print_prompt(prompt);
|
||||||
|
|
||||||
|
let res = Select::new().items(choices).default(0).interact()?;
|
||||||
|
eprintln!("> {}", choices[res]);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn select_async(
|
||||||
|
prompt: String,
|
||||||
|
choices: &'static [&'static str],
|
||||||
|
) -> Result<usize> {
|
||||||
|
tokio::task::spawn_blocking(move || select(&prompt, choices)).await?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmation helpers
|
||||||
|
pub fn confirm(prompt: &str, default: bool) -> Result<bool> {
|
||||||
|
print_prompt(prompt);
|
||||||
|
Ok(Confirm::new().default(default).interact()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn confirm_async(prompt: String, default: bool) -> Result<bool> {
|
||||||
|
tokio::task::spawn_blocking(move || confirm(&prompt, default)).await?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table display helpers
|
||||||
|
pub fn table_path_display(path: &Path) -> String {
|
||||||
|
let mut res = path.display().to_string();
|
||||||
|
|
||||||
|
if let Some(home_dir) = dirs::home_dir() {
|
||||||
|
res = res.replace(&home_dir.display().to_string(), "~");
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helpers
|
||||||
|
fn print_prompt(prompt: &str) {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
paris::formatter::colorize_string(format!("<yellow>?</> {prompt}:"))
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,89 +1,89 @@
|
|||||||
#![cfg_attr(
|
#![cfg_attr(
|
||||||
all(not(debug_assertions), target_os = "windows"),
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use tauri::api::shell;
|
use tauri::api::shell;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
CustomMenuItem, Manager, Menu, MenuEntry, MenuItem, Submenu, WindowBuilder, WindowUrl,
|
CustomMenuItem, Manager, Menu, MenuEntry, MenuItem, Submenu, WindowBuilder, WindowUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let ctx = tauri::generate_context!();
|
let ctx = tauri::generate_context!();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![])
|
.invoke_handler(tauri::generate_handler![])
|
||||||
.create_window("main", WindowUrl::default(), |win, webview| {
|
.create_window("main", WindowUrl::default(), |win, webview| {
|
||||||
let win = win
|
let win = win
|
||||||
.title("Modrinth")
|
.title("Modrinth")
|
||||||
.resizable(true)
|
.resizable(true)
|
||||||
.decorations(true)
|
.decorations(true)
|
||||||
.always_on_top(false)
|
.always_on_top(false)
|
||||||
.inner_size(800.0, 550.0)
|
.inner_size(800.0, 550.0)
|
||||||
.min_inner_size(400.0, 200.0)
|
.min_inner_size(400.0, 200.0)
|
||||||
.skip_taskbar(false)
|
.skip_taskbar(false)
|
||||||
.fullscreen(false);
|
.fullscreen(false);
|
||||||
return (win, webview);
|
return (win, webview);
|
||||||
})
|
})
|
||||||
.menu(Menu::with_items([
|
.menu(Menu::with_items([
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
MenuEntry::Submenu(Submenu::new(
|
MenuEntry::Submenu(Submenu::new(
|
||||||
&ctx.package_info().name,
|
&ctx.package_info().name,
|
||||||
Menu::with_items([
|
Menu::with_items([
|
||||||
MenuItem::About(ctx.package_info().name.clone()).into(),
|
MenuItem::About(ctx.package_info().name.clone()).into(),
|
||||||
MenuItem::Separator.into(),
|
MenuItem::Separator.into(),
|
||||||
MenuItem::Services.into(),
|
MenuItem::Services.into(),
|
||||||
MenuItem::Separator.into(),
|
MenuItem::Separator.into(),
|
||||||
MenuItem::Hide.into(),
|
MenuItem::Hide.into(),
|
||||||
MenuItem::HideOthers.into(),
|
MenuItem::HideOthers.into(),
|
||||||
MenuItem::ShowAll.into(),
|
MenuItem::ShowAll.into(),
|
||||||
MenuItem::Separator.into(),
|
MenuItem::Separator.into(),
|
||||||
MenuItem::Quit.into(),
|
MenuItem::Quit.into(),
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
MenuEntry::Submenu(Submenu::new(
|
MenuEntry::Submenu(Submenu::new(
|
||||||
"File",
|
"File",
|
||||||
Menu::with_items([MenuItem::CloseWindow.into()]),
|
Menu::with_items([MenuItem::CloseWindow.into()]),
|
||||||
)),
|
)),
|
||||||
MenuEntry::Submenu(Submenu::new(
|
MenuEntry::Submenu(Submenu::new(
|
||||||
"Edit",
|
"Edit",
|
||||||
Menu::with_items([
|
Menu::with_items([
|
||||||
MenuItem::Undo.into(),
|
MenuItem::Undo.into(),
|
||||||
MenuItem::Redo.into(),
|
MenuItem::Redo.into(),
|
||||||
MenuItem::Separator.into(),
|
MenuItem::Separator.into(),
|
||||||
MenuItem::Cut.into(),
|
MenuItem::Cut.into(),
|
||||||
MenuItem::Copy.into(),
|
MenuItem::Copy.into(),
|
||||||
MenuItem::Paste.into(),
|
MenuItem::Paste.into(),
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
MenuItem::Separator.into(),
|
MenuItem::Separator.into(),
|
||||||
MenuItem::SelectAll.into(),
|
MenuItem::SelectAll.into(),
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
MenuEntry::Submenu(Submenu::new(
|
MenuEntry::Submenu(Submenu::new(
|
||||||
"View",
|
"View",
|
||||||
Menu::with_items([MenuItem::EnterFullScreen.into()]),
|
Menu::with_items([MenuItem::EnterFullScreen.into()]),
|
||||||
)),
|
)),
|
||||||
MenuEntry::Submenu(Submenu::new(
|
MenuEntry::Submenu(Submenu::new(
|
||||||
"Window",
|
"Window",
|
||||||
Menu::with_items([MenuItem::Minimize.into(), MenuItem::Zoom.into()]),
|
Menu::with_items([MenuItem::Minimize.into(), MenuItem::Zoom.into()]),
|
||||||
)),
|
)),
|
||||||
// You should always have a Help menu on macOS because it will automatically
|
// You should always have a Help menu on macOS because it will automatically
|
||||||
// show a menu search field
|
// show a menu search field
|
||||||
MenuEntry::Submenu(Submenu::new(
|
MenuEntry::Submenu(Submenu::new(
|
||||||
"Help",
|
"Help",
|
||||||
Menu::with_items([CustomMenuItem::new("Learn More", "Learn More").into()]),
|
Menu::with_items([CustomMenuItem::new("Learn More", "Learn More").into()]),
|
||||||
)),
|
)),
|
||||||
]))
|
]))
|
||||||
.on_menu_event(|event| {
|
.on_menu_event(|event| {
|
||||||
let event_name = event.menu_item_id();
|
let event_name = event.menu_item_id();
|
||||||
match event_name {
|
match event_name {
|
||||||
"Learn More" => {
|
"Learn More" => {
|
||||||
let url = "https://modrinth.com".to_string();
|
let url = "https://github.com/probablykasper/tauri-template".to_string();
|
||||||
shell::open(&event.window().shell_scope(), url, None).unwrap();
|
shell::open(&event.window().shell_scope(), url, None).unwrap();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.run(ctx)
|
.run(ctx)
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user