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/Masterminds/semver/v3"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||||
"github.com/satisfactorymodding/ficsit-cli/utils"
|
|
||||||
"github.com/spf13/viper"
|
"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")
|
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{
|
instance := &resolvingInstance{
|
||||||
Resolver: d,
|
Resolver: d,
|
||||||
InputLock: lockFile,
|
InputLock: lockFile,
|
||||||
ToResolve: utils.CopyMap(constraints),
|
ToResolve: copied,
|
||||||
OutputLock: make(LockFile),
|
OutputLock: make(LockFile),
|
||||||
SMLVersions: smlVersionsDB,
|
SMLVersions: smlVersionsDB,
|
||||||
GameVersion: gameVersion,
|
GameVersion: gameVersion,
|
||||||
|
@ -50,7 +54,7 @@ type resolvingInstance struct {
|
||||||
|
|
||||||
InputLock *LockFile
|
InputLock *LockFile
|
||||||
|
|
||||||
ToResolve map[string]string
|
ToResolve map[string][]string
|
||||||
|
|
||||||
OutputLock LockFile
|
OutputLock LockFile
|
||||||
|
|
||||||
|
@ -69,11 +73,12 @@ func (r *resolvingInstance) Step() error {
|
||||||
if id != "SML" {
|
if id != "SML" {
|
||||||
converted = append(converted, ficsit.ModVersionConstraint{
|
converted = append(converted, ficsit.ModVersionConstraint{
|
||||||
ModIdOrReference: id,
|
ModIdOrReference: id,
|
||||||
Version: constraint,
|
Version: constraint[0],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
smlVersionConstraint, _ := semver.NewConstraint(constraint)
|
|
||||||
if existingSML, ok := r.OutputLock[id]; ok {
|
if existingSML, ok := r.OutputLock[id]; ok {
|
||||||
|
for _, cs := range constraint {
|
||||||
|
smlVersionConstraint, _ := semver.NewConstraint(cs)
|
||||||
if !smlVersionConstraint.Check(semver.MustParse(existingSML.Version)) {
|
if !smlVersionConstraint.Check(semver.MustParse(existingSML.Version)) {
|
||||||
return errors.Errorf("mod %s version %s does not match constraint %s",
|
return errors.Errorf("mod %s version %s does not match constraint %s",
|
||||||
id,
|
id,
|
||||||
|
@ -82,6 +87,7 @@ func (r *resolvingInstance) Step() error {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var chosenSMLVersion *semver.Version
|
var chosenSMLVersion *semver.Version
|
||||||
for _, version := range r.SMLVersions.SmlVersions.Sml_versions {
|
for _, version := range r.SMLVersions.SmlVersions.Sml_versions {
|
||||||
|
@ -90,7 +96,18 @@ func (r *resolvingInstance) Step() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
currentVersion := semver.MustParse(version.Version)
|
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) {
|
if chosenSMLVersion == nil || currentVersion.GreaterThan(chosenSMLVersion) {
|
||||||
chosenSMLVersion = currentVersion
|
chosenSMLVersion = currentVersion
|
||||||
}
|
}
|
||||||
|
@ -98,7 +115,7 @@ func (r *resolvingInstance) Step() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if chosenSMLVersion == nil {
|
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{
|
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
|
// TODO Cache
|
||||||
dependencies, err := ficsit.ResolveModDependencies(context.TODO(), r.Resolver.apiClient, converted)
|
dependencies, err := ficsit.ResolveModDependencies(context.TODO(), r.Resolver.apiClient, converted)
|
||||||
|
@ -147,7 +164,27 @@ func (r *resolvingInstance) Step() error {
|
||||||
|
|
||||||
// Pick latest version
|
// Pick latest version
|
||||||
// TODO Clone and branch
|
// 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 _, ok := r.OutputLock[mod.Mod_reference]; ok {
|
||||||
if r.OutputLock[mod.Mod_reference].Version != selectedVersion.Version {
|
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)
|
constraint, _ := semver.NewConstraint(dependency.Constraint)
|
||||||
resolvingConstraint, _ := semver.NewConstraint(resolving)
|
|
||||||
|
for _, cs := range resolving {
|
||||||
|
resolvingConstraint, _ := semver.NewConstraint(cs)
|
||||||
intersects, _ := constraint.Intersects(resolvingConstraint)
|
intersects, _ := constraint.Intersects(resolvingConstraint)
|
||||||
if !intersects {
|
if !intersects {
|
||||||
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
||||||
|
@ -193,15 +232,18 @@ func (r *resolvingInstance) Step() error {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if dependency.Optional {
|
if dependency.Optional {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ToResolve[dependency.ModReference] = dependency.Constraint
|
nextResolve[dependency.ModReference] = append(nextResolve[dependency.ModReference], dependency.Constraint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.ToResolve = nextResolve
|
||||||
|
|
||||||
for _, constraint := range converted {
|
for _, constraint := range converted {
|
||||||
if _, ok := r.OutputLock[constraint.ModIdOrReference]; !ok {
|
if _, ok := r.OutputLock[constraint.ModIdOrReference]; !ok {
|
||||||
return errors.New("failed resolving dependency: " + constraint.ModIdOrReference)
|
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 {
|
func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
||||||
added := false
|
added := false
|
||||||
if r.InputLock != nil {
|
if r.InputLock != nil {
|
||||||
for modReference, version := range r.ToResolve {
|
for modReference, constraints := range r.ToResolve {
|
||||||
if _, ok := viewed[modReference]; ok {
|
if _, ok := viewed[modReference]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -229,15 +271,25 @@ func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
||||||
viewed[modReference] = true
|
viewed[modReference] = true
|
||||||
|
|
||||||
if locked, ok := (*r.InputLock)[modReference]; ok {
|
if locked, ok := (*r.InputLock)[modReference]; ok {
|
||||||
constraint, _ := semver.NewConstraint(version)
|
passes := true
|
||||||
if constraint.Check(semver.MustParse(locked.Version)) {
|
|
||||||
|
for _, cs := range constraints {
|
||||||
|
constraint, _ := semver.NewConstraint(cs)
|
||||||
|
if !constraint.Check(semver.MustParse(locked.Version)) {
|
||||||
|
passes = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if passes {
|
||||||
delete(r.ToResolve, modReference)
|
delete(r.ToResolve, modReference)
|
||||||
r.OutputLock[modReference] = locked
|
r.OutputLock[modReference] = locked
|
||||||
for k, v := range locked.Dependencies {
|
for k, v := range locked.Dependencies {
|
||||||
if alreadyResolving, ok := r.ToResolve[k]; ok {
|
if alreadyResolving, ok := r.ToResolve[k]; ok {
|
||||||
cs1, _ := semver.NewConstraint(v)
|
newConstraint, _ := semver.NewConstraint(v)
|
||||||
cs2, _ := semver.NewConstraint(alreadyResolving)
|
for _, resolvingConstraint := range alreadyResolving {
|
||||||
intersects, _ := cs1.Intersects(cs2)
|
cs2, _ := semver.NewConstraint(resolvingConstraint)
|
||||||
|
intersects, _ := newConstraint.Intersects(cs2)
|
||||||
if !intersects {
|
if !intersects {
|
||||||
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
||||||
k,
|
k,
|
||||||
|
@ -245,6 +297,10 @@ func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
||||||
alreadyResolving,
|
alreadyResolving,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ToResolve[k] = append(r.ToResolve[k], v)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,7 +316,7 @@ func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ToResolve[k] = v
|
r.ToResolve[k] = append(r.ToResolve[k], v)
|
||||||
added = true
|
added = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/satisfactorymodding/ficsit-cli/utils"
|
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||||
|
|
||||||
|
@ -224,30 +226,84 @@ func (i *Installation) Validate(ctx *GlobalContext) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Installation) Install(ctx *GlobalContext) error {
|
var (
|
||||||
if err := i.Validate(ctx); err != nil {
|
lockFileCleaner = regexp.MustCompile(`[^a-zA-Z\d]]`)
|
||||||
return errors.Wrap(err, "failed to validate installation")
|
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)
|
platform, err := i.GetPlatform(ctx)
|
||||||
if err != nil {
|
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
|
var lockFile *LockFile
|
||||||
lockFileJSON, err := os.ReadFile(lockfilePath)
|
lockFileJSON, err := os.ReadFile(lockfilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return errors.Wrap(err, "failed reading lockfile")
|
return nil, errors.Wrap(err, "failed reading lockfile")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := json.Unmarshal(lockFileJSON, &lockFile); err != nil {
|
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)
|
resolver := NewDependencyResolver(ctx.APIClient)
|
||||||
|
|
||||||
gameVersion, err := i.GetGameVersion(ctx)
|
gameVersion, err := i.GetGameVersion(ctx)
|
||||||
|
@ -266,28 +322,79 @@ func (i *Installation) Install(ctx *GlobalContext) error {
|
||||||
return errors.Wrap(err, "failed creating Mods directory")
|
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 {
|
for modReference, version := range lockfile {
|
||||||
// Only install if a link is provided, otherwise assume mod is already installed
|
// Only install if a link is provided, otherwise assume mod is already installed
|
||||||
if version.Link != "" {
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to download "+modReference+" from: "+version.Link)
|
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)
|
return errors.Wrap(err, "could not extract "+modReference)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
close(genericUpdates)
|
||||||
}
|
}
|
||||||
|
|
||||||
marshaledLockfile, err := json.MarshalIndent(lockfile, "", " ")
|
completed++
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to serialize lockfile json")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(lockfilePath, marshaledLockfile, 0777); err != nil {
|
if err := i.WriteLockFile(ctx, lockfile); err != nil {
|
||||||
return errors.Wrap(err, "failed writing lockfile")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -34,7 +34,7 @@ func TestAddInstallation(t *testing.T) {
|
||||||
testza.AssertNoError(t, err)
|
testza.AssertNoError(t, err)
|
||||||
testza.AssertNotNil(t, installation)
|
testza.AssertNotNil(t, installation)
|
||||||
|
|
||||||
err = installation.Install(ctx)
|
err = installation.Install(ctx, nil)
|
||||||
testza.AssertNoError(t, err)
|
testza.AssertNoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,14 +10,14 @@ type Platform struct {
|
||||||
var platforms = []Platform{
|
var platforms = []Platform{
|
||||||
{
|
{
|
||||||
VersionPath: path.Join("Engine", "Binaries", "Linux", "UE4Server-Linux-Shipping.version"),
|
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"),
|
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"),
|
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 {
|
switch runtime.GOOS {
|
||||||
case "windows":
|
case "windows":
|
||||||
baseLocalDir = os.Getenv("APPDATA")
|
baseLocalDir = os.Getenv("APPDATA")
|
||||||
break
|
|
||||||
case "linux":
|
case "linux":
|
||||||
baseLocalDir = path.Join(os.Getenv("HOME"), ".local", "share")
|
baseLocalDir = path.Join(os.Getenv("HOME"), ".local", "share")
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
panic("unsupported platform: " + runtime.GOOS)
|
panic("unsupported platform: " + runtime.GOOS)
|
||||||
}
|
}
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -5,7 +5,7 @@ go 1.18
|
||||||
require (
|
require (
|
||||||
github.com/JohannesKaufmann/html-to-markdown v1.3.3
|
github.com/JohannesKaufmann/html-to-markdown v1.3.3
|
||||||
github.com/Khan/genqlient v0.4.0
|
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/Masterminds/semver/v3 v3.1.1
|
||||||
github.com/PuerkitoBio/goquery v1.8.0
|
github.com/PuerkitoBio/goquery v1.8.0
|
||||||
github.com/charmbracelet/bubbles v0.10.3
|
github.com/charmbracelet/bubbles v0.10.3
|
||||||
|
@ -30,6 +30,7 @@ require (
|
||||||
github.com/atomicgo/cursor v0.0.1 // indirect
|
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // 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/containerd/console v1.0.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.4.0 // 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.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.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
|
||||||
github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY=
|
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.2 h1:Vbw9GkSB5erJI2BPnBL9SVGV9myE+XmUSFahBGUhW2Q=
|
||||||
github.com/MarvinJWendt/testza v0.4.1/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
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.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
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/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 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g=
|
||||||
github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
|
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/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.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
||||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
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"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
@ -20,6 +21,7 @@ type installation struct {
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
installation *cli.Installation
|
installation *cli.Installation
|
||||||
hadRenamed bool
|
hadRenamed bool
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInstallation(root components.RootModel, parent tea.Model, installationData *cli.Installation) tea.Model {
|
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",
|
ItemTitle: "Select",
|
||||||
Activate: func(msg tea.Msg, currentModel installation) (tea.Model, tea.Cmd) {
|
Activate: func(msg tea.Msg, currentModel installation) (tea.Model, tea.Cmd) {
|
||||||
if err := root.SetCurrentInstallation(installationData); err != nil {
|
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
|
return currentModel.parent, nil
|
||||||
|
@ -44,7 +48,9 @@ func NewInstallation(root components.RootModel, parent tea.Model, installationDa
|
||||||
ItemTitle: "Delete",
|
ItemTitle: "Delete",
|
||||||
Activate: func(msg tea.Msg, currentModel installation) (tea.Model, tea.Cmd) {
|
Activate: func(msg tea.Msg, currentModel installation) (tea.Model, tea.Cmd) {
|
||||||
if err := root.GetGlobal().Installations.DeleteInstallation(installationData.Path); err != nil {
|
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
|
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.StatusMessageLifetime = time.Second * 3
|
||||||
model.list.DisableQuitKeybindings()
|
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
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,11 +130,20 @@ func (m installation) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case updateInstallationNames:
|
case updateInstallationNames:
|
||||||
m.hadRenamed = true
|
m.hadRenamed = true
|
||||||
m.list.Title = fmt.Sprintf("Installation: %s", m.installation.Path)
|
m.list.Title = fmt.Sprintf("Installation: %s", m.installation.Path)
|
||||||
|
case components.ErrorComponentTimeoutMsg:
|
||||||
|
m.error = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m installation) View() string {
|
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())
|
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 {
|
l.AdditionalShortHelpKeys = func() []key.Binding {
|
||||||
return []key.Binding{
|
return []key.Binding{
|
||||||
|
key.NewBinding(key.WithHelp("q", "back")),
|
||||||
key.NewBinding(key.WithHelp("n", "new installation")),
|
key.NewBinding(key.WithHelp("n", "new installation")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ var _ tea.Model = (*mainMenu)(nil)
|
||||||
type mainMenu struct {
|
type mainMenu struct {
|
||||||
root components.RootModel
|
root components.RootModel
|
||||||
list list.Model
|
list list.Model
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMainMenu(root components.RootModel) tea.Model {
|
func NewMainMenu(root components.RootModel) tea.Model {
|
||||||
|
@ -48,8 +49,15 @@ func NewMainMenu(root components.RootModel) tea.Model {
|
||||||
utils.SimpleItem[mainMenu]{
|
utils.SimpleItem[mainMenu]{
|
||||||
ItemTitle: "Apply Changes",
|
ItemTitle: "Apply Changes",
|
||||||
Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) {
|
Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) {
|
||||||
// TODO Apply changes to all changed profiles
|
if err := root.GetGlobal().Save(); err != nil {
|
||||||
return nil, 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]{
|
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) {
|
Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) {
|
||||||
if err := root.GetGlobal().Save(); err != nil {
|
if err := root.GetGlobal().Save(); err != nil {
|
||||||
log.Error().Err(err).Msg(ErrorFailedAddMod)
|
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 currentModel, cmd
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -76,8 +85,6 @@ func NewMainMenu(root components.RootModel) tea.Model {
|
||||||
model.list.SetFilteringEnabled(false)
|
model.list.SetFilteringEnabled(false)
|
||||||
model.list.Title = "Main Menu"
|
model.list.Title = "Main Menu"
|
||||||
model.list.Styles = utils.ListStyles
|
model.list.Styles = utils.ListStyles
|
||||||
model.list.SetSize(model.list.Width(), model.list.Height())
|
|
||||||
model.list.StatusMessageLifetime = time.Second * 3
|
|
||||||
model.list.DisableQuitKeybindings()
|
model.list.DisableQuitKeybindings()
|
||||||
|
|
||||||
return model
|
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()
|
top, right, bottom, left := lipgloss.NewStyle().Margin(2, 2).GetMargin()
|
||||||
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
|
m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
|
||||||
m.root.SetSize(msg)
|
m.root.SetSize(msg)
|
||||||
|
case components.ErrorComponentTimeoutMsg:
|
||||||
|
m.error = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m mainMenu) View() string {
|
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())
|
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package scenes
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"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.StatusMessageLifetime = time.Second * 3
|
||||||
model.list.DisableQuitKeybindings()
|
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
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
@ -30,9 +31,11 @@ type modInfo struct {
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
modData chan ficsit.GetModGetMod
|
modData chan ficsit.GetModGetMod
|
||||||
|
modError chan string
|
||||||
ready bool
|
ready bool
|
||||||
help help.Model
|
help help.Model
|
||||||
keys modInfoKeyMap
|
keys modInfoKeyMap
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
type modInfoKeyMap struct {
|
type modInfoKeyMap struct {
|
||||||
|
@ -65,6 +68,7 @@ func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea.
|
||||||
spinner: spinner.New(),
|
spinner: spinner.New(),
|
||||||
parent: parent,
|
parent: parent,
|
||||||
modData: make(chan ficsit.GetModGetMod),
|
modData: make(chan ficsit.GetModGetMod),
|
||||||
|
modError: make(chan string),
|
||||||
ready: false,
|
ready: false,
|
||||||
help: help.New(),
|
help: help.New(),
|
||||||
keys: modInfoKeyMap{
|
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)
|
fullMod, err := ficsit.GetMod(context.TODO(), root.GetAPIClient(), mod.ID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // TODO Handle Error
|
model.modError <- err.Error()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if fullMod == nil {
|
if fullMod == nil {
|
||||||
panic("mod is nil") // TODO Handle Error
|
model.modError <- "unknown error (mod is nil)"
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
model.modData <- fullMod.GetMod
|
model.modData <- fullMod.GetMod
|
||||||
|
@ -204,6 +210,10 @@ func (m modInfo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
m.viewport, cmd = m.viewport.Update(msg)
|
m.viewport, cmd = m.viewport.Update(msg)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
case err := <-m.modError:
|
||||||
|
errorComponent, cmd := components.NewErrorComponent(err, time.Second*5)
|
||||||
|
m.error = errorComponent
|
||||||
|
return m, cmd
|
||||||
default:
|
default:
|
||||||
return m, utils.Ticker()
|
return m, utils.Ticker()
|
||||||
}
|
}
|
||||||
|
@ -213,6 +223,11 @@ func (m modInfo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m modInfo) View() string {
|
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 {
|
if m.viewport.Height == 0 {
|
||||||
spinnerView := lipgloss.NewStyle().Padding(0, 2, 1).Render(m.spinner.View() + " Loading...")
|
spinnerView := lipgloss.NewStyle().Padding(0, 2, 1).Render(m.spinner.View() + " Loading...")
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), spinnerView)
|
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), spinnerView)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package scenes
|
package scenes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
@ -16,6 +18,7 @@ type modSemver struct {
|
||||||
input textinput.Model
|
input textinput.Model
|
||||||
title string
|
title string
|
||||||
mod utils.Mod
|
mod utils.Mod
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModSemver(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model {
|
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:
|
case KeyEnter:
|
||||||
err := m.root.GetCurrentProfile().AddMod(m.mod.Reference, m.input.Value())
|
err := m.root.GetCurrentProfile().AddMod(m.mod.Reference, m.input.Value())
|
||||||
if err != nil {
|
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
|
return m.parent, nil
|
||||||
default:
|
default:
|
||||||
|
@ -59,6 +64,8 @@ func (m modSemver) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.root.SetSize(msg)
|
m.root.SetSize(msg)
|
||||||
|
case components.ErrorComponentTimeoutMsg:
|
||||||
|
m.error = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
|
@ -66,5 +73,10 @@ func (m modSemver) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
func (m modSemver) View() string {
|
func (m modSemver) View() string {
|
||||||
inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View())
|
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)
|
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package scenes
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"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.StatusMessageLifetime = time.Second * 3
|
||||||
model.list.DisableQuitKeybindings()
|
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
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package scenes
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
|
||||||
|
@ -40,6 +41,9 @@ type modsList struct {
|
||||||
|
|
||||||
showSortOrderList bool
|
showSortOrderList bool
|
||||||
sortOrderList list.Model
|
sortOrderList list.Model
|
||||||
|
|
||||||
|
err chan string
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMods(root components.RootModel, parent tea.Model) tea.Model {
|
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 {
|
l.AdditionalShortHelpKeys = func() []key.Binding {
|
||||||
return []key.Binding{
|
return []key.Binding{
|
||||||
|
key.NewBinding(key.WithHelp("q", "back")),
|
||||||
key.NewBinding(key.WithHelp("s", "sort")),
|
key.NewBinding(key.WithHelp("s", "sort")),
|
||||||
key.NewBinding(key.WithHelp("o", "order")),
|
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 {
|
l.AdditionalFullHelpKeys = func() []key.Binding {
|
||||||
return []key.Binding{
|
return []key.Binding{
|
||||||
|
key.NewBinding(key.WithHelp("q", "back")),
|
||||||
key.NewBinding(key.WithHelp("s", "sort")),
|
key.NewBinding(key.WithHelp("s", "sort")),
|
||||||
key.NewBinding(key.WithHelp("o", "order")),
|
key.NewBinding(key.WithHelp("o", "order")),
|
||||||
}
|
}
|
||||||
|
@ -145,6 +151,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
|
||||||
sortingOrder: sortOrderDesc,
|
sortingOrder: sortOrderDesc,
|
||||||
sortFieldList: sortFieldList,
|
sortFieldList: sortFieldList,
|
||||||
sortOrderList: sortOrderList,
|
sortOrderList: sortOrderList,
|
||||||
|
err: make(chan string),
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -160,7 +167,8 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // TODO Handle Error
|
m.err <- err.Error()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(mods.GetMods.Mods) == 0 {
|
if len(mods.GetMods.Mods) == 0 {
|
||||||
|
@ -273,6 +281,10 @@ func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.list.StopSpinner()
|
m.list.StopSpinner()
|
||||||
cmd := m.list.SetItems(items)
|
cmd := m.list.SetItems(items)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
case err := <-m.err:
|
||||||
|
errorComponent, cmd := components.NewErrorComponent(err, time.Second*5)
|
||||||
|
m.error = errorComponent
|
||||||
|
return m, cmd
|
||||||
default:
|
default:
|
||||||
start := m.list.StartSpinner()
|
start := m.list.StartSpinner()
|
||||||
return m, tea.Batch(utils.Ticker(), start)
|
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 {
|
func (m modsList) View() string {
|
||||||
var bottom string
|
var bottomList list.Model
|
||||||
if m.showSortFieldList {
|
if m.showSortFieldList {
|
||||||
bottom = m.sortFieldList.View()
|
bottomList = m.sortFieldList
|
||||||
} else if m.showSortOrderList {
|
} else if m.showSortOrderList {
|
||||||
bottom = m.sortOrderList.View()
|
bottomList = m.sortOrderList
|
||||||
} else {
|
} 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 {
|
func sortItems(items []list.Item, field string, direction sortOrder) []list.Item {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package scenes
|
package scenes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
@ -15,6 +17,7 @@ type newInstallation struct {
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
input textinput.Model
|
input textinput.Model
|
||||||
title string
|
title string
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNewInstallation(root components.RootModel, parent tea.Model) tea.Model {
|
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 Tab-completion for input field
|
||||||
// TODO Directory listing
|
// TODO Directory listing
|
||||||
|
// TODO SSH/FTP/SFTP support
|
||||||
|
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
@ -48,7 +52,9 @@ func (m newInstallation) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m.parent, nil
|
return m.parent, nil
|
||||||
case KeyEnter:
|
case KeyEnter:
|
||||||
if _, err := m.root.GetGlobal().Installations.AddInstallation(m.root.GetGlobal(), m.input.Value(), m.root.GetGlobal().Profiles.SelectedProfile); err != nil {
|
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
|
return m.parent, updateInstallationListCmd
|
||||||
|
@ -59,6 +65,8 @@ func (m newInstallation) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.root.SetSize(msg)
|
m.root.SetSize(msg)
|
||||||
|
case components.ErrorComponentTimeoutMsg:
|
||||||
|
m.error = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
|
@ -66,5 +74,10 @@ func (m newInstallation) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
func (m newInstallation) View() string {
|
func (m newInstallation) View() string {
|
||||||
inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View())
|
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)
|
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package scenes
|
package scenes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
@ -15,6 +17,7 @@ type newProfile struct {
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
input textinput.Model
|
input textinput.Model
|
||||||
title string
|
title string
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNewProfile(root components.RootModel, parent tea.Model) tea.Model {
|
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
|
return m.parent, nil
|
||||||
case KeyEnter:
|
case KeyEnter:
|
||||||
if _, err := m.root.GetGlobal().Profiles.AddProfile(m.input.Value()); err != nil {
|
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
|
return m.parent, updateProfileListCmd
|
||||||
|
@ -56,6 +61,8 @@ func (m newProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.root.SetSize(msg)
|
m.root.SetSize(msg)
|
||||||
|
case components.ErrorComponentTimeoutMsg:
|
||||||
|
m.error = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
|
@ -63,5 +70,10 @@ func (m newProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
func (m newProfile) View() string {
|
func (m newProfile) View() string {
|
||||||
inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View())
|
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)
|
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
@ -20,6 +21,7 @@ type profile struct {
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
profile *cli.Profile
|
profile *cli.Profile
|
||||||
hadRenamed bool
|
hadRenamed bool
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProfile(root components.RootModel, parent tea.Model, profileData *cli.Profile) tea.Model {
|
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",
|
ItemTitle: "Select",
|
||||||
Activate: func(msg tea.Msg, currentModel profile) (tea.Model, tea.Cmd) {
|
Activate: func(msg tea.Msg, currentModel profile) (tea.Model, tea.Cmd) {
|
||||||
if err := root.SetCurrentProfile(profileData); err != nil {
|
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
|
return currentModel.parent, nil
|
||||||
|
@ -55,7 +59,9 @@ func NewProfile(root components.RootModel, parent tea.Model, profileData *cli.Pr
|
||||||
ItemTitle: "Delete",
|
ItemTitle: "Delete",
|
||||||
Activate: func(msg tea.Msg, currentModel profile) (tea.Model, tea.Cmd) {
|
Activate: func(msg tea.Msg, currentModel profile) (tea.Model, tea.Cmd) {
|
||||||
if err := root.GetGlobal().Profiles.DeleteProfile(profileData.Name); err != nil {
|
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
|
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.StatusMessageLifetime = time.Second * 3
|
||||||
model.list.DisableQuitKeybindings()
|
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
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,11 +142,20 @@ func (m profile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case updateProfileNames:
|
case updateProfileNames:
|
||||||
m.hadRenamed = true
|
m.hadRenamed = true
|
||||||
m.list.Title = fmt.Sprintf("Profile: %s", m.profile.Name)
|
m.list.Title = fmt.Sprintf("Profile: %s", m.profile.Name)
|
||||||
|
case components.ErrorComponentTimeoutMsg:
|
||||||
|
m.error = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m profile) View() string {
|
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())
|
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 {
|
l.AdditionalShortHelpKeys = func() []key.Binding {
|
||||||
return []key.Binding{
|
return []key.Binding{
|
||||||
|
key.NewBinding(key.WithHelp("q", "back")),
|
||||||
key.NewBinding(key.WithHelp("n", "new profile")),
|
key.NewBinding(key.WithHelp("n", "new profile")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package scenes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
@ -19,6 +20,7 @@ type renameProfile struct {
|
||||||
input textinput.Model
|
input textinput.Model
|
||||||
title string
|
title string
|
||||||
oldName string
|
oldName string
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRenameProfile(root components.RootModel, parent tea.Model, profileData *cli.Profile) tea.Model {
|
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
|
return m.parent, nil
|
||||||
case KeyEnter:
|
case KeyEnter:
|
||||||
if err := m.root.GetGlobal().Profiles.RenameProfile(m.oldName, m.input.Value()); err != nil {
|
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
|
return m.parent, updateProfileNamesCmd
|
||||||
|
@ -62,6 +66,8 @@ func (m renameProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.root.SetSize(msg)
|
m.root.SetSize(msg)
|
||||||
|
case components.ErrorComponentTimeoutMsg:
|
||||||
|
m.error = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
|
@ -69,5 +75,10 @@ func (m renameProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
func (m renameProfile) View() string {
|
func (m renameProfile) View() string {
|
||||||
inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View())
|
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)
|
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ package scenes
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
@ -20,6 +22,8 @@ type selectModVersionList struct {
|
||||||
list list.Model
|
list list.Model
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
items chan []list.Item
|
items chan []list.Item
|
||||||
|
err chan string
|
||||||
|
error *components.ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model {
|
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.KeyMap.Quit.SetHelp("q", "back")
|
||||||
l.DisableQuitKeybindings()
|
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{
|
m := &selectModVersionList{
|
||||||
root: root,
|
root: root,
|
||||||
list: l,
|
list: l,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
items: make(chan []list.Item),
|
items: make(chan []list.Item),
|
||||||
|
err: make(chan string),
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -53,7 +70,8 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // TODO Handle Error
|
m.err <- err.Error()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(versions.Mod.Versions) == 0 {
|
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]
|
version := allVersions[currentOffset+currentI]
|
||||||
err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version)
|
err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version)
|
||||||
if err != nil {
|
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
|
return currentModel.parent, nil
|
||||||
},
|
},
|
||||||
|
@ -137,6 +157,10 @@ func (m selectModVersionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.list.StopSpinner()
|
m.list.StopSpinner()
|
||||||
cmd := m.list.SetItems(items)
|
cmd := m.list.SetItems(items)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
case err := <-m.err:
|
||||||
|
errorComponent, cmd := components.NewErrorComponent(err, time.Second*5)
|
||||||
|
m.error = errorComponent
|
||||||
|
return m, cmd
|
||||||
default:
|
default:
|
||||||
start := m.list.StartSpinner()
|
start := m.list.StartSpinner()
|
||||||
return m, tea.Batch(utils.Ticker(), start)
|
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 {
|
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())
|
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"
|
"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")
|
downloadCache := path.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||||
if err := os.MkdirAll(downloadCache, 0777); err != nil {
|
if err := os.MkdirAll(downloadCache, 0777); err != nil {
|
||||||
if !os.IsExist(err) {
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, 0, errors.Wrap(err, "failed writing file to disk")
|
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)
|
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
|
return f, resp.ContentLength, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +136,7 @@ func SHA256Data(f io.Reader) (string, error) {
|
||||||
return hex.EncodeToString(h.Sum(nil)), nil
|
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 err := os.MkdirAll(location, 0777); err != nil {
|
||||||
if !os.IsExist(err) {
|
if !os.IsExist(err) {
|
||||||
return errors.Wrap(err, "failed to create mod directory: "+location)
|
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")
|
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() {
|
if !file.FileInfo().IsDir() {
|
||||||
outFileLocation := path.Join(location, file.Name)
|
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)
|
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)
|
outFile, err := os.OpenFile(outFileLocation, os.O_CREATE|os.O_RDWR, 0644)
|
||||||
if err != nil {
|
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()
|
inFile, err := file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to process mod zip")
|
return errors.Wrap(err, "failed to process mod zip")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := io.Copy(outFile, inFile); err != nil {
|
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
|
return nil
|
||||||
|
|
Loading…
Reference in a new issue