technically "functional", but you be the judge
This commit is contained in:
parent
688b8ca175
commit
bdcbb0b677
24 changed files with 800 additions and 107 deletions
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
@ -29,10 +28,15 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
|
|||
return nil, errors.Wrap(err, "failed fetching SMl versions")
|
||||
}
|
||||
|
||||
copied := make(map[string][]string, len(constraints))
|
||||
for k, v := range constraints {
|
||||
copied[k] = []string{v}
|
||||
}
|
||||
|
||||
instance := &resolvingInstance{
|
||||
Resolver: d,
|
||||
InputLock: lockFile,
|
||||
ToResolve: utils.CopyMap(constraints),
|
||||
ToResolve: copied,
|
||||
OutputLock: make(LockFile),
|
||||
SMLVersions: smlVersionsDB,
|
||||
GameVersion: gameVersion,
|
||||
|
@ -50,7 +54,7 @@ type resolvingInstance struct {
|
|||
|
||||
InputLock *LockFile
|
||||
|
||||
ToResolve map[string]string
|
||||
ToResolve map[string][]string
|
||||
|
||||
OutputLock LockFile
|
||||
|
||||
|
@ -69,11 +73,12 @@ func (r *resolvingInstance) Step() error {
|
|||
if id != "SML" {
|
||||
converted = append(converted, ficsit.ModVersionConstraint{
|
||||
ModIdOrReference: id,
|
||||
Version: constraint,
|
||||
Version: constraint[0],
|
||||
})
|
||||
} else {
|
||||
smlVersionConstraint, _ := semver.NewConstraint(constraint)
|
||||
if existingSML, ok := r.OutputLock[id]; ok {
|
||||
for _, cs := range constraint {
|
||||
smlVersionConstraint, _ := semver.NewConstraint(cs)
|
||||
if !smlVersionConstraint.Check(semver.MustParse(existingSML.Version)) {
|
||||
return errors.Errorf("mod %s version %s does not match constraint %s",
|
||||
id,
|
||||
|
@ -82,6 +87,7 @@ func (r *resolvingInstance) Step() error {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var chosenSMLVersion *semver.Version
|
||||
for _, version := range r.SMLVersions.SmlVersions.Sml_versions {
|
||||
|
@ -90,7 +96,18 @@ func (r *resolvingInstance) Step() error {
|
|||
}
|
||||
|
||||
currentVersion := semver.MustParse(version.Version)
|
||||
if smlVersionConstraint.Check(currentVersion) {
|
||||
|
||||
matches := true
|
||||
for _, cs := range constraint {
|
||||
smlVersionConstraint, _ := semver.NewConstraint(cs)
|
||||
|
||||
if !smlVersionConstraint.Check(currentVersion) {
|
||||
matches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matches {
|
||||
if chosenSMLVersion == nil || currentVersion.GreaterThan(chosenSMLVersion) {
|
||||
chosenSMLVersion = currentVersion
|
||||
}
|
||||
|
@ -98,7 +115,7 @@ func (r *resolvingInstance) Step() error {
|
|||
}
|
||||
|
||||
if chosenSMLVersion == nil {
|
||||
return fmt.Errorf("could not find an SML version that matches constraint %s and game version %d", constraint, r.GameVersion)
|
||||
return errors.Errorf("could not find an SML version that matches constraint %s and game version %d", constraint, r.GameVersion)
|
||||
}
|
||||
|
||||
r.OutputLock[id] = LockedMod{
|
||||
|
@ -109,7 +126,7 @@ func (r *resolvingInstance) Step() error {
|
|||
}
|
||||
}
|
||||
|
||||
r.ToResolve = make(map[string]string)
|
||||
nextResolve := make(map[string][]string)
|
||||
|
||||
// TODO Cache
|
||||
dependencies, err := ficsit.ResolveModDependencies(context.TODO(), r.Resolver.apiClient, converted)
|
||||
|
@ -147,7 +164,27 @@ func (r *resolvingInstance) Step() error {
|
|||
|
||||
// Pick latest version
|
||||
// TODO Clone and branch
|
||||
selectedVersion := modVersions[0]
|
||||
var selectedVersion ModVersion
|
||||
for _, version := range modVersions {
|
||||
matches := true
|
||||
|
||||
for _, cs := range r.ToResolve[mod.Mod_reference] {
|
||||
resolvingConstraint, _ := semver.NewConstraint(cs)
|
||||
if !resolvingConstraint.Check(semver.MustParse(version.Version)) {
|
||||
matches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matches {
|
||||
selectedVersion = version
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if selectedVersion.Version == "" {
|
||||
return errors.Errorf("no version of %s matches constraints", mod.Mod_reference)
|
||||
}
|
||||
|
||||
if _, ok := r.OutputLock[mod.Mod_reference]; ok {
|
||||
if r.OutputLock[mod.Mod_reference].Version != selectedVersion.Version {
|
||||
|
@ -181,9 +218,11 @@ func (r *resolvingInstance) Step() error {
|
|||
}
|
||||
}
|
||||
|
||||
if resolving, ok := r.ToResolve[dependency.ModReference]; ok {
|
||||
if resolving, ok := nextResolve[dependency.ModReference]; ok {
|
||||
constraint, _ := semver.NewConstraint(dependency.Constraint)
|
||||
resolvingConstraint, _ := semver.NewConstraint(resolving)
|
||||
|
||||
for _, cs := range resolving {
|
||||
resolvingConstraint, _ := semver.NewConstraint(cs)
|
||||
intersects, _ := constraint.Intersects(resolvingConstraint)
|
||||
if !intersects {
|
||||
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
||||
|
@ -193,15 +232,18 @@ func (r *resolvingInstance) Step() error {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dependency.Optional {
|
||||
continue
|
||||
}
|
||||
|
||||
r.ToResolve[dependency.ModReference] = dependency.Constraint
|
||||
nextResolve[dependency.ModReference] = append(nextResolve[dependency.ModReference], dependency.Constraint)
|
||||
}
|
||||
}
|
||||
|
||||
r.ToResolve = nextResolve
|
||||
|
||||
for _, constraint := range converted {
|
||||
if _, ok := r.OutputLock[constraint.ModIdOrReference]; !ok {
|
||||
return errors.New("failed resolving dependency: " + constraint.ModIdOrReference)
|
||||
|
@ -221,7 +263,7 @@ func (r *resolvingInstance) Step() error {
|
|||
func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
||||
added := false
|
||||
if r.InputLock != nil {
|
||||
for modReference, version := range r.ToResolve {
|
||||
for modReference, constraints := range r.ToResolve {
|
||||
if _, ok := viewed[modReference]; ok {
|
||||
continue
|
||||
}
|
||||
|
@ -229,15 +271,25 @@ func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
|||
viewed[modReference] = true
|
||||
|
||||
if locked, ok := (*r.InputLock)[modReference]; ok {
|
||||
constraint, _ := semver.NewConstraint(version)
|
||||
if constraint.Check(semver.MustParse(locked.Version)) {
|
||||
passes := true
|
||||
|
||||
for _, cs := range constraints {
|
||||
constraint, _ := semver.NewConstraint(cs)
|
||||
if !constraint.Check(semver.MustParse(locked.Version)) {
|
||||
passes = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if passes {
|
||||
delete(r.ToResolve, modReference)
|
||||
r.OutputLock[modReference] = locked
|
||||
for k, v := range locked.Dependencies {
|
||||
if alreadyResolving, ok := r.ToResolve[k]; ok {
|
||||
cs1, _ := semver.NewConstraint(v)
|
||||
cs2, _ := semver.NewConstraint(alreadyResolving)
|
||||
intersects, _ := cs1.Intersects(cs2)
|
||||
newConstraint, _ := semver.NewConstraint(v)
|
||||
for _, resolvingConstraint := range alreadyResolving {
|
||||
cs2, _ := semver.NewConstraint(resolvingConstraint)
|
||||
intersects, _ := newConstraint.Intersects(cs2)
|
||||
if !intersects {
|
||||
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
||||
k,
|
||||
|
@ -245,6 +297,10 @@ func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
|||
alreadyResolving,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
r.ToResolve[k] = append(r.ToResolve[k], v)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -260,7 +316,7 @@ func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
|||
continue
|
||||
}
|
||||
|
||||
r.ToResolve[k] = v
|
||||
r.ToResolve[k] = append(r.ToResolve[k], v)
|
||||
added = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||
|
||||
|
@ -224,30 +226,84 @@ func (i *Installation) Validate(ctx *GlobalContext) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (i *Installation) Install(ctx *GlobalContext) error {
|
||||
if err := i.Validate(ctx); err != nil {
|
||||
return errors.Wrap(err, "failed to validate installation")
|
||||
}
|
||||
var (
|
||||
lockFileCleaner = regexp.MustCompile(`[^a-zA-Z\d]]`)
|
||||
matchFirstCap = regexp.MustCompile(`(.)([A-Z][a-z]+)`)
|
||||
matchAllCap = regexp.MustCompile(`([a-z\d])([A-Z])`)
|
||||
)
|
||||
|
||||
func (i *Installation) LockFilePath(ctx *GlobalContext) (string, error) {
|
||||
platform, err := i.GetPlatform(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
lockfilePath := path.Join(i.Path, platform.LockfilePath)
|
||||
lockFileName := ctx.Profiles.Profiles[i.Profile].Name
|
||||
lockFileName = matchFirstCap.ReplaceAllString(lockFileName, "${1}_${2}")
|
||||
lockFileName = matchAllCap.ReplaceAllString(lockFileName, "${1}_${2}")
|
||||
lockFileName = lockFileCleaner.ReplaceAllLiteralString(lockFileName, "-")
|
||||
lockFileName = strings.ToLower(lockFileName) + "-lock.json"
|
||||
|
||||
return path.Join(i.Path, platform.LockfilePath, lockFileName), nil
|
||||
}
|
||||
|
||||
func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) {
|
||||
lockfilePath, err := i.LockFilePath(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lockFile *LockFile
|
||||
lockFileJSON, err := os.ReadFile(lockfilePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return errors.Wrap(err, "failed reading lockfile")
|
||||
return nil, errors.Wrap(err, "failed reading lockfile")
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal(lockFileJSON, &lockFile); err != nil {
|
||||
return errors.Wrap(err, "failed parsing lockfile")
|
||||
return nil, errors.Wrap(err, "failed parsing lockfile")
|
||||
}
|
||||
}
|
||||
|
||||
return lockFile, nil
|
||||
}
|
||||
|
||||
func (i *Installation) WriteLockFile(ctx *GlobalContext, lockfile LockFile) error {
|
||||
lockfilePath, err := i.LockFilePath(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
marshaledLockfile, err := json.MarshalIndent(lockfile, "", " ")
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to serialize lockfile json")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(lockfilePath, marshaledLockfile, 0777); err != nil {
|
||||
return errors.Wrap(err, "failed writing lockfile")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type InstallUpdate struct {
|
||||
ModName string
|
||||
OverallProgress float64
|
||||
DownloadProgress float64
|
||||
ExtractProgress float64
|
||||
}
|
||||
|
||||
func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) error {
|
||||
if err := i.Validate(ctx); err != nil {
|
||||
return errors.Wrap(err, "failed to validate installation")
|
||||
}
|
||||
|
||||
lockFile, err := i.LockFile(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resolver := NewDependencyResolver(ctx.APIClient)
|
||||
|
||||
gameVersion, err := i.GetGameVersion(ctx)
|
||||
|
@ -266,28 +322,79 @@ func (i *Installation) Install(ctx *GlobalContext) error {
|
|||
return errors.Wrap(err, "failed creating Mods directory")
|
||||
}
|
||||
|
||||
dir, err := os.ReadDir(modsDirectory)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read mods directory")
|
||||
}
|
||||
|
||||
for _, entry := range dir {
|
||||
if entry.IsDir() {
|
||||
if _, ok := lockfile[entry.Name()]; !ok {
|
||||
if err := os.RemoveAll(path.Join(modsDirectory, entry.Name())); err != nil {
|
||||
return errors.Wrap(err, "failed to delete mod directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completed := 0
|
||||
for modReference, version := range lockfile {
|
||||
// Only install if a link is provided, otherwise assume mod is already installed
|
||||
if version.Link != "" {
|
||||
reader, size, err := utils.DownloadOrCache(modReference+"_"+version.Version+".zip", version.Hash, version.Link)
|
||||
downloading := true
|
||||
|
||||
var genericUpdates chan utils.GenericUpdate
|
||||
if updates != nil {
|
||||
genericUpdates = make(chan utils.GenericUpdate)
|
||||
|
||||
go func() {
|
||||
update := InstallUpdate{
|
||||
ModName: modReference,
|
||||
OverallProgress: float64(completed) / float64(len(lockfile)),
|
||||
DownloadProgress: 0,
|
||||
ExtractProgress: 0,
|
||||
}
|
||||
|
||||
select {
|
||||
case updates <- update:
|
||||
default:
|
||||
}
|
||||
|
||||
for up := range genericUpdates {
|
||||
if downloading {
|
||||
update.DownloadProgress = up.Progress
|
||||
} else {
|
||||
update.DownloadProgress = 1
|
||||
update.ExtractProgress = up.Progress
|
||||
}
|
||||
|
||||
select {
|
||||
case updates <- update:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
reader, size, err := utils.DownloadOrCache(modReference+"_"+version.Version+".zip", version.Hash, version.Link, genericUpdates)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to download "+modReference+" from: "+version.Link)
|
||||
}
|
||||
|
||||
if err := utils.ExtractMod(reader, size, path.Join(modsDirectory, modReference)); err != nil {
|
||||
downloading = false
|
||||
|
||||
if err := utils.ExtractMod(reader, size, path.Join(modsDirectory, modReference), genericUpdates); err != nil {
|
||||
return errors.Wrap(err, "could not extract "+modReference)
|
||||
}
|
||||
}
|
||||
|
||||
close(genericUpdates)
|
||||
}
|
||||
|
||||
marshaledLockfile, err := json.MarshalIndent(lockfile, "", " ")
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to serialize lockfile json")
|
||||
completed++
|
||||
}
|
||||
|
||||
if err := os.WriteFile(lockfilePath, marshaledLockfile, 0777); err != nil {
|
||||
return errors.Wrap(err, "failed writing lockfile")
|
||||
if err := i.WriteLockFile(ctx, lockfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestAddInstallation(t *testing.T) {
|
|||
testza.AssertNoError(t, err)
|
||||
testza.AssertNotNil(t, installation)
|
||||
|
||||
err = installation.Install(ctx)
|
||||
err = installation.Install(ctx, nil)
|
||||
testza.AssertNoError(t, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,14 +10,14 @@ type Platform struct {
|
|||
var platforms = []Platform{
|
||||
{
|
||||
VersionPath: path.Join("Engine", "Binaries", "Linux", "UE4Server-Linux-Shipping.version"),
|
||||
LockfilePath: path.Join("FactoryGame", "Mods", "mods-lock.json"),
|
||||
LockfilePath: path.Join("FactoryGame", "Mods"),
|
||||
},
|
||||
{
|
||||
VersionPath: path.Join("Engine", "Binaries", "Win64", "UE4Server-Win64-Shipping.version"),
|
||||
LockfilePath: path.Join("FactoryGame", "Mods", "mods-lock.json"),
|
||||
LockfilePath: path.Join("FactoryGame", "Mods"),
|
||||
},
|
||||
{
|
||||
VersionPath: path.Join("Engine", "Binaries", "Win64", "FactoryGame-Win64-Shipping.version"),
|
||||
LockfilePath: path.Join("FactoryGame", "Mods", "mods-lock.json"),
|
||||
LockfilePath: path.Join("FactoryGame", "Mods"),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -95,10 +95,8 @@ func init() {
|
|||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
baseLocalDir = os.Getenv("APPDATA")
|
||||
break
|
||||
case "linux":
|
||||
baseLocalDir = path.Join(os.Getenv("HOME"), ".local", "share")
|
||||
break
|
||||
default:
|
||||
panic("unsupported platform: " + runtime.GOOS)
|
||||
}
|
||||
|
|
3
go.mod
3
go.mod
|
@ -5,7 +5,7 @@ go 1.18
|
|||
require (
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.3.3
|
||||
github.com/Khan/genqlient v0.4.0
|
||||
github.com/MarvinJWendt/testza v0.4.1
|
||||
github.com/MarvinJWendt/testza v0.4.2
|
||||
github.com/Masterminds/semver/v3 v3.1.1
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/charmbracelet/bubbles v0.10.3
|
||||
|
@ -30,6 +30,7 @@ require (
|
|||
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/harmonica v0.1.0 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
|
|
5
go.sum
5
go.sum
|
@ -50,8 +50,8 @@ github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSr
|
|||
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
|
||||
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
|
||||
github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY=
|
||||
github.com/MarvinJWendt/testza v0.4.1 h1:bqidLqFVtySvyq7D+xIfFKefl+AfJtDpivXC9fx3hm4=
|
||||
github.com/MarvinJWendt/testza v0.4.1/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||
github.com/MarvinJWendt/testza v0.4.2 h1:Vbw9GkSB5erJI2BPnBL9SVGV9myE+XmUSFahBGUhW2Q=
|
||||
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
|
@ -89,6 +89,7 @@ github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtD
|
|||
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
|
||||
github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g=
|
||||
github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
|
||||
github.com/charmbracelet/harmonica v0.1.0 h1:lFKeSd6OAckQ/CEzPVd2mqj+YMEubQ/3FM2IYY3xNm0=
|
||||
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
|
|
48
tea/components/error.go
Normal file
48
tea/components/error.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package components
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
|
||||
)
|
||||
|
||||
var _ tea.Model = (*ErrorComponent)(nil)
|
||||
|
||||
type ErrorComponent struct {
|
||||
message string
|
||||
labelStyle lipgloss.Style
|
||||
}
|
||||
|
||||
func NewErrorComponent(message string, duration time.Duration) (*ErrorComponent, tea.Cmd) {
|
||||
timer := time.NewTimer(duration)
|
||||
|
||||
return &ErrorComponent{
|
||||
message: message,
|
||||
labelStyle: utils.LabelStyle,
|
||||
}, func() tea.Msg {
|
||||
<-timer.C
|
||||
return ErrorComponentTimeoutMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
func (e ErrorComponent) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e ErrorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (e ErrorComponent) View() string {
|
||||
return lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("196")).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderForeground(lipgloss.Color("196")).
|
||||
Padding(0, 1).
|
||||
Margin(0, 0, 1, 2).
|
||||
Render(e.message)
|
||||
}
|
||||
|
||||
type ErrorComponentTimeoutMsg struct{}
|
207
tea/scenes/apply.go
Normal file
207
tea/scenes/apply.go
Normal file
|
@ -0,0 +1,207 @@
|
|||
package scenes
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/progress"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli"
|
||||
"github.com/satisfactorymodding/ficsit-cli/tea/components"
|
||||
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
|
||||
)
|
||||
|
||||
var _ tea.Model = (*apply)(nil)
|
||||
|
||||
type update struct {
|
||||
completed []string
|
||||
|
||||
installName string
|
||||
installTotal int
|
||||
installCurrent int
|
||||
|
||||
modName string
|
||||
modTotal int
|
||||
modCurrent int
|
||||
|
||||
done bool
|
||||
}
|
||||
|
||||
type apply struct {
|
||||
root components.RootModel
|
||||
parent tea.Model
|
||||
title string
|
||||
error *components.ErrorComponent
|
||||
overall progress.Model
|
||||
sub progress.Model
|
||||
|
||||
status update
|
||||
updateChannel chan update
|
||||
errorChannel chan error
|
||||
}
|
||||
|
||||
func NewApply(root components.RootModel, parent tea.Model) tea.Model {
|
||||
overall := progress.New(progress.WithSolidFill("118"))
|
||||
sub := progress.New(progress.WithSolidFill("202"))
|
||||
|
||||
updateChannel := make(chan update)
|
||||
errorChannel := make(chan error)
|
||||
|
||||
model := &apply{
|
||||
root: root,
|
||||
parent: parent,
|
||||
title: utils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"),
|
||||
overall: overall,
|
||||
sub: sub,
|
||||
status: update{
|
||||
completed: []string{},
|
||||
|
||||
installName: "",
|
||||
installTotal: 100,
|
||||
installCurrent: 0,
|
||||
|
||||
modName: "",
|
||||
modTotal: 100,
|
||||
modCurrent: 0,
|
||||
|
||||
done: false,
|
||||
},
|
||||
updateChannel: updateChannel,
|
||||
errorChannel: errorChannel,
|
||||
}
|
||||
|
||||
go func() {
|
||||
result := &update{
|
||||
completed: make([]string, 0),
|
||||
|
||||
installName: "",
|
||||
installTotal: 100,
|
||||
installCurrent: 0,
|
||||
|
||||
modName: "",
|
||||
modTotal: 100,
|
||||
modCurrent: 0,
|
||||
|
||||
done: false,
|
||||
}
|
||||
updateChannel <- *result
|
||||
|
||||
for _, installation := range root.GetGlobal().Installations.Installations {
|
||||
result.installName = installation.Path
|
||||
updateChannel <- *result
|
||||
|
||||
installChannel := make(chan cli.InstallUpdate)
|
||||
|
||||
go func() {
|
||||
for data := range installChannel {
|
||||
result.installName = installation.Path
|
||||
result.installCurrent = int(data.OverallProgress * 100)
|
||||
|
||||
if data.DownloadProgress < 1 {
|
||||
result.modName = "Downloading: " + data.ModName
|
||||
result.modCurrent = int(data.DownloadProgress * 100)
|
||||
} else {
|
||||
result.modName = "Extracting: " + data.ModName
|
||||
result.modCurrent = int(data.ExtractProgress * 100)
|
||||
}
|
||||
|
||||
updateChannel <- *result
|
||||
}
|
||||
}()
|
||||
|
||||
if err := installation.Install(root.GetGlobal(), installChannel); err != nil {
|
||||
errorChannel <- err
|
||||
return
|
||||
}
|
||||
|
||||
close(installChannel)
|
||||
|
||||
result.modName = ""
|
||||
result.installTotal = 100
|
||||
result.completed = append(result.completed, installation.Path)
|
||||
updateChannel <- *result
|
||||
}
|
||||
|
||||
result.done = true
|
||||
result.installName = ""
|
||||
updateChannel <- *result
|
||||
}()
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
func (m apply) Init() tea.Cmd {
|
||||
return utils.Ticker()
|
||||
}
|
||||
|
||||
func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch keypress := msg.String(); keypress {
|
||||
case KeyControlC:
|
||||
return m, tea.Quit
|
||||
case KeyEscape:
|
||||
// TODO Cancel
|
||||
return m, nil
|
||||
case KeyEnter:
|
||||
if m.status.done {
|
||||
if m.parent != nil {
|
||||
return m.parent, nil
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.root.SetSize(msg)
|
||||
case components.ErrorComponentTimeoutMsg:
|
||||
m.error = nil
|
||||
case utils.TickMsg:
|
||||
select {
|
||||
case newStatus := <-m.updateChannel:
|
||||
m.status = newStatus
|
||||
break
|
||||
case err := <-m.errorChannel:
|
||||
errorComponent, _ := components.NewErrorComponent(err.Error(), 0)
|
||||
m.error = errorComponent
|
||||
break
|
||||
default:
|
||||
// Skip if nothing there
|
||||
break
|
||||
}
|
||||
return m, utils.Ticker()
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m apply) View() string {
|
||||
strs := make([]string, 0)
|
||||
for _, s := range m.status.completed {
|
||||
strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+s)
|
||||
}
|
||||
|
||||
if m.status.installName != "" {
|
||||
marginTop := 0
|
||||
if len(m.status.completed) > 0 {
|
||||
marginTop = 1
|
||||
}
|
||||
|
||||
strs = append(strs, lipgloss.NewStyle().MarginTop(marginTop).Render(m.status.installName))
|
||||
strs = append(strs, m.overall.ViewAs(float64(m.status.installCurrent)/float64(m.status.installTotal)))
|
||||
}
|
||||
|
||||
if m.status.modName != "" {
|
||||
strs = append(strs, lipgloss.NewStyle().MarginTop(1).Render(m.status.modName))
|
||||
strs = append(strs, m.sub.ViewAs(float64(m.status.modCurrent)/float64(m.status.modTotal)))
|
||||
}
|
||||
|
||||
if m.status.done {
|
||||
strs = append(strs, utils.LabelStyle.Padding(0).Margin(1).Render("Done! Press Enter to return"))
|
||||
}
|
||||
|
||||
result := lipgloss.NewStyle().MarginLeft(1).Render(lipgloss.JoinVertical(lipgloss.Left, strs...))
|
||||
|
||||
if m.error != nil {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.title, (*m.error).View(), result)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.title, result)
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -20,6 +21,7 @@ type installation struct {
|
|||
parent tea.Model
|
||||
installation *cli.Installation
|
||||
hadRenamed bool
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
func NewInstallation(root components.RootModel, parent tea.Model, installationData *cli.Installation) tea.Model {
|
||||
|
@ -34,7 +36,9 @@ func NewInstallation(root components.RootModel, parent tea.Model, installationDa
|
|||
ItemTitle: "Select",
|
||||
Activate: func(msg tea.Msg, currentModel installation) (tea.Model, tea.Cmd) {
|
||||
if err := root.SetCurrentInstallation(installationData); err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
currentModel.error = errorComponent
|
||||
return currentModel, cmd
|
||||
}
|
||||
|
||||
return currentModel.parent, nil
|
||||
|
@ -44,7 +48,9 @@ func NewInstallation(root components.RootModel, parent tea.Model, installationDa
|
|||
ItemTitle: "Delete",
|
||||
Activate: func(msg tea.Msg, currentModel installation) (tea.Model, tea.Cmd) {
|
||||
if err := root.GetGlobal().Installations.DeleteInstallation(installationData.Path); err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
currentModel.error = errorComponent
|
||||
return currentModel, cmd
|
||||
}
|
||||
|
||||
return currentModel.parent, updateInstallationListCmd
|
||||
|
@ -61,6 +67,18 @@ func NewInstallation(root components.RootModel, parent tea.Model, installationDa
|
|||
model.list.StatusMessageLifetime = time.Second * 3
|
||||
model.list.DisableQuitKeybindings()
|
||||
|
||||
model.list.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
model.list.AdditionalFullHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
|
@ -112,11 +130,20 @@ func (m installation) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case updateInstallationNames:
|
||||
m.hadRenamed = true
|
||||
m.list.Title = fmt.Sprintf("Installation: %s", m.installation.Path)
|
||||
case components.ErrorComponentTimeoutMsg:
|
||||
m.error = nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m installation) View() string {
|
||||
if m.error != nil {
|
||||
err := (*m.error).View()
|
||||
m.list.SetSize(m.list.Width(), m.root.Size().Height-m.root.Height()-lipgloss.Height(err))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), err, m.list.View())
|
||||
}
|
||||
|
||||
m.list.SetSize(m.list.Width(), m.root.Size().Height-m.root.Height())
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ func NewInstallations(root components.RootModel, parent tea.Model) tea.Model {
|
|||
|
||||
l.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
key.NewBinding(key.WithHelp("n", "new installation")),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ var _ tea.Model = (*mainMenu)(nil)
|
|||
type mainMenu struct {
|
||||
root components.RootModel
|
||||
list list.Model
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
func NewMainMenu(root components.RootModel) tea.Model {
|
||||
|
@ -48,8 +49,15 @@ func NewMainMenu(root components.RootModel) tea.Model {
|
|||
utils.SimpleItem[mainMenu]{
|
||||
ItemTitle: "Apply Changes",
|
||||
Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) {
|
||||
// TODO Apply changes to all changed profiles
|
||||
return nil, nil
|
||||
if err := root.GetGlobal().Save(); err != nil {
|
||||
log.Error().Err(err).Msg(ErrorFailedAddMod)
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
currentModel.error = errorComponent
|
||||
return currentModel, cmd
|
||||
}
|
||||
|
||||
newModel := NewApply(root, currentModel)
|
||||
return newModel, newModel.Init()
|
||||
},
|
||||
},
|
||||
utils.SimpleItem[mainMenu]{
|
||||
|
@ -57,7 +65,8 @@ func NewMainMenu(root components.RootModel) tea.Model {
|
|||
Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) {
|
||||
if err := root.GetGlobal().Save(); err != nil {
|
||||
log.Error().Err(err).Msg(ErrorFailedAddMod)
|
||||
cmd := currentModel.list.NewStatusMessage(ErrorFailedAddMod)
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
currentModel.error = errorComponent
|
||||
return currentModel, cmd
|
||||
}
|
||||
return nil, nil
|
||||
|
@ -76,8 +85,6 @@ func NewMainMenu(root components.RootModel) tea.Model {
|
|||
model.list.SetFilteringEnabled(false)
|
||||
model.list.Title = "Main Menu"
|
||||
model.list.Styles = utils.ListStyles
|
||||
model.list.SetSize(model.list.Width(), model.list.Height())
|
||||
model.list.StatusMessageLifetime = time.Second * 3
|
||||
model.list.DisableQuitKeybindings()
|
||||
|
||||
return model
|
||||
|
@ -119,11 +126,20 @@ func (m mainMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
top, right, bottom, left := lipgloss.NewStyle().Margin(2, 2).GetMargin()
|
||||
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
|
||||
m.root.SetSize(msg)
|
||||
case components.ErrorComponentTimeoutMsg:
|
||||
m.error = nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m mainMenu) View() string {
|
||||
if m.error != nil {
|
||||
err := (*m.error).View()
|
||||
m.list.SetSize(m.list.Width(), m.root.Size().Height-m.root.Height()-lipgloss.Height(err))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), err, m.list.View())
|
||||
}
|
||||
|
||||
m.list.SetSize(m.list.Width(), m.root.Size().Height-m.root.Height())
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package scenes
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -85,6 +86,18 @@ func NewModMenu(root components.RootModel, parent tea.Model, mod utils.Mod) tea.
|
|||
model.list.StatusMessageLifetime = time.Second * 3
|
||||
model.list.DisableQuitKeybindings()
|
||||
|
||||
model.list.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
model.list.AdditionalFullHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
|
@ -30,9 +31,11 @@ type modInfo struct {
|
|||
spinner spinner.Model
|
||||
parent tea.Model
|
||||
modData chan ficsit.GetModGetMod
|
||||
modError chan string
|
||||
ready bool
|
||||
help help.Model
|
||||
keys modInfoKeyMap
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
type modInfoKeyMap struct {
|
||||
|
@ -65,6 +68,7 @@ func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea.
|
|||
spinner: spinner.New(),
|
||||
parent: parent,
|
||||
modData: make(chan ficsit.GetModGetMod),
|
||||
modError: make(chan string),
|
||||
ready: false,
|
||||
help: help.New(),
|
||||
keys: modInfoKeyMap{
|
||||
|
@ -86,11 +90,13 @@ func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea.
|
|||
fullMod, err := ficsit.GetMod(context.TODO(), root.GetAPIClient(), mod.ID)
|
||||
|
||||
if err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
model.modError <- err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
if fullMod == nil {
|
||||
panic("mod is nil") // TODO Handle Error
|
||||
model.modError <- "unknown error (mod is nil)"
|
||||
return
|
||||
}
|
||||
|
||||
model.modData <- fullMod.GetMod
|
||||
|
@ -204,6 +210,10 @@ func (m modInfo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
var cmd tea.Cmd
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
return m, cmd
|
||||
case err := <-m.modError:
|
||||
errorComponent, cmd := components.NewErrorComponent(err, time.Second*5)
|
||||
m.error = errorComponent
|
||||
return m, cmd
|
||||
default:
|
||||
return m, utils.Ticker()
|
||||
}
|
||||
|
@ -213,6 +223,11 @@ func (m modInfo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
|
||||
func (m modInfo) View() string {
|
||||
if m.error != nil {
|
||||
helpBar := lipgloss.NewStyle().Padding(1, 2).Render(m.help.View(m.keys))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), (*m.error).View(), m.viewport.View(), helpBar)
|
||||
}
|
||||
|
||||
if m.viewport.Height == 0 {
|
||||
spinnerView := lipgloss.NewStyle().Padding(0, 2, 1).Render(m.spinner.View() + " Loading...")
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), spinnerView)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package scenes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -16,6 +18,7 @@ type modSemver struct {
|
|||
input textinput.Model
|
||||
title string
|
||||
mod utils.Mod
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
func NewModSemver(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model {
|
||||
|
@ -49,7 +52,9 @@ func (m modSemver) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case KeyEnter:
|
||||
err := m.root.GetCurrentProfile().AddMod(m.mod.Reference, m.input.Value())
|
||||
if err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
m.error = errorComponent
|
||||
return m, cmd
|
||||
}
|
||||
return m.parent, nil
|
||||
default:
|
||||
|
@ -59,6 +64,8 @@ func (m modSemver) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.root.SetSize(msg)
|
||||
case components.ErrorComponentTimeoutMsg:
|
||||
m.error = nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
|
@ -66,5 +73,10 @@ func (m modSemver) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
|
||||
func (m modSemver) View() string {
|
||||
inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View())
|
||||
|
||||
if m.error != nil {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, (*m.error).View(), inputView)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package scenes
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -69,6 +70,18 @@ func NewModVersion(root components.RootModel, parent tea.Model, mod utils.Mod) t
|
|||
model.list.StatusMessageLifetime = time.Second * 3
|
||||
model.list.DisableQuitKeybindings()
|
||||
|
||||
model.list.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
model.list.AdditionalFullHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package scenes
|
|||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
|
||||
|
@ -40,6 +41,9 @@ type modsList struct {
|
|||
|
||||
showSortOrderList bool
|
||||
sortOrderList list.Model
|
||||
|
||||
err chan string
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
func NewMods(root components.RootModel, parent tea.Model) tea.Model {
|
||||
|
@ -57,6 +61,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
|
|||
|
||||
l.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
key.NewBinding(key.WithHelp("s", "sort")),
|
||||
key.NewBinding(key.WithHelp("o", "order")),
|
||||
}
|
||||
|
@ -64,6 +69,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
|
|||
|
||||
l.AdditionalFullHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
key.NewBinding(key.WithHelp("s", "sort")),
|
||||
key.NewBinding(key.WithHelp("o", "order")),
|
||||
}
|
||||
|
@ -145,6 +151,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
|
|||
sortingOrder: sortOrderDesc,
|
||||
sortFieldList: sortFieldList,
|
||||
sortOrderList: sortOrderList,
|
||||
err: make(chan string),
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
@ -160,7 +167,8 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
|
|||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
m.err <- err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
if len(mods.GetMods.Mods) == 0 {
|
||||
|
@ -273,6 +281,10 @@ func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.list.StopSpinner()
|
||||
cmd := m.list.SetItems(items)
|
||||
return m, cmd
|
||||
case err := <-m.err:
|
||||
errorComponent, cmd := components.NewErrorComponent(err, time.Second*5)
|
||||
m.error = errorComponent
|
||||
return m, cmd
|
||||
default:
|
||||
start := m.list.StartSpinner()
|
||||
return m, tea.Batch(utils.Ticker(), start)
|
||||
|
@ -295,16 +307,23 @@ func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
|
||||
func (m modsList) View() string {
|
||||
var bottom string
|
||||
var bottomList list.Model
|
||||
if m.showSortFieldList {
|
||||
bottom = m.sortFieldList.View()
|
||||
bottomList = m.sortFieldList
|
||||
} else if m.showSortOrderList {
|
||||
bottom = m.sortOrderList.View()
|
||||
bottomList = m.sortOrderList
|
||||
} else {
|
||||
bottom = m.list.View()
|
||||
bottomList = m.list
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), bottom)
|
||||
if m.error != nil {
|
||||
err := (*m.error).View()
|
||||
bottomList.SetSize(bottomList.Width(), m.root.Size().Height-m.root.Height()-lipgloss.Height(err))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), err, bottomList.View())
|
||||
}
|
||||
|
||||
bottomList.SetSize(bottomList.Width(), m.root.Size().Height-m.root.Height())
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), bottomList.View())
|
||||
}
|
||||
|
||||
func sortItems(items []list.Item, field string, direction sortOrder) []list.Item {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package scenes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -15,6 +17,7 @@ type newInstallation struct {
|
|||
parent tea.Model
|
||||
input textinput.Model
|
||||
title string
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
func NewNewInstallation(root components.RootModel, parent tea.Model) tea.Model {
|
||||
|
@ -30,6 +33,7 @@ func NewNewInstallation(root components.RootModel, parent tea.Model) tea.Model {
|
|||
|
||||
// TODO Tab-completion for input field
|
||||
// TODO Directory listing
|
||||
// TODO SSH/FTP/SFTP support
|
||||
|
||||
return model
|
||||
}
|
||||
|
@ -48,7 +52,9 @@ func (m newInstallation) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m.parent, nil
|
||||
case KeyEnter:
|
||||
if _, err := m.root.GetGlobal().Installations.AddInstallation(m.root.GetGlobal(), m.input.Value(), m.root.GetGlobal().Profiles.SelectedProfile); err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
m.error = errorComponent
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
return m.parent, updateInstallationListCmd
|
||||
|
@ -59,6 +65,8 @@ func (m newInstallation) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.root.SetSize(msg)
|
||||
case components.ErrorComponentTimeoutMsg:
|
||||
m.error = nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
|
@ -66,5 +74,10 @@ func (m newInstallation) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
|
||||
func (m newInstallation) View() string {
|
||||
inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View())
|
||||
|
||||
if m.error != nil {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, (*m.error).View(), inputView)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package scenes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -15,6 +17,7 @@ type newProfile struct {
|
|||
parent tea.Model
|
||||
input textinput.Model
|
||||
title string
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
func NewNewProfile(root components.RootModel, parent tea.Model) tea.Model {
|
||||
|
@ -45,7 +48,9 @@ func (m newProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m.parent, nil
|
||||
case KeyEnter:
|
||||
if _, err := m.root.GetGlobal().Profiles.AddProfile(m.input.Value()); err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
m.error = errorComponent
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
return m.parent, updateProfileListCmd
|
||||
|
@ -56,6 +61,8 @@ func (m newProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.root.SetSize(msg)
|
||||
case components.ErrorComponentTimeoutMsg:
|
||||
m.error = nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
|
@ -63,5 +70,10 @@ func (m newProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
|
||||
func (m newProfile) View() string {
|
||||
inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View())
|
||||
|
||||
if m.error != nil {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, (*m.error).View(), inputView)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
@ -20,6 +21,7 @@ type profile struct {
|
|||
parent tea.Model
|
||||
profile *cli.Profile
|
||||
hadRenamed bool
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
func NewProfile(root components.RootModel, parent tea.Model, profileData *cli.Profile) tea.Model {
|
||||
|
@ -34,7 +36,9 @@ func NewProfile(root components.RootModel, parent tea.Model, profileData *cli.Pr
|
|||
ItemTitle: "Select",
|
||||
Activate: func(msg tea.Msg, currentModel profile) (tea.Model, tea.Cmd) {
|
||||
if err := root.SetCurrentProfile(profileData); err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
currentModel.error = errorComponent
|
||||
return currentModel, cmd
|
||||
}
|
||||
|
||||
return currentModel.parent, nil
|
||||
|
@ -55,7 +59,9 @@ func NewProfile(root components.RootModel, parent tea.Model, profileData *cli.Pr
|
|||
ItemTitle: "Delete",
|
||||
Activate: func(msg tea.Msg, currentModel profile) (tea.Model, tea.Cmd) {
|
||||
if err := root.GetGlobal().Profiles.DeleteProfile(profileData.Name); err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
currentModel.error = errorComponent
|
||||
return currentModel, cmd
|
||||
}
|
||||
|
||||
return currentModel.parent, updateProfileListCmd
|
||||
|
@ -73,6 +79,18 @@ func NewProfile(root components.RootModel, parent tea.Model, profileData *cli.Pr
|
|||
model.list.StatusMessageLifetime = time.Second * 3
|
||||
model.list.DisableQuitKeybindings()
|
||||
|
||||
model.list.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
model.list.AdditionalFullHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
|
@ -124,11 +142,20 @@ func (m profile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case updateProfileNames:
|
||||
m.hadRenamed = true
|
||||
m.list.Title = fmt.Sprintf("Profile: %s", m.profile.Name)
|
||||
case components.ErrorComponentTimeoutMsg:
|
||||
m.error = nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m profile) View() string {
|
||||
if m.error != nil {
|
||||
err := (*m.error).View()
|
||||
m.list.SetSize(m.list.Width(), m.root.Size().Height-m.root.Height()-lipgloss.Height(err))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), err, m.list.View())
|
||||
}
|
||||
|
||||
m.list.SetSize(m.list.Width(), m.root.Size().Height-m.root.Height())
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ func NewProfiles(root components.RootModel, parent tea.Model) tea.Model {
|
|||
|
||||
l.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
key.NewBinding(key.WithHelp("n", "new profile")),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package scenes
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
@ -19,6 +20,7 @@ type renameProfile struct {
|
|||
input textinput.Model
|
||||
title string
|
||||
oldName string
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
func NewRenameProfile(root components.RootModel, parent tea.Model, profileData *cli.Profile) tea.Model {
|
||||
|
@ -51,7 +53,9 @@ func (m renameProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m.parent, nil
|
||||
case KeyEnter:
|
||||
if err := m.root.GetGlobal().Profiles.RenameProfile(m.oldName, m.input.Value()); err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
m.error = errorComponent
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
return m.parent, updateProfileNamesCmd
|
||||
|
@ -62,6 +66,8 @@ func (m renameProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.root.SetSize(msg)
|
||||
case components.ErrorComponentTimeoutMsg:
|
||||
m.error = nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
|
@ -69,5 +75,10 @@ func (m renameProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
|
||||
func (m renameProfile) View() string {
|
||||
inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View())
|
||||
|
||||
if m.error != nil {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, (*m.error).View(), inputView)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ package scenes
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
@ -20,6 +22,8 @@ type selectModVersionList struct {
|
|||
list list.Model
|
||||
parent tea.Model
|
||||
items chan []list.Item
|
||||
err chan string
|
||||
error *components.ErrorComponent
|
||||
}
|
||||
|
||||
func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model {
|
||||
|
@ -33,11 +37,24 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
|
|||
l.KeyMap.Quit.SetHelp("q", "back")
|
||||
l.DisableQuitKeybindings()
|
||||
|
||||
l.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
l.AdditionalFullHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(key.WithHelp("q", "back")),
|
||||
}
|
||||
}
|
||||
|
||||
m := &selectModVersionList{
|
||||
root: root,
|
||||
list: l,
|
||||
parent: parent,
|
||||
items: make(chan []list.Item),
|
||||
err: make(chan string),
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
@ -53,7 +70,8 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
|
|||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
m.err <- err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
if len(versions.Mod.Versions) == 0 {
|
||||
|
@ -71,7 +89,9 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
|
|||
version := allVersions[currentOffset+currentI]
|
||||
err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version)
|
||||
if err != nil {
|
||||
panic(err) // TODO Handle Error
|
||||
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
|
||||
currentModel.error = errorComponent
|
||||
return currentModel, cmd
|
||||
}
|
||||
return currentModel.parent, nil
|
||||
},
|
||||
|
@ -137,6 +157,10 @@ func (m selectModVersionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.list.StopSpinner()
|
||||
cmd := m.list.SetItems(items)
|
||||
return m, cmd
|
||||
case err := <-m.err:
|
||||
errorComponent, cmd := components.NewErrorComponent(err, time.Second*5)
|
||||
m.error = errorComponent
|
||||
return m, cmd
|
||||
default:
|
||||
start := m.list.StartSpinner()
|
||||
return m, tea.Batch(utils.Ticker(), start)
|
||||
|
@ -147,5 +171,12 @@ func (m selectModVersionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
|
||||
func (m selectModVersionList) View() string {
|
||||
if m.error != nil {
|
||||
err := (*m.error).View()
|
||||
m.list.SetSize(m.list.Width(), m.root.Size().Height-m.root.Height()-lipgloss.Height(err))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), err, m.list.View())
|
||||
}
|
||||
|
||||
m.list.SetSize(m.list.Width(), m.root.Size().Height-m.root.Height())
|
||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
|
||||
}
|
||||
|
|
80
utils/io.go
80
utils/io.go
|
@ -14,7 +14,34 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func DownloadOrCache(cacheKey string, hash string, url string) (r io.ReaderAt, size int64, err error) {
|
||||
type Progresser struct {
|
||||
io.Reader
|
||||
total int64
|
||||
running int64
|
||||
updates chan GenericUpdate
|
||||
}
|
||||
|
||||
func (pt *Progresser) Read(p []byte) (int, error) {
|
||||
n, err := pt.Reader.Read(p)
|
||||
pt.running += int64(n)
|
||||
|
||||
if err == nil {
|
||||
if pt.updates != nil {
|
||||
select {
|
||||
case pt.updates <- GenericUpdate{Progress: float64(pt.running) / float64(pt.total)}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return n, errors.Wrap(err, "failed to read")
|
||||
}
|
||||
|
||||
type GenericUpdate struct {
|
||||
Progress float64
|
||||
}
|
||||
|
||||
func DownloadOrCache(cacheKey string, hash string, url string, updates chan GenericUpdate) (r io.ReaderAt, size int64, err error) {
|
||||
downloadCache := path.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||
if err := os.MkdirAll(downloadCache, 0777); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
|
@ -74,7 +101,13 @@ func DownloadOrCache(cacheKey string, hash string, url string) (r io.ReaderAt, s
|
|||
return nil, 0, fmt.Errorf("bad status: %s on url: %s", resp.Status, url)
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
progresser := &Progresser{
|
||||
Reader: resp.Body,
|
||||
total: resp.ContentLength,
|
||||
updates: updates,
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, progresser)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed writing file to disk")
|
||||
}
|
||||
|
@ -84,6 +117,13 @@ func DownloadOrCache(cacheKey string, hash string, url string) (r io.ReaderAt, s
|
|||
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
|
||||
}
|
||||
|
||||
if updates != nil {
|
||||
select {
|
||||
case updates <- GenericUpdate{Progress: 1}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return f, resp.ContentLength, nil
|
||||
}
|
||||
|
||||
|
@ -96,7 +136,7 @@ func SHA256Data(f io.Reader) (string, error) {
|
|||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func ExtractMod(f io.ReaderAt, size int64, location string) error {
|
||||
func ExtractMod(f io.ReaderAt, size int64, location string, updates chan GenericUpdate) error {
|
||||
if err := os.MkdirAll(location, 0777); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return errors.Wrap(err, "failed to create mod directory: "+location)
|
||||
|
@ -116,7 +156,7 @@ func ExtractMod(f io.ReaderAt, size int64, location string) error {
|
|||
return errors.Wrap(err, "failed to read file as zip")
|
||||
}
|
||||
|
||||
for _, file := range reader.File {
|
||||
for i, file := range reader.File {
|
||||
if !file.FileInfo().IsDir() {
|
||||
outFileLocation := path.Join(location, file.Name)
|
||||
|
||||
|
@ -124,20 +164,44 @@ func ExtractMod(f io.ReaderAt, size int64, location string) error {
|
|||
return errors.Wrap(err, "failed to create mod directory: "+location)
|
||||
}
|
||||
|
||||
if err := writeZipFile(outFileLocation, file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if updates != nil {
|
||||
select {
|
||||
case updates <- GenericUpdate{Progress: float64(i) / float64(len(reader.File)-1)}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updates != nil {
|
||||
select {
|
||||
case updates <- GenericUpdate{Progress: 1}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeZipFile(outFileLocation string, file *zip.File) error {
|
||||
outFile, err := os.OpenFile(outFileLocation, os.O_CREATE|os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to write to file: "+location)
|
||||
return errors.Wrap(err, "failed to write to file: "+outFileLocation)
|
||||
}
|
||||
|
||||
defer outFile.Close()
|
||||
|
||||
inFile, err := file.Open()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to process mod zip")
|
||||
}
|
||||
|
||||
if _, err := io.Copy(outFile, inFile); err != nil {
|
||||
return errors.Wrap(err, "failed to write to file: "+location)
|
||||
}
|
||||
}
|
||||
return errors.Wrap(err, "failed to write to file: "+outFileLocation)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
Loading…
Reference in a new issue