From b6592fe1852c99f2661658fdeeb55ce006cc96e5 Mon Sep 17 00:00:00 2001 From: Vilsol Date: Wed, 13 Dec 2023 15:34:01 -0800 Subject: [PATCH] feat: parallel apply view (#47) * feat: parallel apply view * chore: cleaner readme * chore: lint * chore: remove debug logging * chore: lint --- .envrc | 1 + .github/screenshot.png | Bin 0 -> 70741 bytes .github/workflows/push.yaml | 8 +- .gitignore | 3 +- .golangci.yml | 1 - README.md | 13 +- cli/cache/download.go | 4 - cli/dependency_resolver.go | 23 +- cli/installations.go | 5 +- cli/provider/ficsit.go | 4 +- cli/provider/local.go | 16 +- cli/provider/mixed.go | 2 +- cli/provider/provider.go | 2 +- cli/resolving_test.go | 2 +- cli/test_helpers.go | 445 +++++++++++++++++------------------- ficsit/rest.go | 33 +++ ficsit/types_rest.go | 32 +++ flake.lock | 75 ++++++ flake.nix | 19 ++ go.mod | 2 + shell.nix | 8 + tea/scenes/apply.go | 244 ++++++++++++-------- tea/scenes/main_menu.go | 2 +- 23 files changed, 575 insertions(+), 369 deletions(-) create mode 100644 .envrc create mode 100644 .github/screenshot.png create mode 100644 ficsit/rest.go create mode 100644 ficsit/types_rest.go create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 shell.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8392d15 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake \ No newline at end of file diff --git a/.github/screenshot.png b/.github/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c55906c6332411259c14eb9c3fbbab3f9f6b5ecf GIT binary patch literal 70741 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 + # ficsit-cli [![push](https://github.com/Vilsol/ficsit-cli/actions/workflows/push.yaml/badge.svg)](https://github.com/Vilsol/ficsit-cli/actions/workflows/push.yaml) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/vilsol/ficsit-cli) ![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/vilsol/ficsit-cli) [![GitHub license](https://img.shields.io/github/license/Vilsol/ficsit-cli)](https://github.com/Vilsol/ficsit-cli/blob/master/LICENSE) ![GitHub all releases](https://img.shields.io/github/downloads/vilsol/ficsit-cli/total) A CLI tool for managing mods for the game Satisfactory +--- + ## Installation @@ -49,6 +53,14 @@ A CLI tool for managing mods for the game Satisfactory + + + + + + + + @@ -56,7 +68,6 @@ A CLI tool for managing mods for the game Satisfactory
armv7 ppc64le
Linuxamd64386arm64armv7ppc64le
macOS darwin_all
- ## Usage ### Interactive CLI diff --git a/cli/cache/download.go b/cli/cache/download.go index 4f6774c..5219ee3 100644 --- a/cli/cache/download.go +++ b/cli/cache/download.go @@ -14,10 +14,6 @@ import ( ) func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- utils.GenericProgress, downloadSemaphore chan int) (*os.File, int64, error) { - if updates != nil { - defer close(updates) - } - downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache") if err := os.MkdirAll(downloadCache, 0o777); err != nil { if !os.IsExist(err) { diff --git a/cli/dependency_resolver.go b/cli/dependency_resolver.go index a0b7c7a..c7765d4 100644 --- a/cli/dependency_resolver.go +++ b/cli/dependency_resolver.go @@ -34,7 +34,7 @@ type ficsitAPISource struct { provider provider.Provider lockfile *LockFile toInstall map[string]semver.Constraint - modVersionInfo *xsync.MapOf[string, ficsit.ModVersionsWithDependenciesResponse] + modVersionInfo *xsync.MapOf[string, ficsit.AllVersionsResponse] gameVersion semver.Version smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion } @@ -70,12 +70,15 @@ func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersi if err != nil { return nil, errors.Wrapf(err, "failed to fetch mod %s", pkg) } - if response.Mod.Id == "" { + if !response.Success { + if response.Error != nil { + return nil, errors.Errorf("mod %s not found: %s", pkg, response.Error.Message) + } return nil, errors.Errorf("mod %s not found", pkg) } f.modVersionInfo.Store(pkg, *response) - versions := make([]pubgrub.PackageVersion, len(response.Mod.Versions)) - for i, modVersion := range response.Mod.Versions { + versions := make([]pubgrub.PackageVersion, len(response.Data)) + for i, modVersion := range response.Data { v, err := semver.NewVersion(modVersion.Version) if err != nil { return nil, errors.Wrapf(err, "failed to parse version %s", modVersion.Version) @@ -88,9 +91,9 @@ func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersi return nil, errors.Wrapf(err, "failed to parse constraint %s", dependency.Condition) } if dependency.Optional { - optionalDependencies[dependency.Mod_id] = c + optionalDependencies[dependency.ModID] = c } else { - dependencies[dependency.Mod_id] = c + dependencies[dependency.ModID] = c } } versions[i] = pubgrub.PackageVersion{ @@ -144,7 +147,7 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string gameVersion: gameVersionSemver, lockfile: lockFile, toInstall: toInstall, - modVersionInfo: xsync.NewMapOf[string, ficsit.ModVersionsWithDependenciesResponse](), + modVersionInfo: xsync.NewMapOf[string, ficsit.AllVersionsResponse](), } result, err := pubgrub.Solve(helpers.NewCachingSource(ficsitSource), rootPkg) @@ -182,13 +185,13 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string } value, _ := ficsitSource.modVersionInfo.Load(k) - versions := value.Mod.Versions + versions := value.Data for _, ver := range versions { if ver.Version == v.RawString() { targets := make(map[string]LockedModTarget) for _, target := range ver.Targets { - targets[string(target.TargetName)] = LockedModTarget{ - Link: viper.GetString("api-base") + target.Link, + targets[target.TargetName] = LockedModTarget{ + Link: viper.GetString("api-base") + "/v1/version/" + ver.ID + "/" + target.TargetName + "/download", Hash: target.Hash, } } diff --git a/cli/installations.go b/cli/installations.go index 428545c..5690f71 100644 --- a/cli/installations.go +++ b/cli/installations.go @@ -588,6 +588,9 @@ func downloadAndExtractMod(modReference string, version string, link string, has } if updates != nil { + close(downloadUpdates) + close(extractUpdates) + updates <- InstallUpdate{ Type: InstallUpdateTypeModComplete, Item: InstallUpdateItem{ @@ -595,8 +598,6 @@ func downloadAndExtractMod(modReference string, version string, link string, has Version: version, }, } - - close(extractUpdates) } wg.Wait() diff --git a/cli/provider/ficsit.go b/cli/provider/ficsit.go index fa95459..9f5ccf1 100644 --- a/cli/provider/ficsit.go +++ b/cli/provider/ficsit.go @@ -34,8 +34,8 @@ func (p ficsitProvider) SMLVersions(context context.Context) (*ficsit.SMLVersion return ficsit.SMLVersions(context, p.client) } -func (p ficsitProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) { - return ficsit.ModVersionsWithDependencies(context, p.client, modID) +func (p ficsitProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) { + return ficsit.GetAllModVersions(modID) } func (p ficsitProvider) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error) { diff --git a/cli/provider/local.go b/cli/provider/local.go index 1423f5c..8bdeec1 100644 --- a/cli/provider/local.go +++ b/cli/provider/local.go @@ -176,26 +176,24 @@ func (p localProvider) SMLVersions(_ context.Context) (*ficsit.SMLVersionsRespon }, nil } -func (p localProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) { +func (p localProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) { cachedModFiles, err := cache.GetCacheMod(modID) if err != nil { return nil, errors.Wrap(err, "failed to get cache") } - versions := make([]ficsit.ModVersionsWithDependenciesModVersionsVersion, 0) + versions := make([]ficsit.ModVersion, 0) for _, modFile := range cachedModFiles { - versions = append(versions, ficsit.ModVersionsWithDependenciesModVersionsVersion{ - Id: modID + ":" + modFile.Plugin.SemVersion, + versions = append(versions, ficsit.ModVersion{ + ID: modID + ":" + modFile.Plugin.SemVersion, Version: modFile.Plugin.SemVersion, }) } - return &ficsit.ModVersionsWithDependenciesResponse{ - Mod: ficsit.ModVersionsWithDependenciesMod{ - Id: modID, - Versions: versions, - }, + return &ficsit.AllVersionsResponse{ + Success: true, + Data: versions, }, nil } diff --git a/cli/provider/mixed.go b/cli/provider/mixed.go index 0081f0c..00def81 100644 --- a/cli/provider/mixed.go +++ b/cli/provider/mixed.go @@ -50,7 +50,7 @@ func (p MixedProvider) SMLVersions(context context.Context) (*ficsit.SMLVersions return p.ficsitProvider.SMLVersions(context) } -func (p MixedProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) { +func (p MixedProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.AllVersionsResponse, error) { if p.Offline { return p.localProvider.ModVersionsWithDependencies(context, modID) } diff --git a/cli/provider/provider.go b/cli/provider/provider.go index 1e211db..d0b22a4 100644 --- a/cli/provider/provider.go +++ b/cli/provider/provider.go @@ -11,7 +11,7 @@ type Provider interface { GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error) ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error) - ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) + ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.AllVersionsResponse, error) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error) IsOffline() bool } diff --git a/cli/resolving_test.go b/cli/resolving_test.go index 9f0bfbb..28963e5 100644 --- a/cli/resolving_test.go +++ b/cli/resolving_test.go @@ -86,7 +86,7 @@ func TestResolutionNonExistentMod(t *testing.T) { }, }).Resolve(resolver, nil, math.MaxInt) - testza.AssertEqual(t, "failed resolving profile dependencies: failed to solve dependencies: failed to make decision: failed to get package versions: mod ThisModDoesNotExist$$$ not found", err.Error()) + testza.AssertEqual(t, "failed resolving profile dependencies: failed to solve dependencies: failed to make decision: failed to get package versions: mod ThisModDoesNotExist$$$ not found: mod not found", err.Error()) } func TestUpdateMods(t *testing.T) { diff --git a/cli/test_helpers.go b/cli/test_helpers.go index 38101a0..e7d434e 100644 --- a/cli/test_helpers.go +++ b/cli/test_helpers.go @@ -163,305 +163,268 @@ func (m MockProvider) SMLVersions(_ context.Context) (*ficsit.SMLVersionsRespons }, nil } -var commonTargets = []ficsit.ModVersionsWithDependenciesModVersionsVersionTargetsVersionTarget{ +var commonTargets = []ficsit.Target{ { - TargetName: ficsit.TargetNameWindows, - Link: "/v1/version/7QcfNdo5QAAyoC/Windows/download", + TargetName: "Windows", Hash: "62f5c84eca8480b3ffe7d6c90f759e3b463f482530e27d854fd48624fdd3acc9", }, { - TargetName: ficsit.TargetNameWindowsserver, - Link: "/v1/version/7QcfNdo5QAAyoC/WindowsServer/download", + TargetName: "WindowsServer", Hash: "8a83fcd4abece4192038769cc672fff6764d72c32fb6c7a8c58d66156bb07917", }, { - TargetName: ficsit.TargetNameLinuxserver, - Link: "/v1/version/7QcfNdo5QAAyoC/LinuxServer/download", + TargetName: "LinuxServer", Hash: "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85", }, } -func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) { +func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) { switch modID { case "RefinedPower": - return &ficsit.ModVersionsWithDependenciesResponse{ - Mod: ficsit.ModVersionsWithDependenciesMod{ - Id: "DGiLzB3ZErWu2V", - Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ - { - Id: "Eqgr4VcB8y1z9a", - Version: "3.2.13", - Link: "/v1/version/Eqgr4VcB8y1z9a/download", - Hash: "8cabf9245e3f2a01b95cd3d39d98e407cfeccf355c19f1538fcbf868f81de008", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "ModularUI", - Condition: "^2.1.11", - Optional: false, - }, - { - Mod_id: "RefinedRDLib", - Condition: "^1.1.7", - Optional: false, - }, - { - Mod_id: "SML", - Condition: "^3.6.1", - Optional: false, - }, + return &ficsit.AllVersionsResponse{ + Success: true, + Data: []ficsit.ModVersion{ + { + ID: "7QcfNdo5QAAyoC", + Version: "3.2.13", + Dependencies: []ficsit.Dependency{ + { + ModID: "ModularUI", + Condition: "^2.1.11", + Optional: false, }, - Targets: commonTargets, - }, - { - Id: "BwVKMJNP8doDLg", - Version: "3.2.11", - Link: "/v1/version/BwVKMJNP8doDLg/download", - Hash: "b64aa7b3a4766295323eac47d432e0d857d042c9cfb1afdd16330483b0476c89", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "ModularUI", - Condition: "^2.1.10", - Optional: false, - }, - { - Mod_id: "RefinedRDLib", - Condition: "^1.1.6", - Optional: false, - }, - { - Mod_id: "SML", - Condition: "^3.6.0", - Optional: false, - }, + { + ModID: "RefinedRDLib", + Condition: "^1.1.7", + Optional: false, }, - Targets: commonTargets, - }, - { - Id: "4XTjMpqFngbu9r", - Version: "3.2.10", - Link: "/v1/version/4XTjMpqFngbu9r/download", - Hash: "093f92c6d52c853bade386d5bc79cf103b27fb6e9d6f806850929b866ff98222", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "ModularUI", - Condition: "^2.1.9", - Optional: false, - }, - { - Mod_id: "RefinedRDLib", - Condition: "^1.1.5", - Optional: false, - }, - { - Mod_id: "SML", - Condition: "^3.6.0", - Optional: false, - }, + { + ModID: "SML", + Condition: "^3.6.1", + Optional: false, }, - Targets: commonTargets, }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "3.2.11", + Dependencies: []ficsit.Dependency{ + { + ModID: "ModularUI", + Condition: "^2.1.10", + Optional: false, + }, + { + ModID: "RefinedRDLib", + Condition: "^1.1.6", + Optional: false, + }, + { + ModID: "SML", + Condition: "^3.6.0", + Optional: false, + }, + }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "3.2.10", + Dependencies: []ficsit.Dependency{ + { + ModID: "ModularUI", + Condition: "^2.1.9", + Optional: false, + }, + { + ModID: "RefinedRDLib", + Condition: "^1.1.5", + Optional: false, + }, + { + ModID: "SML", + Condition: "^3.6.0", + Optional: false, + }, + }, + Targets: commonTargets, }, }, }, nil case "AreaActions": - return &ficsit.ModVersionsWithDependenciesResponse{ - Mod: ficsit.ModVersionsWithDependenciesMod{ - Id: "6vQ6ckVYFiidDh", - Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ - { - Id: "5KMXBkdAz5YJe", - Version: "1.6.7", - Link: "/v1/version/5KMXBkdAz5YJe/download", - Hash: "0baa673eea245b8ec5fe203a70b98deb666d85e27fb6ce9201e3c0fa3aaedcbe", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.4.1", - Optional: false, - }, + return &ficsit.AllVersionsResponse{ + Success: true, + Data: []ficsit.ModVersion{ + { + ID: "7QcfNdo5QAAyoC", + Version: "1.6.7", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.4.1", + Optional: false, }, - Targets: commonTargets, }, - { - Id: "EtEbwJj3smMn3o", - Version: "1.6.6", - Link: "/v1/version/EtEbwJj3smMn3o/download", - Hash: "b64aa7b3a4766295323eac47d432e0d857d042c9cfb1afdd16330483b0476c89", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.2.0", - Optional: false, - }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "1.6.6", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.2.0", + Optional: false, }, - Targets: commonTargets, }, - { - Id: "9uw1eDwgrQs279", - Version: "1.6.5", - Link: "/v1/version/9uw1eDwgrQs279/download", - Hash: "427a93383fe8a8557096666b7e81bf5fb25f54a5428248904f52adc4dc34d60c", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.0.0", - Optional: false, - }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "1.6.5", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.0.0", + Optional: false, }, - Targets: commonTargets, }, + Targets: commonTargets, }, }, }, nil case "RefinedRDLib": - return &ficsit.ModVersionsWithDependenciesResponse{ - Mod: ficsit.ModVersionsWithDependenciesMod{ - Id: "B24emzbs6xVZQr", - Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ - { - Id: "2XcE6RUzGhZW7p", - Version: "1.1.7", - Link: "/v1/version/2XcE6RUzGhZW7p/download", - Hash: "034f3a7862d0153768e1a95d29d47a9d08ebcb7ff0fc8f9f2cb59147b09f16dd", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.6.1", - Optional: false, - }, + return &ficsit.AllVersionsResponse{ + Success: true, + Data: []ficsit.ModVersion{ + { + ID: "7QcfNdo5QAAyoC", + Version: "1.1.7", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.6.1", + Optional: false, }, - Targets: commonTargets, }, - { - Id: "52RMLEigqT5Ksn", - Version: "1.1.6", - Link: "/v1/version/52RMLEigqT5Ksn/download", - Hash: "9577e401e1a12a29657c8e3ed0cff34815009504dc62fc1a335b1e7a3b6fed12", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.6.0", - Optional: false, - }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "1.1.6", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.6.0", + Optional: false, }, - Targets: commonTargets, }, - { - Id: "F4HY9eP4D5XjWQ", - Version: "1.1.5", - Link: "/v1/version/F4HY9eP4D5XjWQ/download", - Hash: "9cbeae078e28a661ebe15642e6d8f652c6c40c50dabd79a0781e25b84ed9bddf", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.6.0", - Optional: false, - }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "1.1.5", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.6.0", + Optional: false, }, - Targets: commonTargets, }, + Targets: commonTargets, }, }, }, nil case "ModularUI": - return &ficsit.ModVersionsWithDependenciesResponse{ - Mod: ficsit.ModVersionsWithDependenciesMod{ - Id: "As2uJmQLLxjXLG", - Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ - { - Id: "7ay11W9MAv6MHs", - Version: "2.1.12", - Link: "/v1/version/7ay11W9MAv6MHs/download", - Hash: "a0de64c02448f9e37903e7569cc6ceee67f8e018f2774aac9cf295704b9e4696", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.6.1", - Optional: false, - }, + return &ficsit.AllVersionsResponse{ + Success: true, + Data: []ficsit.ModVersion{ + { + ID: "7QcfNdo5QAAyoC", + Version: "2.1.12", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.6.1", + Optional: false, }, - Targets: commonTargets, }, - { - Id: "4YuL9UbCDdzm68", - Version: "2.1.11", - Link: "/v1/version/4YuL9UbCDdzm68/download", - Hash: "b70658bfa74c132530046bee886c3c0f0277b95339b4fc67da6207cbd2cd422d", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.6.0", - Optional: false, - }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "2.1.11", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.6.0", + Optional: false, }, - Targets: commonTargets, }, - { - Id: "5yY2zmx5nTyhWv", - Version: "2.1.10", - Link: "/v1/version/5yY2zmx5nTyhWv/download", - Hash: "7c523c9e6263a0b182ed42fe4d4de40aada10c17b1b344219618cd39055870bd", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.6.0", - Optional: false, - }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "2.1.10", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.6.0", + Optional: false, }, - Targets: commonTargets, }, + Targets: commonTargets, }, }, }, nil case "ThisModDoesNotExist$$$": - return &ficsit.ModVersionsWithDependenciesResponse{}, nil + return &ficsit.AllVersionsResponse{ + Success: false, + Error: &ficsit.Error{ + Message: "mod not found", + Code: 200, + }, + }, nil case "FicsitRemoteMonitoring": - return &ficsit.ModVersionsWithDependenciesResponse{ - Mod: ficsit.ModVersionsWithDependenciesMod{ - Id: "9LguyCdDUrpT9N", - Versions: []ficsit.ModVersionsWithDependenciesModVersionsVersion{ - { - Id: "7ay11W9MAv6MHs", - Version: "0.10.1", - Link: "/v1/version/9LguyCdDUrpT9N/download", - Hash: "9278b37653ad33dd859875929b15cd1f8aba88d0ea65879df2db1ae8808029d4", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.6.0", - Optional: false, - }, + return &ficsit.AllVersionsResponse{ + Success: true, + Data: []ficsit.ModVersion{ + { + ID: "7QcfNdo5QAAyoC", + Version: "0.10.1", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.6.0", + Optional: false, }, - Targets: commonTargets, }, - { - Id: "DYvfwan5tYqZKE", - Version: "0.10.0", - Link: "/v1/version/DYvfwan5tYqZKE/download", - Hash: "8666b37b24188c3f56b1dad6f1d437c1127280381172a1046e85142e7cb81c64", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.5.0", - Optional: false, - }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "0.10.0", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.5.0", + Optional: false, }, - Targets: commonTargets, }, - { - Id: "918KMrX94xFpVw", - Version: "0.9.8", - Link: "/v1/version/918KMrX94xFpVw/download", - Hash: "d4fed641b6ecb25b9191f4dd7210576e9bd7bc644abcb3ca592200ccfd08fc44", - Dependencies: []ficsit.ModVersionsWithDependenciesModVersionsVersionDependenciesVersionDependency{ - { - Mod_id: "SML", - Condition: "^3.4.1", - Optional: false, - }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "0.9.8", + Dependencies: []ficsit.Dependency{ + { + ModID: "SML", + Condition: "^3.4.1", + Optional: false, }, - Targets: commonTargets, }, + Targets: commonTargets, }, }, }, nil diff --git a/ficsit/rest.go b/ficsit/rest.go new file mode 100644 index 0000000..d7bbc1e --- /dev/null +++ b/ficsit/rest.go @@ -0,0 +1,33 @@ +package ficsit + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/spf13/viper" +) + +const allVersionEndpoint = `/v1/mod/%s/versions/all` + +func GetAllModVersions(modID string) (*AllVersionsResponse, error) { + response, err := http.DefaultClient.Get(viper.GetString("api-base") + fmt.Sprintf(allVersionEndpoint, modID)) + if err != nil { + return nil, fmt.Errorf("failed fetching all versions: %w", err) + } + + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("failed reading response body: %w", err) + } + + allVersions := AllVersionsResponse{} + if err := json.Unmarshal(body, &allVersions); err != nil { + return nil, fmt.Errorf("failed parsing json: %w", err) + } + + return &allVersions, nil +} diff --git a/ficsit/types_rest.go b/ficsit/types_rest.go new file mode 100644 index 0000000..f9c5337 --- /dev/null +++ b/ficsit/types_rest.go @@ -0,0 +1,32 @@ +package ficsit + +type AllVersionsResponse struct { + Error *Error `json:"error,omitempty"` + Data []ModVersion `json:"data,omitempty"` + Success bool `json:"success"` +} + +type ModVersion struct { + ID string `json:"id"` + Version string `json:"version"` + Dependencies []Dependency `json:"dependencies"` + Targets []Target `json:"targets"` +} + +type Dependency struct { + ModID string `json:"mod_id"` + Condition string `json:"condition"` + Optional bool `json:"optional"` +} + +type Target struct { + VersionID string `json:"version_id"` + TargetName string `json:"target_name"` + Hash string `json:"hash"` + Size int64 `json:"size"` +} + +type Error struct { + Message string `json:"message"` + Code int64 `json:"code"` +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0c52fe0 --- /dev/null +++ b/flake.lock @@ -0,0 +1,75 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1700014976, + "narHash": "sha256-dSGpS2YeJrXW5aH9y7Abd235gGufY3RuZFth6vuyVtU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "592047fc9e4f7b74a4dc85d1b9f5243dfe4899e3", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1701040486, + "narHash": "sha256-vawYwoHA5CwvjfqaT3A5CT9V36Eq43gxdwpux32Qkjw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "45827faa2132b8eade424f6bdd48d8828754341a", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixpkgs-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0de54ec --- /dev/null +++ b/flake.nix @@ -0,0 +1,19 @@ +{ + description = "smr-cli"; + + inputs = { + flake-utils.url = "github:numtide/flake-utils"; + nixpkgs-unstable.url = "flake:nixpkgs/nixpkgs-unstable"; + }; + + outputs = { self, nixpkgs, flake-utils, nixpkgs-unstable }: + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + unstable = nixpkgs-unstable.legacyPackages.${system}; in + { + devShells.default = import ./shell.nix { inherit pkgs unstable; }; + } + ); +} \ No newline at end of file diff --git a/go.mod b/go.mod index 9ecf293..9fdd0cf 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/satisfactorymodding/ficsit-cli go 1.21 +toolchain go1.21.4 + require ( github.com/JohannesKaufmann/html-to-markdown v1.4.2 github.com/Khan/genqlient v0.6.0 diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..5d349dc --- /dev/null +++ b/shell.nix @@ -0,0 +1,8 @@ +{ pkgs, unstable }: + +pkgs.mkShell { + nativeBuildInputs = with pkgs.buildPackages; [ + unstable.go_1_21 + unstable.golangci-lint + ]; +} diff --git a/tea/scenes/apply.go b/tea/scenes/apply.go index eaac113..4f46559 100644 --- a/tea/scenes/apply.go +++ b/tea/scenes/apply.go @@ -2,6 +2,7 @@ package scenes import ( "sort" + "sync" "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" @@ -32,57 +33,70 @@ type status struct { } type apply struct { - root components.RootModel - parent tea.Model - error *components.ErrorComponent - installChannel chan string - updateChannel chan cli.InstallUpdate - doneChannel chan bool - errorChannel chan error - cancelChannel chan bool - title string - status status - overall progress.Model - sub progress.Model - cancelled bool + root components.RootModel + parent tea.Model + error *components.ErrorComponent + updateChannel chan applyUpdate + doneChannel chan bool + errorChannel chan error + cancelChannel chan bool + title string + status map[string]status + overall progress.Model + sub progress.Model + cancelled bool + done bool +} + +type applyUpdate struct { + Installation *cli.Installation + Update cli.InstallUpdate + Done bool } func NewApply(root components.RootModel, parent tea.Model) tea.Model { overall := progress.New(progress.WithSolidFill("118")) sub := progress.New(progress.WithSolidFill("202")) - installChannel := make(chan string) - updateChannel := make(chan cli.InstallUpdate) + updateChannel := make(chan applyUpdate) doneChannel := make(chan bool, 1) errorChannel := make(chan error) cancelChannel := make(chan bool, 1) model := &apply{ - root: root, - parent: parent, - title: teaUtils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"), - overall: overall, - sub: sub, - status: status{ - installName: "", - done: false, - }, - installChannel: installChannel, - updateChannel: updateChannel, - doneChannel: doneChannel, - errorChannel: errorChannel, - cancelChannel: cancelChannel, - cancelled: false, + root: root, + parent: parent, + title: teaUtils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"), + overall: overall, + sub: sub, + status: make(map[string]status), + updateChannel: updateChannel, + doneChannel: doneChannel, + errorChannel: errorChannel, + cancelChannel: cancelChannel, } - go func() { - for _, installation := range root.GetGlobal().Installations.Installations { - installChannel <- installation.Path + var wg sync.WaitGroup + + for _, installation := range root.GetGlobal().Installations.Installations { + wg.Add(1) + + model.status[installation.Path] = status{ + modProgresses: make(map[string]modProgress), + installName: installation.Path, + overallProgress: utils.GenericProgress{}, + } + + go func(installation *cli.Installation) { + defer wg.Done() installUpdateChannel := make(chan cli.InstallUpdate) go func() { for update := range installUpdateChannel { - updateChannel <- update + updateChannel <- applyUpdate{ + Installation: installation, + Update: update, + } } }() @@ -91,18 +105,15 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model { return } - stop := false - select { - case <-cancelChannel: - stop = true - default: + updateChannel <- applyUpdate{ + Installation: installation, + Done: true, } + }(installation) + } - if stop { - break - } - } - + go func() { + wg.Wait() doneChannel <- true }() @@ -122,6 +133,13 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case keys.KeyQ: fallthrough case keys.KeyEscape: + if m.done { + if m.parent != nil { + return m.parent, m.parent.Init() + } + return m, tea.Quit + } + m.cancelled = true if m.error != nil { @@ -134,7 +152,7 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cancelChannel <- true return m, nil case keys.KeyEnter: - if m.status.done || m.error != nil { + if m.done || m.error != nil { if m.parent != nil { return m.parent, m.parent.Init() } @@ -149,35 +167,37 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case teaUtils.TickMsg: select { case <-m.doneChannel: - m.status.done = true - m.status.installName = "" - break - case installName := <-m.installChannel: - m.status.installName = installName - m.status.modProgresses = make(map[string]modProgress) - m.status.overallProgress = utils.GenericProgress{} + m.done = true break case update := <-m.updateChannel: - switch update.Type { - case cli.InstallUpdateTypeOverall: - m.status.overallProgress = update.Progress - case cli.InstallUpdateTypeModDownload: - m.status.modProgresses[update.Item.Mod] = modProgress{ - downloadProgress: update.Progress, - downloading: true, - complete: false, - } - case cli.InstallUpdateTypeModExtract: - m.status.modProgresses[update.Item.Mod] = modProgress{ - extractProgress: update.Progress, - downloading: false, - complete: false, - } - case cli.InstallUpdateTypeModComplete: - m.status.modProgresses[update.Item.Mod] = modProgress{ - complete: true, + s := m.status[update.Installation.Path] + + if update.Done { + s.done = true + } else { + switch update.Update.Type { + case cli.InstallUpdateTypeOverall: + s.overallProgress = update.Update.Progress + case cli.InstallUpdateTypeModDownload: + s.modProgresses[update.Update.Item.Mod] = modProgress{ + downloadProgress: update.Update.Progress, + downloading: true, + complete: false, + } + case cli.InstallUpdateTypeModExtract: + s.modProgresses[update.Update.Item.Mod] = modProgress{ + extractProgress: update.Update.Progress, + downloading: false, + complete: false, + } + case cli.InstallUpdateTypeModComplete: + s.modProgresses[update.Update.Item.Mod] = modProgress{ + complete: true, + } } } + + m.status[update.Installation.Path] = s break case err := <-m.errorChannel: wrappedErrMessage := wrap.String(err.Error(), int(float64(m.root.Size().Width)*0.8)) @@ -197,33 +217,77 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m apply) View() string { strs := make([]string, 0) - if m.status.installName != "" { - strs = append(strs, lipgloss.NewStyle().Render(m.status.installName)) - strs = append(strs, lipgloss.NewStyle().MarginBottom(1).Render(m.overall.ViewAs(m.status.overallProgress.Percentage()))) + installationList := make([]string, len(m.status)) + i := 0 + for key := range m.status { + installationList[i] = key + i++ } - modReferences := make([]string, 0) - for k := range m.status.modProgresses { - modReferences = append(modReferences, k) - } - sort.Strings(modReferences) + sort.Strings(installationList) - for _, modReference := range modReferences { - p := m.status.modProgresses[modReference] - if p.complete { - strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+modReference) - } else { - if p.downloading { - strs = append(strs, lipgloss.NewStyle().Render(modReference+" (Downloading)")) - strs = append(strs, m.sub.ViewAs(p.downloadProgress.Percentage())) - } else { - strs = append(strs, lipgloss.NewStyle().Render(modReference+" (Extracting)")) - strs = append(strs, m.sub.ViewAs(p.extractProgress.Percentage())) + totalHeight := 3 + 3 // Header + Footer + totalHeight += len(installationList) * 2 // Bottom Margin + Overall progress per-install + + bottomMargins := 1 + if m.root.Size().Height < totalHeight { + bottomMargins = 0 + } + + totalHeight += len(installationList) // Top margin + + topMargins := 1 + if m.root.Size().Height < totalHeight { + topMargins = 0 + } + + for _, installPath := range installationList { + totalHeight += len(m.status[installPath].modProgresses) + } + + for _, installPath := range installationList { + s := m.status[installPath] + + strs = append(strs, lipgloss.NewStyle().Margin(topMargins, 0, bottomMargins, 1).Render(lipgloss.JoinHorizontal( + lipgloss.Left, + m.overall.ViewAs(s.overallProgress.Percentage()), + " - ", + lipgloss.NewStyle().Render(installPath), + ))) + + modReferences := make([]string, 0) + for k := range s.modProgresses { + modReferences = append(modReferences, k) + } + sort.Strings(modReferences) + + if m.root.Size().Height > totalHeight { + for _, modReference := range modReferences { + p := s.modProgresses[modReference] + if p.complete || s.done { + strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+modReference) + } else { + if p.downloading { + strs = append(strs, lipgloss.JoinHorizontal( + lipgloss.Left, + m.sub.ViewAs(p.downloadProgress.Percentage()), + " - ", + lipgloss.NewStyle().Render(modReference+" (Downloading)"), + )) + } else { + strs = append(strs, lipgloss.JoinHorizontal( + lipgloss.Left, + m.sub.ViewAs(p.extractProgress.Percentage()), + " - ", + lipgloss.NewStyle().Render(modReference+" (Extracting)"), + )) + } + } } } } - if m.status.done { + if m.done { if m.cancelled { strs = append(strs, teaUtils.LabelStyle.Copy().Foreground(lipgloss.Color("196")).Padding(0).Margin(1).Render("Cancelled! Press Enter to return")) } else { diff --git a/tea/scenes/main_menu.go b/tea/scenes/main_menu.go index 6de6792..21bf546 100644 --- a/tea/scenes/main_menu.go +++ b/tea/scenes/main_menu.go @@ -228,6 +228,6 @@ func (m mainMenu) View() string { return lipgloss.JoinVertical(lipgloss.Left, header, err, m.list.View()) } - m.list.SetSize(m.list.Width(), m.root.Size().Height-lipgloss.Height(header)-1) + m.list.SetSize(m.list.Width(), m.root.Size().Height-lipgloss.Height(header)) return lipgloss.JoinVertical(lipgloss.Left, header, m.list.View()) }