From e329e48e9b6b7523de68adbb14ac0ea45041bd5b Mon Sep 17 00:00:00 2001 From: Vilsol Date: Thu, 2 Dec 2021 06:00:33 +0200 Subject: [PATCH] Tooling, Mod Browser, Cleanup, CI --- .github/workflows/build.yaml | 27 ++++ .gitignore | 8 +- .golangci.yml | 40 ++++++ .goreleaser.yml | 47 +++++++ cfg/test_defaults.go | 17 +++ cli/context.go | 47 +++++++ cli/installations.go | 173 +++++++++++++++++++++++- cli/installations_test.go | 35 +++++ cli/profiles.go | 198 ++++++++++++++++++++++++++- cli/profiles_test.go | 18 +++ cmd/cli.go | 24 ++++ cmd/download.go | 84 ------------ cmd/root.go | 69 ++++++++-- ficsit/.gitignore | 1 + ficsit/api_test.go | 36 +++++ ficsit/queries/mod.graphql | 18 +++ ficsit/queries/mod_versions.graphql | 13 ++ ficsit/queries/mods.graphql | 11 ++ ficsit/root.go | 12 ++ genqlient.yaml | 23 ++++ go.mod | 23 +++- go.sum | 79 +++++++++++ tea/components/header.go | 48 +++++++ tea/components/types.go | 25 ++++ tea/keys.go | 55 -------- tea/root.go | 85 +++++++----- tea/scenes/exit_menu.go | 90 +++++++++++++ tea/scenes/installations.go | 7 +- tea/scenes/keys.go | 6 + tea/scenes/main_menu.go | 154 +++++++++++---------- tea/scenes/mod.go | 135 +++++++++++++++++++ tea/scenes/mod_info.go | 202 ++++++++++++++++++++++++++++ tea/scenes/mod_semver.go | 74 ++++++++++ tea/scenes/mod_version.go | 119 ++++++++++++++++ tea/scenes/mods.go | 154 ++++++++++++++++++++- tea/scenes/profiles.go | 7 +- tea/scenes/select_mod_version.go | 155 +++++++++++++++++++++ tea/scenes/types.go | 16 --- tea/utils/basic_list.go | 51 +++++++ tea/utils/styles.go | 19 +++ tea/utils/tick.go | 15 +++ tea/utils/types.go | 7 + tools.go | 8 ++ utils/version.go | 5 + 44 files changed, 2155 insertions(+), 285 deletions(-) create mode 100644 .github/workflows/build.yaml create mode 100644 .golangci.yml create mode 100644 .goreleaser.yml create mode 100644 cfg/test_defaults.go create mode 100644 cli/context.go create mode 100644 cli/installations_test.go create mode 100644 cli/profiles_test.go create mode 100644 cmd/cli.go delete mode 100644 cmd/download.go create mode 100644 ficsit/.gitignore create mode 100644 ficsit/api_test.go create mode 100644 ficsit/queries/mod.graphql create mode 100644 ficsit/queries/mod_versions.graphql create mode 100644 ficsit/queries/mods.graphql create mode 100644 ficsit/root.go create mode 100644 genqlient.yaml create mode 100644 tea/components/header.go create mode 100644 tea/components/types.go delete mode 100644 tea/keys.go create mode 100644 tea/scenes/exit_menu.go create mode 100644 tea/scenes/keys.go create mode 100644 tea/scenes/mod.go create mode 100644 tea/scenes/mod_info.go create mode 100644 tea/scenes/mod_semver.go create mode 100644 tea/scenes/mod_version.go create mode 100644 tea/scenes/select_mod_version.go delete mode 100644 tea/scenes/types.go create mode 100644 tea/utils/basic_list.go create mode 100644 tea/utils/styles.go create mode 100644 tea/utils/tick.go create mode 100644 tea/utils/types.go create mode 100644 tools.go create mode 100644 utils/version.go diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..f6ad3e6 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,27 @@ +name: build + +on: [push, pull_request] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Go Generate + run: go generate -tags tools -x ./... + + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + + - name: Build + run: go build -v -o ficsit-cli . + env: + CGO_ENABLED: 1 diff --git a/.gitignore b/.gitignore index 1efc4fc..dd0e828 100644 --- a/.gitignore +++ b/.gitignore @@ -108,4 +108,10 @@ modules.xml # Sonarlint plugin .idea/sonarlint -# End of https://www.gitignore.io/api/go,jetbrains+all \ No newline at end of file +# End of https://www.gitignore.io/api/go,jetbrains+all + +dist/ +/testdata +/.graphqlconfig +schema.graphql +*.log \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..774e436 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,40 @@ +linters-settings: + wrapcheck: + ignoreSigs: + - .Errorf( + - errors.New( + - errors.Unwrap( + - .Wrap( + - .Wrapf( + - .WithMessage( + - .WithMessagef( + - .WithStack( + + ignorePackageGlobs: + - github.com/satisfactorymodding/ficsit-cli/* + +linters: + disable-all: true + enable: + - deadcode + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - unused + - varcheck + - bidichk + - contextcheck + - durationcheck + - errorlint + - goconst + - goimports + - revive + - ifshort + - misspell + - prealloc + - whitespace + - wrapcheck diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..71ccb4c --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,47 @@ +project_name: ficsit + +before: + hooks: + - go generate -x -tags tools ./... + - go mod tidy + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm + - arm64 + - ppc64le + goarm: + - 7 + +universal_binaries: + - replace: true + +archives: + - format: binary + allow_different_binary_count: true + +nfpms: + - formats: + - apk + - deb + - rpm + +checksum: + name_template: 'checksums.txt' + +snapshot: + name_template: "{{ .Tag }}-next" + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/cfg/test_defaults.go b/cfg/test_defaults.go new file mode 100644 index 0000000..e30aad9 --- /dev/null +++ b/cfg/test_defaults.go @@ -0,0 +1,17 @@ +package cfg + +import ( + "path/filepath" + "runtime" + + "github.com/spf13/viper" +) + +func SetDefaults() { + _, file, _, _ := runtime.Caller(0) + viper.SetDefault("cache-dir", filepath.Clean(filepath.Join(filepath.Dir(file), "../", "testdata", "cache"))) + viper.SetDefault("profiles-file", "profiles.json") + viper.SetDefault("installations-file", "installations.json") + viper.SetDefault("dry-run", false) + viper.SetDefault("api", "https://api.ficsit.app/v2/query") +} diff --git a/cli/context.go b/cli/context.go new file mode 100644 index 0000000..d334b34 --- /dev/null +++ b/cli/context.go @@ -0,0 +1,47 @@ +package cli + +import "github.com/pkg/errors" + +type GlobalContext struct { + Installations *Installations + Profiles *Profiles +} + +var globalContext *GlobalContext + +func InitCLI() (*GlobalContext, error) { + if globalContext != nil { + return globalContext, nil + } + + profiles, err := InitProfiles() + if err != nil { + return nil, errors.Wrap(err, "failed to initialize profiles") + } + + installations, err := InitInstallations() + if err != nil { + return nil, errors.Wrap(err, "failed to initialize installations") + } + + ctx := &GlobalContext{ + Installations: installations, + Profiles: profiles, + } + + globalContext = ctx + + return ctx, nil +} + +func (g *GlobalContext) Save() error { + if err := g.Installations.Save(); err != nil { + return err + } + + if err := g.Profiles.Save(); err != nil { + return err + } + + return nil +} diff --git a/cli/installations.go b/cli/installations.go index d88d740..a6a62de 100644 --- a/cli/installations.go +++ b/cli/installations.go @@ -1,4 +1,175 @@ package cli -type Installation struct { +import ( + "encoding/json" + "fmt" + "os" + "path" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +type InstallationsVersion int + +const ( + InitialInstallationsVersion = InstallationsVersion(iota) + + // Always last + nextInstallationsVersion +) + +type Installations struct { + Version InstallationsVersion `json:"version"` + Installations []*Installation `json:"installations"` + SelectedInstallation string `json:"selected_installation"` +} + +type Installation struct { + Path string `json:"path"` + Profile string `json:"profile"` +} + +func InitInstallations() (*Installations, error) { + cacheDir := viper.GetString("cache-dir") + + installationsFile := path.Join(cacheDir, viper.GetString("installations-file")) + _, err := os.Stat(installationsFile) + if err != nil { + if !os.IsNotExist(err) { + return nil, errors.Wrap(err, "failed to stat installations file") + } + + _, err := os.Stat(cacheDir) + if err != nil { + if !os.IsNotExist(err) { + return nil, errors.Wrap(err, "failed to read cache directory") + } + + err = os.MkdirAll(cacheDir, 0755) + if err != nil { + return nil, errors.Wrap(err, "failed to create cache directory") + } + } + + emptyInstallations := Installations{ + Version: nextInstallationsVersion - 1, + } + + if err := emptyInstallations.Save(); err != nil { + return nil, errors.Wrap(err, "failed to save empty installations") + } + } + + installationsData, err := os.ReadFile(installationsFile) + if err != nil { + return nil, errors.Wrap(err, "failed to read installations") + } + + var installations Installations + if err := json.Unmarshal(installationsData, &installations); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal installations") + } + + if installations.Version >= nextInstallationsVersion { + return nil, fmt.Errorf("unknown installations version: %d", installations.Version) + } + + return &installations, nil +} + +func (i *Installations) Save() error { + if viper.GetBool("dry-run") { + log.Info().Msg("dry-run: skipping installation saving") + return nil + } + + installationsFile := path.Join(viper.GetString("cache-dir"), viper.GetString("installations-file")) + + log.Info().Str("path", installationsFile).Msg("saving installations") + + installationsJSON, err := json.MarshalIndent(i, "", " ") + if err != nil { + return errors.Wrap(err, "failed to marshal installations") + } + + if err := os.WriteFile(installationsFile, installationsJSON, 0755); err != nil { + return errors.Wrap(err, "failed to write installations") + } + + return nil +} + +func (i *Installations) AddInstallation(ctx *GlobalContext, installPath string, profile string) (*Installation, error) { + installation := &Installation{ + Path: installPath, + Profile: profile, + } + + if err := installation.Validate(ctx); err != nil { + return nil, errors.Wrap(err, "failed to validate installation") + } + + newStat, err := os.Stat(installation.Path) + if err != nil { + return nil, errors.Wrap(err, "failed to stat installation directory") + } + + found := false + for _, install := range ctx.Installations.Installations { + stat, err := os.Stat(install.Path) + if err != nil { + continue + } + + found = os.SameFile(newStat, stat) + if found { + break + } + } + + if found { + return nil, errors.New("installation already present") + } + + i.Installations = append(i.Installations, installation) + + return installation, nil +} + +func (i *Installations) GetInstallation(installPath string) *Installation { + for _, install := range i.Installations { + if install.Path == installPath { + return install + } + } + + return nil +} + +func (i *Installation) Validate(ctx *GlobalContext) error { + found := false + for _, p := range ctx.Profiles.Profiles { + if p.Name == i.Profile { + found = true + break + } + } + + if !found { + return errors.New("profile not found") + } + + // TODO Validate installation path + + return nil +} + +func (i *Installation) Install(ctx *GlobalContext) error { + if err := i.Validate(ctx); err != nil { + return errors.Wrap(err, "failed to validate installation") + } + + return nil } diff --git a/cli/installations_test.go b/cli/installations_test.go new file mode 100644 index 0000000..8c7cdaf --- /dev/null +++ b/cli/installations_test.go @@ -0,0 +1,35 @@ +package cli + +import ( + "testing" + + "github.com/MarvinJWendt/testza" + "github.com/satisfactorymodding/ficsit-cli/cfg" +) + +func init() { + cfg.SetDefaults() +} + +func TestInstallationsInit(t *testing.T) { + installations, err := InitInstallations() + testza.AssertNoError(t, err) + testza.AssertNotNil(t, installations) +} + +func TestAddInstallation(t *testing.T) { + ctx, err := InitCLI() + testza.AssertNoError(t, err) + + profileName := "InstallationTest" + profile := ctx.Profiles.AddProfile(profileName) + testza.AssertNoError(t, profile.AddMod("AreaActions", ">=1.6.5")) + testza.AssertNoError(t, profile.AddMod("ArmorModules__Modpack_All", ">=1.4.1")) + + installation, err := ctx.Installations.AddInstallation(ctx, "../testdata/server", profileName) + testza.AssertNoError(t, err) + testza.AssertNotNil(t, installation) + + err = installation.Install(ctx) + testza.AssertNoError(t, err) +} diff --git a/cli/profiles.go b/cli/profiles.go index bf1d427..7e897bb 100644 --- a/cli/profiles.go +++ b/cli/profiles.go @@ -1,4 +1,200 @@ package cli -type Profile struct { +import ( + "encoding/json" + "fmt" + "os" + "path" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/ficsit-cli/utils" + "github.com/spf13/viper" +) + +const defaultProfileName = "Default" + +var defaultProfile = Profile{ + Name: defaultProfileName, +} + +type ProfilesVersion int + +const ( + InitialProfilesVersion = ProfilesVersion(iota) + + // Always last + nextProfilesVersion +) + +type Profiles struct { + Version ProfilesVersion `json:"version"` + Profiles []*Profile `json:"profiles"` + SelectedProfile string `json:"selected_profile"` +} + +type Profile struct { + Name string `json:"name"` + Mods map[string]ProfileMod `json:"mods"` +} + +type ProfileMod struct { + Version string `json:"version"` + InstalledVersion string `json:"installed_version"` +} + +func InitProfiles() (*Profiles, error) { + cacheDir := viper.GetString("cache-dir") + + profilesFile := path.Join(cacheDir, viper.GetString("profiles-file")) + _, err := os.Stat(profilesFile) + if err != nil { + if !os.IsNotExist(err) { + return nil, errors.Wrap(err, "failed to stat profiles file") + } + + _, err := os.Stat(cacheDir) + if err != nil { + if !os.IsNotExist(err) { + return nil, errors.Wrap(err, "failed to read cache directory") + } + + err = os.MkdirAll(cacheDir, 0755) + if err != nil { + return nil, errors.Wrap(err, "failed to create cache directory") + } + } + + emptyProfiles := Profiles{ + Version: nextProfilesVersion - 1, + Profiles: []*Profile{&defaultProfile}, + } + + if err := emptyProfiles.Save(); err != nil { + return nil, errors.Wrap(err, "failed to save empty profiles") + } + } + + profilesData, err := os.ReadFile(profilesFile) + if err != nil { + return nil, errors.Wrap(err, "failed to read profiles") + } + + var profiles Profiles + if err := json.Unmarshal(profilesData, &profiles); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal profiles") + } + + if profiles.Version >= nextProfilesVersion { + return nil, fmt.Errorf("unknown profiles version: %d", profiles.Version) + } + + if len(profiles.Profiles) == 0 { + profiles.Profiles = []*Profile{&defaultProfile} + profiles.SelectedProfile = defaultProfileName + } + + if profiles.SelectedProfile == "" { + profiles.SelectedProfile = profiles.Profiles[0].Name + } + + return &profiles, nil +} + +// Save the profiles to the profiles file. +func (p *Profiles) Save() error { + if viper.GetBool("dry-run") { + log.Info().Msg("dry-run: skipping profile saving") + return nil + } + + profilesFile := path.Join(viper.GetString("cache-dir"), viper.GetString("profiles-file")) + + log.Info().Str("path", profilesFile).Msg("saving profiles") + + profilesJSON, err := json.MarshalIndent(p, "", " ") + if err != nil { + return errors.Wrap(err, "failed to marshal profiles") + } + + if err := os.WriteFile(profilesFile, profilesJSON, 0755); err != nil { + return errors.Wrap(err, "failed to write profiles") + } + + return nil +} + +// AddProfile adds a new profile with the given name to the profiles list. +func (p *Profiles) AddProfile(name string) *Profile { + profile := &Profile{ + Name: name, + } + + p.Profiles = append(p.Profiles, profile) + + return profile +} + +// DeleteProfile deletes the profile with the given name. +func (p *Profiles) DeleteProfile(name string) { + i := 0 + for _, profile := range p.Profiles { + if profile.Name == name { + break + } + + i++ + } + + if i < len(p.Profiles) { + p.Profiles = append(p.Profiles[:i], p.Profiles[i+1:]...) + } +} + +// GetProfile returns the profile with the given name or nil if it doesn't exist. +func (p *Profiles) GetProfile(name string) *Profile { + for _, profile := range p.Profiles { + if profile.Name == name { + return profile + } + } + + return nil +} + +// AddMod adds a mod to the profile with given version. +func (p *Profile) AddMod(reference string, version string) error { + if p.Mods == nil { + p.Mods = make(map[string]ProfileMod) + } + + if !utils.SemVerRegex.MatchString(version) { + return errors.New("invalid semver version") + } + + p.Mods[reference] = ProfileMod{ + Version: version, + } + + return nil +} + +// RemoveMod removes a mod from the profile. +func (p *Profile) RemoveMod(reference string) { + if p.Mods == nil { + return + } + + delete(p.Mods, reference) +} + +// HasMod returns true if the profile has a mod with the given reference +func (p *Profile) HasMod(reference string) bool { + if p.Mods == nil { + return false + } + + _, ok := p.Mods[reference] + + return ok } diff --git a/cli/profiles_test.go b/cli/profiles_test.go new file mode 100644 index 0000000..d83ac94 --- /dev/null +++ b/cli/profiles_test.go @@ -0,0 +1,18 @@ +package cli + +import ( + "testing" + + "github.com/MarvinJWendt/testza" + "github.com/satisfactorymodding/ficsit-cli/cfg" +) + +func init() { + cfg.SetDefaults() +} + +func TestProfilesInit(t *testing.T) { + profiles, err := InitProfiles() + testza.AssertNoError(t, err) + testza.AssertNotNil(t, profiles) +} diff --git a/cmd/cli.go b/cmd/cli.go new file mode 100644 index 0000000..7e63fbd --- /dev/null +++ b/cmd/cli.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "github.com/satisfactorymodding/ficsit-cli/cli" + "github.com/satisfactorymodding/ficsit-cli/tea" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(cliCmd) +} + +var cliCmd = &cobra.Command{ + Use: "cli", + Short: "Start interactive CLI", + RunE: func(cmd *cobra.Command, args []string) error { + global, err := cli.InitCLI() + if err != nil { + panic(err) + } + + return tea.RunTea(global) + }, +} diff --git a/cmd/download.go b/cmd/download.go deleted file mode 100644 index 40269e6..0000000 --- a/cmd/download.go +++ /dev/null @@ -1,84 +0,0 @@ -package cmd - -import ( - "bufio" - "encoding/binary" - "fmt" - "github.com/davecgh/go-spew/spew" - "github.com/spf13/cobra" - "net" - "strings" - "time" -) - -// Slice of strings with placeholder text. -var fakeInstallList = strings.Split("pseudo-excel pseudo-photoshop pseudo-chrome pseudo-outlook pseudo-explorer "+ - "pseudo-dops pseudo-git pseudo-vsc pseudo-intellij pseudo-minecraft pseudo-scoop pseudo-chocolatey", " ") - -func init() { - rootCmd.AddCommand(downloadCmd) -} - -var downloadCmd = &cobra.Command{ - Use: "download", - Aliases: []string{"dl"}, - Short: "Download a mod", - RunE: func(cmd *cobra.Command, args []string) error { - - conn, err := net.Dial("udp", "127.0.0.1:15777") - if err != nil { - return err - } - defer conn.Close() - - const protoVersion = 0 - encoded := make([]byte, 8) - binary.LittleEndian.PutUint64(encoded, uint64(time.Now().UnixMilli())) - - query := append([]byte{0, protoVersion}, encoded...) - spew.Dump("Query:", query) - - if _, err := conn.Write(query); err != nil { - return err - } - - response := make([]byte, 17) - _, err = bufio.NewReader(conn).Read(response) - - spew.Dump("Response:", response) - - serverQueryID := response[0] - serverProtocolVersion := response[1] - serverTimestamp := binary.LittleEndian.Uint64(response[2:10]) - serverState := response[10] - serverNetCL := binary.LittleEndian.Uint32(response[11:15]) - beaconPort := binary.LittleEndian.Uint16(response[15:]) - - fmt.Printf("Server query ID: %d\n", serverQueryID) - fmt.Printf("Server protocol version: %d\n", serverProtocolVersion) - fmt.Printf("Server timestamp: %d\n", serverTimestamp) - fmt.Printf("Server state: %d\n", serverState) - fmt.Printf("Server net CL: %d\n", serverNetCL) - fmt.Printf("Server beacon port: %d\n", beaconPort) - //for i := 0; i < 5; i++ { - // log.Info().Int("i", i).Msg("Foo") - // time.Sleep(time.Second) - //} - // - //p, _ := pterm.DefaultProgressbar.WithTotal(len(fakeInstallList)).WithTitle("Downloading stuff").Start() - // - //for i := 0; i < p.Total; i++ { - // p.UpdateTitle("Downloading " + fakeInstallList[i]) // Update the title of the progressbar. - // pterm.Success.Println("Downloading " + fakeInstallList[i]) // If a progressbar is running, each print will be printed above the progressbar. - // p.Increment() // Increment the progressbar by one. Use Add(x int) to increment by a custom amount. - // time.Sleep(time.Millisecond * 350) // Sleep 350 milliseconds. - //} - // - //for i := 0; i < 5; i++ { - // log.Info().Int("i", i).Msg("Bar") - // time.Sleep(time.Second) - //} - - return nil - }, -} diff --git a/cmd/root.go b/cmd/root.go index f8027f6..6b91eae 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,21 +1,24 @@ package cmd import ( + "io" + "os" + "path/filepath" + "time" + + "github.com/pkg/errors" + "github.com/pterm/pterm" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/satisfactorymodding/ficsit-cli/tea" "github.com/spf13/cobra" "github.com/spf13/viper" - "os" - "path" - "time" ) var rootCmd = &cobra.Command{ Use: "ficsit", Short: "cli mod manager for satisfactory", - PersistentPreRun: func(cmd *cobra.Command, args []string) { + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { viper.SetConfigName("config") viper.AddConfigPath(".") viper.SetEnvPrefix("ficsit") @@ -30,24 +33,50 @@ var rootCmd = &cobra.Command{ zerolog.SetGlobalLevel(level) + writers := make([]io.Writer, 0) if viper.GetBool("pretty") { pterm.EnableStyling() - log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger().Output(zerolog.ConsoleWriter{ - Out: os.Stdout, - TimeFormat: time.RFC3339, - }) } else { pterm.DisableStyling() } + + if !viper.GetBool("quiet") { + writers = append(writers, zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + }) + } + + if viper.GetString("log-file") != "" { + logFile, err := os.OpenFile(viper.GetString("log-file"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + + if err != nil { + return errors.Wrap(err, "failed to open log file") + } + + writers = append(writers, logFile) + } + + log.Logger = zerolog.New(io.MultiWriter(writers...)).With().Timestamp().Logger() + + return nil }, } func Execute() { // Execute tea as default cmd, _, err := rootCmd.Find(os.Args[1:]) + + cli := len(os.Args) >= 2 && os.Args[1] == "cli" if (len(os.Args) <= 1 || os.Args[1] != "help") && (err != nil || cmd == rootCmd) { - tea.RunTea() - return + args := append([]string{"cli"}, os.Args[1:]...) + rootCmd.SetArgs(args) + cli = true + } + + // Always be quiet in CLI mode + if cli { + viper.Set("quiet", true) } if err := rootCmd.Execute(); err != nil { @@ -62,12 +91,28 @@ func init() { } rootCmd.PersistentFlags().String("log", "info", "The log level to output") + rootCmd.PersistentFlags().String("log-file", "", "File to output logs to") + rootCmd.PersistentFlags().Bool("quiet", false, "Do not log anything to console") rootCmd.PersistentFlags().Bool("pretty", true, "Whether to render pretty terminal output") - rootCmd.PersistentFlags().String("cache-dir", path.Join(baseCacheDir, "ficsit"), "The cache directory") + rootCmd.PersistentFlags().Bool("dry-run", false, "Dry-run. Do not save any changes") + + rootCmd.PersistentFlags().String("cache-dir", filepath.Clean(filepath.Join(baseCacheDir, "ficsit")), "The cache directory") + rootCmd.PersistentFlags().String("profiles-file", "profiles.json", "The profiles file") + rootCmd.PersistentFlags().String("installations-file", "installations.json", "The installations file") + + rootCmd.PersistentFlags().String("api", "https://api.ficsit.app/v2/query", "URL for API") _ = viper.BindPFlag("log", rootCmd.PersistentFlags().Lookup("log")) + _ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file")) + _ = viper.BindPFlag("quiet", rootCmd.PersistentFlags().Lookup("quiet")) _ = viper.BindPFlag("pretty", rootCmd.PersistentFlags().Lookup("pretty")) + _ = viper.BindPFlag("dry-run", rootCmd.PersistentFlags().Lookup("dry-run")) + _ = viper.BindPFlag("cache-dir", rootCmd.PersistentFlags().Lookup("cache-dir")) + _ = viper.BindPFlag("profiles-file", rootCmd.PersistentFlags().Lookup("profiles-file")) + _ = viper.BindPFlag("installations-file", rootCmd.PersistentFlags().Lookup("installations-file")) + + _ = viper.BindPFlag("api", rootCmd.PersistentFlags().Lookup("api")) } diff --git a/ficsit/.gitignore b/ficsit/.gitignore new file mode 100644 index 0000000..108d03a --- /dev/null +++ b/ficsit/.gitignore @@ -0,0 +1 @@ +types.go \ No newline at end of file diff --git a/ficsit/api_test.go b/ficsit/api_test.go new file mode 100644 index 0000000..1eb17df --- /dev/null +++ b/ficsit/api_test.go @@ -0,0 +1,36 @@ +package ficsit + +import ( + "context" + "testing" + + "github.com/Khan/genqlient/graphql" + "github.com/MarvinJWendt/testza" + "github.com/satisfactorymodding/ficsit-cli/cfg" +) + +var client graphql.Client + +func init() { + cfg.SetDefaults() + client = InitAPI() +} + +func TestModVersions(t *testing.T) { + response, err := ModVersions(context.Background(), client, "SmartFoundations", VersionFilter{}) + testza.AssertNoError(t, err) + testza.AssertNotNil(t, response) + testza.AssertNotNil(t, response.GetMod) + testza.AssertNotNil(t, response.GetMod.Versions) + testza.AssertNotZero(t, len(response.GetMod.Versions)) +} + +func TestMods(t *testing.T) { + response, err := Mods(context.Background(), client, ModFilter{}) + testza.AssertNoError(t, err) + testza.AssertNotNil(t, response) + testza.AssertNotNil(t, response.GetMods) + testza.AssertNotNil(t, response.GetMods.Mods) + testza.AssertNotZero(t, response.GetMods.Count) + testza.AssertNotZero(t, len(response.GetMods.Mods)) +} diff --git a/ficsit/queries/mod.graphql b/ficsit/queries/mod.graphql new file mode 100644 index 0000000..8e66f5c --- /dev/null +++ b/ficsit/queries/mod.graphql @@ -0,0 +1,18 @@ +query GetMod ($modId: ModID!) { + getMod(modId: $modId) { + id + mod_reference + name + views + downloads + authors { + role + user { + username + } + } + full_description + source_url + created_at + } +} \ No newline at end of file diff --git a/ficsit/queries/mod_versions.graphql b/ficsit/queries/mod_versions.graphql new file mode 100644 index 0000000..34c1b00 --- /dev/null +++ b/ficsit/queries/mod_versions.graphql @@ -0,0 +1,13 @@ +# @genqlient(omitempty: true) +query ModVersions ( + $modId: ModID!, + $filter: VersionFilter +) { + getMod(modId: $modId) { + id + versions (filter: $filter) { + id + version + } + } +} \ No newline at end of file diff --git a/ficsit/queries/mods.graphql b/ficsit/queries/mods.graphql new file mode 100644 index 0000000..6a6559f --- /dev/null +++ b/ficsit/queries/mods.graphql @@ -0,0 +1,11 @@ +# @genqlient(omitempty: true) +query Mods ($filter: ModFilter) { + getMods (filter: $filter) { + count + mods { + id + name + mod_reference + } + } +} diff --git a/ficsit/root.go b/ficsit/root.go new file mode 100644 index 0000000..ce1e85f --- /dev/null +++ b/ficsit/root.go @@ -0,0 +1,12 @@ +package ficsit + +import ( + "net/http" + + "github.com/Khan/genqlient/graphql" + "github.com/spf13/viper" +) + +func InitAPI() graphql.Client { + return graphql.NewClient(viper.GetString("api"), http.DefaultClient) +} diff --git a/genqlient.yaml b/genqlient.yaml new file mode 100644 index 0000000..427ed70 --- /dev/null +++ b/genqlient.yaml @@ -0,0 +1,23 @@ +# https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +schema: schema.graphql +operations: +- ficsit/queries/*.graphql +generated: ficsit/types.go +package: ficsit +bindings: + UserID: + type: string + ModReference: + type: string + BootstrapVersionID: + type: string + ModID: + type: string + VersionID: + type: string + GuideID: + type: string + SMLVersionID: + type: string + Date: + type: time.Time \ No newline at end of file diff --git a/go.mod b/go.mod index ad2f9aa..2bc797f 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,14 @@ module github.com/satisfactorymodding/ficsit-cli go 1.17 require ( + github.com/Khan/genqlient v0.3.0 + github.com/MarvinJWendt/testza v0.2.10 github.com/charmbracelet/bubbles v0.9.0 github.com/charmbracelet/bubbletea v0.19.0 + github.com/charmbracelet/glamour v0.3.0 github.com/charmbracelet/lipgloss v0.4.0 github.com/davecgh/go-spew v1.1.1 + github.com/pkg/errors v0.9.1 github.com/pterm/pterm v0.12.33 github.com/rs/zerolog v1.25.0 github.com/spf13/cobra v1.2.1 @@ -14,23 +18,33 @@ require ( ) require ( + github.com/agnivade/levenshtein v1.0.3 // indirect + github.com/alecthomas/chroma v0.8.2 // indirect + github.com/alexflint/go-arg v1.4.2 // indirect + github.com/alexflint/go-scalar v1.0.0 // indirect github.com/atomicgo/cursor v0.0.1 // indirect github.com/atotto/clipboard v0.1.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/containerd/console v1.0.2 // indirect + github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect + github.com/dlclark/regexp2 v1.2.0 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/gookit/color v1.4.2 // indirect + github.com/gorilla/css v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-isatty v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/microcosm-cc/bluemonday v1.0.6 // indirect github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.9.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.9.4 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect github.com/spf13/afero v1.6.0 // indirect @@ -38,10 +52,17 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.2.0 // indirect + github.com/vektah/gqlparser/v2 v2.1.0 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + github.com/yuin/goldmark v1.3.5 // indirect + github.com/yuin/goldmark-emoji v1.0.1 // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 // indirect golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.6 // indirect + golang.org/x/tools v0.1.5 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 440e35b..a6a6775 100644 --- a/go.sum +++ b/go.sum @@ -43,15 +43,37 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Khan/genqlient v0.3.0 h1:G35N630mNCW+j0rqSJUsvNkPLoX0bjrllRMnaQTbCak= +github.com/Khan/genqlient v0.3.0/go.mod h1:o9QUG7O7GhCeB3C83scbUQtdp+tdErC8OkVbSxIw1g4= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= github.com/MarvinJWendt/testza v0.2.10 h1:cX4zE9TofXxe72a6EPIYAxC+8cVWTsmmgsXTZIT+5bQ= github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0= +github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.8.2 h1:x3zkuE2lUk/RIekyAJ3XRqSCP4zwWDfcw/YJCuCAACg= +github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0= +github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= +github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70= +github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -60,8 +82,11 @@ github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/charmbracelet/bubbles v0.9.0 h1:lqJ8FXwoLceQF2J0A+dWo1Cuu1dNyjbW4Opgdi2vkhw= @@ -69,6 +94,8 @@ github.com/charmbracelet/bubbles v0.9.0/go.mod h1:NWT/c+0rYEnYChz5qCyX4Lj6fDw9gG github.com/charmbracelet/bubbletea v0.14.1/go.mod h1:b5lOf5mLjMg1tRn1HVla54guZB+jvsyV0yYAQja95zE= github.com/charmbracelet/bubbletea v0.19.0 h1:1gz4rbxl3qZik/oP8QW2vUtul2gO8RDDzmoLGERpTQc= github.com/charmbracelet/bubbletea v0.19.0/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= +github.com/charmbracelet/glamour v0.3.0 h1:3H+ZrKlSg8s+WU6V7eF2eRVYt8lCueffbi7r2+ffGkc= +github.com/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw= github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.3.0/go.mod h1:VkhdBS2eNAmRkTwRKLJCFhCOVkjntMusBDxv7TXahuk= github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g= @@ -86,10 +113,16 @@ github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -104,10 +137,12 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -181,6 +216,11 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= @@ -230,10 +270,12 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -244,10 +286,13 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.6 h1:ZOvqHKtnx0fUpnbQm3m3zKFWE+DRC+XB1onh8JoEObE= +github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -258,6 +303,7 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= @@ -267,12 +313,17 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0= github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= @@ -297,6 +348,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II= github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI= @@ -306,7 +358,12 @@ github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYI github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -325,6 +382,8 @@ github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -334,13 +393,21 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= +github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns= +github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= +github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -398,6 +465,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -435,7 +503,9 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -494,6 +564,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -544,6 +615,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -551,6 +623,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -567,6 +640,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -597,10 +671,12 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -738,6 +814,7 @@ gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -754,3 +831,5 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= diff --git a/tea/components/header.go b/tea/components/header.go new file mode 100644 index 0000000..825db2c --- /dev/null +++ b/tea/components/header.go @@ -0,0 +1,48 @@ +package components + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*headerComponent)(nil) + +type headerComponent struct { + root RootModel + labelStyle lipgloss.Style +} + +func NewHeaderComponent(root RootModel) tea.Model { + return headerComponent{ + root: root, + labelStyle: utils.LabelStyle, + } +} + +func (h headerComponent) Init() tea.Cmd { + return nil +} + +func (h headerComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + return h, nil +} + +func (h headerComponent) View() string { + out := h.labelStyle.Render("Installation: ") + if h.root.GetCurrentInstallation() != nil { + out += h.root.GetCurrentInstallation().Path + } else { + out += "None" + } + out += "\n" + + out += h.labelStyle.Render("Profile: ") + if h.root.GetCurrentProfile() != nil { + out += h.root.GetCurrentProfile().Name + } else { + out += "None" + } + + return lipgloss.NewStyle().Margin(1, 0).Render(out) +} diff --git a/tea/components/types.go b/tea/components/types.go new file mode 100644 index 0000000..de21f8c --- /dev/null +++ b/tea/components/types.go @@ -0,0 +1,25 @@ +package components + +import ( + "github.com/Khan/genqlient/graphql" + tea "github.com/charmbracelet/bubbletea" + "github.com/satisfactorymodding/ficsit-cli/cli" +) + +type RootModel interface { + GetGlobal() *cli.GlobalContext + + GetCurrentProfile() *cli.Profile + SetCurrentProfile(profile *cli.Profile) error + + GetCurrentInstallation() *cli.Installation + SetCurrentInstallation(installation *cli.Installation) error + + GetAPIClient() graphql.Client + + Size() tea.WindowSizeMsg + SetSize(size tea.WindowSizeMsg) + + View() string + Height() int +} diff --git a/tea/keys.go b/tea/keys.go deleted file mode 100644 index 376b794..0000000 --- a/tea/keys.go +++ /dev/null @@ -1,55 +0,0 @@ -package tea - -import "github.com/charmbracelet/bubbles/key" - -type keyMap struct { - Up key.Binding - Down key.Binding - Left key.Binding - Right key.Binding - Enter key.Binding - Help key.Binding - Quit key.Binding -} - -func (k keyMap) ShortHelp() []key.Binding { - return []key.Binding{k.Help, k.Quit} -} - -func (k keyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - {k.Up, k.Down, k.Left, k.Right}, - {k.Enter, k.Help, k.Quit}, - } -} - -var keys = keyMap{ - Up: key.NewBinding( - key.WithKeys("up", "k"), - key.WithHelp("↑/k", "move up"), - ), - Down: key.NewBinding( - key.WithKeys("down", "j"), - key.WithHelp("↓/j", "move down"), - ), - Left: key.NewBinding( - key.WithKeys("left", "h"), - key.WithHelp("←/h", "move left"), - ), - Right: key.NewBinding( - key.WithKeys("right", "l"), - key.WithHelp("→/l", "move right"), - ), - Enter: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("Enter", "confirm selection"), - ), - Help: key.NewBinding( - key.WithKeys("?"), - key.WithHelp("?", "toggle help"), - ), - Quit: key.NewBinding( - key.WithKeys("q", "esc", "ctrl+c"), - key.WithHelp("q", "quit"), - ), -} diff --git a/tea/root.go b/tea/root.go index e573719..6b624ef 100644 --- a/tea/root.go +++ b/tea/root.go @@ -1,76 +1,89 @@ package tea import ( - "fmt" + "github.com/Khan/genqlient/graphql" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/pkg/errors" "github.com/satisfactorymodding/ficsit-cli/cli" + "github.com/satisfactorymodding/ficsit-cli/ficsit" + "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/scenes" - "os" ) -var docStyle = lipgloss.NewStyle().Margin(1, 2) - -type item string - -func (i item) FilterValue() string { return string(i) } - type rootModel struct { - currentModel tea.Model currentProfile *cli.Profile currentInstallation *cli.Installation + global *cli.GlobalContext + apiClient graphql.Client + currentSize tea.WindowSizeMsg + headerComponent tea.Model } -func (m *rootModel) ChangeScene(model tea.Model) { - m.currentModel = model +func newModel(global *cli.GlobalContext) *rootModel { + m := &rootModel{ + global: global, + currentProfile: global.Profiles.GetProfile(global.Profiles.SelectedProfile), + currentInstallation: global.Installations.GetInstallation(global.Installations.SelectedInstallation), + apiClient: ficsit.InitAPI(), + currentSize: tea.WindowSizeMsg{ + Width: 20, + Height: 14, + }, + } + + m.headerComponent = components.NewHeaderComponent(m) + + return m } func (m *rootModel) GetCurrentProfile() *cli.Profile { return m.currentProfile } -func (m *rootModel) SetCurrentProfile(profile *cli.Profile) { +func (m *rootModel) SetCurrentProfile(profile *cli.Profile) error { m.currentProfile = profile + m.global.Profiles.SelectedProfile = profile.Name + return m.global.Save() } func (m *rootModel) GetCurrentInstallation() *cli.Installation { return m.currentInstallation } -func (m *rootModel) SetCurrentInstallation(installation *cli.Installation) { +func (m *rootModel) SetCurrentInstallation(installation *cli.Installation) error { m.currentInstallation = installation + m.global.Installations.SelectedInstallation = installation.Path + return m.global.Save() } -func newModel() rootModel { - m := rootModel{} - m.currentModel = scenes.NewMainMenu(&m) - return m +func (m *rootModel) GetAPIClient() graphql.Client { + return m.apiClient } -func (m rootModel) Init() tea.Cmd { - return m.currentModel.Init() +func (m *rootModel) Size() tea.WindowSizeMsg { + return m.currentSize } -func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - m.currentModel, cmd = m.currentModel.Update(msg) - return m, cmd +func (m *rootModel) SetSize(size tea.WindowSizeMsg) { + m.currentSize = size } -func (m rootModel) View() string { - - style := lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("220")) - - out := style.Render("Installation:") + " " + "// TODO" + "\n" - out += style.Render("Profile:") + " " + "// TODO" + "\n" - out += "\n" - - return out + m.currentModel.View() +func (m *rootModel) View() string { + return m.headerComponent.View() } -func RunTea() { - if err := tea.NewProgram(newModel()).Start(); err != nil { - fmt.Printf("Could not start program :(\n%v\n", err) - os.Exit(1) +func (m *rootModel) Height() int { + return lipgloss.Height(m.View()) + 1 +} + +func (m *rootModel) GetGlobal() *cli.GlobalContext { + return m.global +} + +func RunTea(global *cli.GlobalContext) error { + if err := tea.NewProgram(scenes.NewMainMenu(newModel(global))).Start(); err != nil { + return errors.Wrap(err, "internal tea error") } + return nil } diff --git a/tea/scenes/exit_menu.go b/tea/scenes/exit_menu.go new file mode 100644 index 0000000..3fdc278 --- /dev/null +++ b/tea/scenes/exit_menu.go @@ -0,0 +1,90 @@ +package scenes + +import ( + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/davecgh/go-spew/spew" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*exitMenu)(nil) + +type exitMenu struct { + root components.RootModel + list list.Model +} + +func NewExitMenu(root components.RootModel) tea.Model { + model := mainMenu{ + root: root, + } + + items := []list.Item{ + utils.SimpleItem{ + Title: "Exit Saving Changes", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + if err := root.GetGlobal().Save(); err != nil { + panic(err) // TODO + } + return currentModel, tea.Quit + }, + }, + utils.SimpleItem{ + Title: "Exit Discarding Changes", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + return currentModel, tea.Quit + }, + }, + } + + model.list = list.NewModel(items, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height()) + model.list.SetShowStatusBar(false) + model.list.SetFilteringEnabled(false) + model.list.Title = "Save Changes?" + model.list.Styles = utils.ListStyles + model.list.DisableQuitKeybindings() + model.list.SetSize(model.list.Width(), model.list.Height()) + + return model +} + +func (m exitMenu) Init() tea.Cmd { + return nil +} + +func (m exitMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Warn().Msg(spew.Sdump(msg)) + switch msg := msg.(type) { + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case KeyControlC: + return m, tea.Quit + case KeyEnter: + i, ok := m.list.SelectedItem().(utils.SimpleItem) + if ok { + if i.Activate != nil { + i.Activate(msg, m) + return m, tea.Quit + } + } + return m, tea.Quit + default: + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + 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) + } + + return m, nil +} + +func (m exitMenu) View() string { + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View()) +} diff --git a/tea/scenes/installations.go b/tea/scenes/installations.go index e47b351..a16839e 100644 --- a/tea/scenes/installations.go +++ b/tea/scenes/installations.go @@ -1,7 +1,10 @@ package scenes -import tea "github.com/charmbracelet/bubbletea" +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/satisfactorymodding/ficsit-cli/tea/components" +) -func NewInstallations(root RootModel) tea.Model { +func NewInstallations(root components.RootModel, parent tea.Model) tea.Model { return nil } diff --git a/tea/scenes/keys.go b/tea/scenes/keys.go new file mode 100644 index 0000000..5f1dc12 --- /dev/null +++ b/tea/scenes/keys.go @@ -0,0 +1,6 @@ +package scenes + +const ( + KeyControlC = "ctrl+c" + KeyEnter = "enter" +) diff --git a/tea/scenes/main_menu.go b/tea/scenes/main_menu.go index 7919f62..2a30b97 100644 --- a/tea/scenes/main_menu.go +++ b/tea/scenes/main_menu.go @@ -1,78 +1,82 @@ package scenes import ( - "fmt" - "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "io" + "github.com/davecgh/go-spew/spew" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" ) +var _ tea.Model = (*mainMenu)(nil) + type mainMenu struct { - root RootModel - help help.Model - inputStyle lipgloss.Style - lastKey string - quitting bool - list list.Model + root components.RootModel + list list.Model } -type menuItem struct { - Title string - ModelFn func(model RootModel) tea.Model -} - -func (i menuItem) FilterValue() string { return i.Title } - -type itemDelegate struct{} - -func (d itemDelegate) Height() int { return 1 } -func (d itemDelegate) Spacing() int { return 0 } -func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } -func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - i, ok := listItem.(menuItem) - if !ok { - return - } - - style := lipgloss.NewStyle().PaddingLeft(2) - - str := style.Render("o " + i.Title) - if index == m.Index() { - str = style.Foreground(lipgloss.Color("202")).Render("• " + i.Title) - } - - fmt.Fprintf(w, str) -} - -func NewMainMenu(root RootModel) tea.Model { - items := []list.Item{ - menuItem{ - Title: "Installations", - ModelFn: NewInstallations, - }, - menuItem{ - Title: "Profiles", - ModelFn: NewProfiles, - }, - menuItem{ - Title: "Mods", - ModelFn: NewMods, - }, - } - - l := list.NewModel(items, itemDelegate{}, 20, 14) - l.SetShowStatusBar(false) - l.SetFilteringEnabled(false) - l.SetShowTitle(false) - l.Styles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(2) - l.Styles.HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(2).PaddingBottom(1) - - return mainMenu{ +func NewMainMenu(root components.RootModel) tea.Model { + model := mainMenu{ root: root, - list: l, } + + items := []list.Item{ + utils.SimpleItem{ + Title: "Installations", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewInstallations(root, currentModel) + return newModel, newModel.Init() + }, + }, + utils.SimpleItem{ + Title: "Profiles", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewProfiles(root, currentModel) + return newModel, newModel.Init() + }, + }, + utils.SimpleItem{ + Title: "Mods", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewMods(root, currentModel) + return newModel, newModel.Init() + }, + }, + utils.SimpleItem{ + Title: "Apply Changes", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + // TODO Apply changes to all changed profiles + return nil, nil + }, + }, + utils.SimpleItem{ + Title: "Save", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + if err := root.GetGlobal().Save(); err != nil { + panic(err) // TODO Handle Error + } + return nil, nil + }, + }, + utils.SimpleItem{ + Title: "Exit", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewExitMenu(root) + return newModel, newModel.Init() + }, + }, + } + + model.list = list.NewModel(items, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height()) + model.list.SetShowStatusBar(false) + model.list.SetFilteringEnabled(false) + model.list.Title = "Main Menu" + model.list.Styles = utils.ListStyles + model.list.SetSize(model.list.Width(), model.list.Height()) + + return model } func (m mainMenu) Init() tea.Cmd { @@ -80,19 +84,26 @@ func (m mainMenu) Init() tea.Cmd { } func (m mainMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Warn().Msg(spew.Sdump(msg)) switch msg := msg.(type) { case tea.KeyMsg: switch keypress := msg.String(); keypress { - case "ctrl+c": - fallthrough - case "q": - m.quitting = true + case KeyControlC: return m, tea.Quit - case "enter": - i, ok := m.list.SelectedItem().(menuItem) + case "q": + newModel := NewExitMenu(m.root) + return newModel, newModel.Init() + case KeyEnter: + i, ok := m.list.SelectedItem().(utils.SimpleItem) if ok { - if i.ModelFn != nil { - m.root.ChangeScene(i.ModelFn(m.root)) + if i.Activate != nil { + newModel, cmd := i.Activate(msg, m) + if newModel != nil || cmd != nil { + if newModel == nil { + newModel = m + } + return newModel, cmd + } return m, nil } } @@ -105,11 +116,12 @@ func (m mainMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: 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) } return m, nil } func (m mainMenu) View() string { - return m.list.View() + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View()) } diff --git a/tea/scenes/mod.go b/tea/scenes/mod.go new file mode 100644 index 0000000..463fb23 --- /dev/null +++ b/tea/scenes/mod.go @@ -0,0 +1,135 @@ +package scenes + +import ( + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/davecgh/go-spew/spew" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*modMenu)(nil) + +type modMenu struct { + root components.RootModel + list list.Model + parent tea.Model +} + +func NewModMenu(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model { + model := modMenu{ + root: root, + parent: parent, + } + + var items []list.Item + if root.GetCurrentProfile().HasMod(mod.Reference) { + items = []list.Item{ + utils.SimpleItem{ + Title: "Remove Mod", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + root.GetCurrentProfile().RemoveMod(mod.Reference) + return currentModel.(modMenu).parent, nil + }, + }, + utils.SimpleItem{ + Title: "Change Version", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewModVersion(root, currentModel.(modMenu).parent, mod) + return newModel, newModel.Init() + }, + }, + } + } else { + items = []list.Item{ + utils.SimpleItem{ + Title: "Install Mod", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + err := root.GetCurrentProfile().AddMod(mod.Reference, ">=0.0.0") + if err != nil { + panic(err) // TODO Handle Error + } + return currentModel.(modMenu).parent, nil + }, + }, + utils.SimpleItem{ + Title: "Install Mod with specific version", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewModVersion(root, currentModel.(modMenu).parent, mod) + return newModel, newModel.Init() + }, + }, + } + } + + items = append(items, utils.SimpleItem{ + Title: "View Mod info", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewModInfo(root, currentModel, mod) + return newModel, newModel.Init() + }, + }) + + model.list = list.NewModel(items, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height()) + model.list.SetShowStatusBar(false) + model.list.SetFilteringEnabled(false) + model.list.Title = mod.Name + model.list.Styles = utils.ListStyles + model.list.SetSize(model.list.Width(), model.list.Height()) + model.list.KeyMap.Quit.SetHelp("q", "back") + + return model +} + +func (m modMenu) Init() tea.Cmd { + return nil +} + +func (m modMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Warn().Msg(spew.Sdump(msg)) + switch msg := msg.(type) { + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case KeyControlC: + return m, tea.Quit + case "q": + if m.parent != nil { + m.parent.Update(m.root.Size()) + return m.parent, nil + } + return m, tea.Quit + case KeyEnter: + i, ok := m.list.SelectedItem().(utils.SimpleItem) + if ok { + if i.Activate != nil { + newModel, cmd := i.Activate(msg, m) + if newModel != nil || cmd != nil { + if newModel == nil { + newModel.Update(m.root.Size()) + newModel = m + } + return newModel, cmd + } + return m, nil + } + } + return m, tea.Quit + default: + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + 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) + } + + return m, nil +} + +func (m modMenu) View() string { + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View()) +} diff --git a/tea/scenes/mod_info.go b/tea/scenes/mod_info.go new file mode 100644 index 0000000..7429810 --- /dev/null +++ b/tea/scenes/mod_info.go @@ -0,0 +1,202 @@ +package scenes + +import ( + "context" + "strconv" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" + "github.com/satisfactorymodding/ficsit-cli/ficsit" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*modVersionMenu)(nil) + +type modInfo struct { + root components.RootModel + viewport viewport.Model + spinner spinner.Model + parent tea.Model + modData chan ficsit.GetModGetMod + ready bool + help help.Model + keys modInfoKeyMap +} + +type modInfoKeyMap struct { + Up key.Binding + UpHalf key.Binding + UpPage key.Binding + Down key.Binding + DownHalf key.Binding + DownPage key.Binding + Help key.Binding + Back key.Binding +} + +func (k modInfoKeyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Help, k.Back} +} + +func (k modInfoKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Up, k.UpHalf, k.UpPage}, + {k.Down, k.DownHalf, k.DownPage}, + {k.Help, k.Back}, + } +} + +func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model { + model := modInfo{ + root: root, + viewport: viewport.Model{}, + spinner: spinner.NewModel(), + parent: parent, + modData: make(chan ficsit.GetModGetMod), + ready: false, + help: help.NewModel(), + keys: modInfoKeyMap{ + Up: key.NewBinding(key.WithHelp("↑/k", "move up")), + UpHalf: key.NewBinding(key.WithHelp("u", "up half page")), + UpPage: key.NewBinding(key.WithHelp("pgup/b", "page up")), + Down: key.NewBinding(key.WithHelp("↓/j", "move down")), + DownHalf: key.NewBinding(key.WithHelp("d", "down half page")), + DownPage: key.NewBinding(key.WithHelp("pgdn/ /f", "page down")), + Help: key.NewBinding(key.WithHelp("?", "toggle help")), + Back: key.NewBinding(key.WithHelp("q", "back")), + }, + } + + model.spinner.Spinner = spinner.MiniDot + model.help.Width = root.Size().Width + + go func() { + fullMod, err := ficsit.GetMod(context.TODO(), root.GetAPIClient(), mod.ID) + + if err != nil { + panic(err) // TODO Handle Error + } + + if fullMod == nil { + panic("mod is nil") // TODO Handle Error + } + + model.modData <- fullMod.GetMod + }() + + return model +} + +func (m modInfo) Init() tea.Cmd { + return tea.Batch(utils.Ticker(), spinner.Tick) +} + +func (m modInfo) CalculateSizes(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) { + if m.viewport.Width == 0 { + return m, nil + } + + bottomPadding := 2 + if m.help.ShowAll { + bottomPadding = 4 + } + + top, right, bottom, left := lipgloss.NewStyle().Margin(m.root.Height(), 3, bottomPadding).GetMargin() + m.viewport.Width = msg.Width - left - right + m.viewport.Height = msg.Height - top - bottom + m.root.SetSize(msg) + + m.help.Width = m.viewport.Width + + var cmd tea.Cmd + m.viewport, cmd = m.viewport.Update(msg) + return m, cmd +} + +func (m modInfo) 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 "q": + if m.parent != nil { + m.parent.Update(m.root.Size()) + return m.parent, nil + } + return m, tea.Quit + case "?": + m.help.ShowAll = !m.help.ShowAll + newModel, cmd := m.CalculateSizes(m.root.Size()) + return newModel, cmd + default: + var cmd tea.Cmd + m.viewport, cmd = m.viewport.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + return m.CalculateSizes(msg) + case spinner.TickMsg: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + case utils.TickMsg: + select { + case mod := <-m.modData: + bottomPadding := 2 + if m.help.ShowAll { + bottomPadding = 4 + } + + top, right, bottom, left := lipgloss.NewStyle().Margin(m.root.Height(), 3, bottomPadding).GetMargin() + m.viewport = viewport.Model{Width: m.root.Size().Width - left - right, Height: m.root.Size().Height - top - bottom} + + title := lipgloss.NewStyle().Padding(0, 2).Render(utils.TitleStyle.Render(mod.Name)) + "\n" + + sidebar := "" + sidebar += utils.LabelStyle.Render("Views: ") + strconv.Itoa(mod.Views) + "\n" + sidebar += utils.LabelStyle.Render("Downloads: ") + strconv.Itoa(mod.Downloads) + "\n" + sidebar += "\n" + sidebar += utils.LabelStyle.Render("Authors:") + "\n" + + for _, author := range mod.Authors { + sidebar += "\n" + sidebar += utils.LabelStyle.Render(author.User.Username) + " - " + author.Role + } + + description, err := glamour.Render(mod.Full_description, "dark") + if err != nil { + panic(err) // TODO Handle Error + } + + bottomPart := lipgloss.JoinHorizontal(lipgloss.Top, sidebar, strings.TrimSpace(description)) + + m.viewport.SetContent(lipgloss.JoinVertical(lipgloss.Left, title, bottomPart)) + + var cmd tea.Cmd + m.viewport, cmd = m.viewport.Update(msg) + return m, cmd + default: + return m, utils.Ticker() + } + } + + return m, nil +} + +func (m modInfo) View() string { + 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) + } + + helpBar := lipgloss.NewStyle().Padding(1, 2).Render(m.help.View(m.keys)) + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.viewport.View(), helpBar) +} diff --git a/tea/scenes/mod_semver.go b/tea/scenes/mod_semver.go new file mode 100644 index 0000000..9e287f2 --- /dev/null +++ b/tea/scenes/mod_semver.go @@ -0,0 +1,74 @@ +package scenes + +import ( + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/davecgh/go-spew/spew" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*modSemver)(nil) + +type modSemver struct { + root components.RootModel + parent tea.Model + input textinput.Model + title string + mod utils.Mod +} + +func NewModSemver(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model { + model := modSemver{ + root: root, + parent: parent, + input: textinput.NewModel(), + title: lipgloss.NewStyle().Padding(0, 2).Render(utils.TitleStyle.Render(mod.Name)), + mod: mod, + } + + model.input.Placeholder = ">=1.2.3" + model.input.Focus() + model.input.Width = root.Size().Width + + return model +} + +func (m modSemver) Init() tea.Cmd { + return nil +} + +func (m modSemver) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Warn().Msg(spew.Sdump(msg)) + switch msg := msg.(type) { + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case KeyControlC: + return m, tea.Quit + case "q": + newModel := NewExitMenu(m.root) + return newModel, newModel.Init() + case KeyEnter: + err := m.root.GetCurrentProfile().AddMod(m.mod.Reference, m.input.Value()) + if err != nil { + panic(err) // TODO Handle Error + } + return m.parent, nil + default: + var cmd tea.Cmd + m.input, cmd = m.input.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + m.root.SetSize(msg) + } + + return m, nil +} + +func (m modSemver) View() string { + inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View()) + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView) +} diff --git a/tea/scenes/mod_version.go b/tea/scenes/mod_version.go new file mode 100644 index 0000000..8d79384 --- /dev/null +++ b/tea/scenes/mod_version.go @@ -0,0 +1,119 @@ +package scenes + +import ( + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/davecgh/go-spew/spew" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*modVersionMenu)(nil) + +type modVersionMenu struct { + root components.RootModel + list list.Model + parent tea.Model +} + +func NewModVersion(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model { + model := modMenu{ + root: root, + parent: parent, + } + + items := []list.Item{ + utils.SimpleItem{ + Title: "Select Version", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewModVersionList(root, currentModel.(modMenu).parent, mod) + return newModel, newModel.Init() + }, + }, + utils.SimpleItem{ + Title: "Enter Custom SemVer", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewModSemver(root, currentModel.(modMenu).parent, mod) + return newModel, newModel.Init() + }, + }, + } + + if root.GetCurrentProfile().HasMod(mod.Reference) { + items = append([]list.Item{ + utils.SimpleItem{ + Title: "Latest", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + err := root.GetCurrentProfile().AddMod(mod.Reference, ">=0.0.0") + if err != nil { + panic(err) // TODO Handle Error + } + return currentModel.(modMenu).parent, nil + }, + }, + }, items...) + } + + model.list = list.NewModel(items, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height()) + model.list.SetShowStatusBar(false) + model.list.SetFilteringEnabled(false) + model.list.Title = mod.Name + model.list.Styles = utils.ListStyles + model.list.SetSize(model.list.Width(), model.list.Height()) + model.list.KeyMap.Quit.SetHelp("q", "back") + + return model +} + +func (m modVersionMenu) Init() tea.Cmd { + return nil +} + +func (m modVersionMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Warn().Msg(spew.Sdump(msg)) + switch msg := msg.(type) { + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case KeyControlC: + return m, tea.Quit + case "q": + if m.parent != nil { + m.parent.Update(m.root.Size()) + return m.parent, nil + } + return m, tea.Quit + case KeyEnter: + i, ok := m.list.SelectedItem().(utils.SimpleItem) + if ok { + if i.Activate != nil { + newModel, cmd := i.Activate(msg, m) + if newModel != nil || cmd != nil { + if newModel == nil { + newModel.Update(m.root.Size()) + newModel = m + } + return newModel, cmd + } + return m, nil + } + } + return m, tea.Quit + default: + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + 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) + } + + return m, nil +} + +func (m modVersionMenu) View() string { + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View()) +} diff --git a/tea/scenes/mods.go b/tea/scenes/mods.go index 2b092df..3e6dd35 100644 --- a/tea/scenes/mods.go +++ b/tea/scenes/mods.go @@ -1,7 +1,155 @@ package scenes -import tea "github.com/charmbracelet/bubbletea" +import ( + "context" -func NewMods(root RootModel) tea.Model { - return nil + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/davecgh/go-spew/spew" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/ficsit-cli/ficsit" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*modsList)(nil) + +type modsList struct { + root components.RootModel + list list.Model + parent tea.Model + items chan []list.Item +} + +func NewMods(root components.RootModel, parent tea.Model) tea.Model { + // TODO Color mods that are installed in current profile + l := list.NewModel([]list.Item{}, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height()) + l.SetShowStatusBar(true) + l.SetFilteringEnabled(false) + l.SetSpinner(spinner.MiniDot) + l.Title = "Mods" + l.Styles = utils.ListStyles + l.SetSize(l.Width(), l.Height()) + l.KeyMap.Quit.SetHelp("q", "back") + + m := &modsList{ + root: root, + list: l, + parent: parent, + items: make(chan []list.Item), + } + + go func() { + items := make([]list.Item, 0) + allMods := make([]ficsit.ModsGetModsModsMod, 0) + offset := 0 + for { + mods, err := ficsit.Mods(context.TODO(), root.GetAPIClient(), ficsit.ModFilter{ + Limit: 100, + Offset: offset, + Order_by: ficsit.ModFieldsLastVersionDate, + Order: ficsit.OrderDesc, + }) + + if err != nil { + panic(err) // TODO Handle Error + } + + if len(mods.GetMods.Mods) == 0 { + break + } + + allMods = append(allMods, mods.GetMods.Mods...) + + for i := 0; i < len(mods.GetMods.Mods); i++ { + currentOffset := offset + currentI := i + items = append(items, utils.SimpleItem{ + Title: mods.GetMods.Mods[i].Name, + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + mod := allMods[currentOffset+currentI] + return NewModMenu(root, currentModel, utils.Mod{ + Name: mod.Name, + ID: mod.Id, + Reference: mod.Mod_reference, + }), nil + }, + }) + } + + offset += len(mods.GetMods.Mods) + } + + m.items <- items + }() + + return m +} + +func (m modsList) Init() tea.Cmd { + return utils.Ticker() +} + +func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Info().Msg(spew.Sdump(msg)) + switch msg := msg.(type) { + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case KeyControlC: + return m, tea.Quit + case "q": + if m.parent != nil { + m.parent.Update(m.root.Size()) + return m.parent, nil + } + return m, tea.Quit + case KeyEnter: + i, ok := m.list.SelectedItem().(utils.SimpleItem) + if ok { + if i.Activate != nil { + newModel, cmd := i.Activate(msg, m) + if newModel != nil || cmd != nil { + if newModel == nil { + newModel = m + } + return newModel, cmd + } + return m, nil + } + } + return m, tea.Quit + default: + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + top, right, bottom, left := lipgloss.NewStyle().Margin(m.root.Height(), 2, 0).GetMargin() + m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom) + m.root.SetSize(msg) + case spinner.TickMsg: + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd + case utils.TickMsg: + select { + case items := <-m.items: + m.list.StopSpinner() + cmd := m.list.SetItems(items) + // Done to refresh keymap + m.list.SetFilteringEnabled(m.list.FilteringEnabled()) + return m, cmd + default: + start := m.list.StartSpinner() + return m, tea.Batch(utils.Ticker(), start) + } + } + + return m, nil +} + +func (m modsList) View() string { + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View()) } diff --git a/tea/scenes/profiles.go b/tea/scenes/profiles.go index 0eeedaf..3cd8434 100644 --- a/tea/scenes/profiles.go +++ b/tea/scenes/profiles.go @@ -1,7 +1,10 @@ package scenes -import tea "github.com/charmbracelet/bubbletea" +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/satisfactorymodding/ficsit-cli/tea/components" +) -func NewProfiles(root RootModel) tea.Model { +func NewProfiles(root components.RootModel, parent tea.Model) tea.Model { return nil } diff --git a/tea/scenes/select_mod_version.go b/tea/scenes/select_mod_version.go new file mode 100644 index 0000000..8dd12c0 --- /dev/null +++ b/tea/scenes/select_mod_version.go @@ -0,0 +1,155 @@ +package scenes + +import ( + "context" + "fmt" + + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/davecgh/go-spew/spew" + "github.com/rs/zerolog/log" + "github.com/satisfactorymodding/ficsit-cli/ficsit" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*selectModVersionList)(nil) + +type selectModVersionList struct { + root components.RootModel + list list.Model + parent tea.Model + items chan []list.Item +} + +func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mod) tea.Model { + l := list.NewModel([]list.Item{}, utils.ItemDelegate{}, root.Size().Width, root.Size().Height-root.Height()) + l.SetShowStatusBar(true) + l.SetFilteringEnabled(false) + l.SetSpinner(spinner.MiniDot) + l.Title = fmt.Sprintf("Versions (%s)", mod.Name) + l.Styles = utils.ListStyles + l.SetSize(l.Width(), l.Height()) + l.KeyMap.Quit.SetHelp("q", "back") + + m := &selectModVersionList{ + root: root, + list: l, + parent: parent, + items: make(chan []list.Item), + } + + go func() { + items := make([]list.Item, 0) + allVersions := make([]ficsit.ModVersionsGetModVersionsVersion, 0) + offset := 0 + for { + versions, err := ficsit.ModVersions(context.TODO(), root.GetAPIClient(), mod.ID, ficsit.VersionFilter{ + Limit: 100, + Offset: offset, + Order: ficsit.OrderDesc, + Order_by: ficsit.VersionFieldsCreatedAt, + }) + + if err != nil { + panic(err) // TODO + } + + if len(versions.GetMod.Versions) == 0 { + break + } + + allVersions = append(allVersions, versions.GetMod.Versions...) + + for i := 0; i < len(versions.GetMod.Versions); i++ { + currentOffset := offset + currentI := i + items = append(items, utils.SimpleItem{ + Title: versions.GetMod.Versions[i].Version, + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + version := allVersions[currentOffset+currentI] + err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version) + if err != nil { + panic(err) // TODO + } + return currentModel.(selectModVersionList).parent, nil + }, + }) + } + + offset += len(versions.GetMod.Versions) + } + + m.items <- items + }() + + return m +} + +func (m selectModVersionList) Init() tea.Cmd { + return utils.Ticker() +} + +func (m selectModVersionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Info().Msg(spew.Sdump(msg)) + switch msg := msg.(type) { + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case KeyControlC: + return m, tea.Quit + case "q": + if m.parent != nil { + m.parent.Update(m.root.Size()) + return m.parent, nil + } + return m, tea.Quit + case KeyEnter: + i, ok := m.list.SelectedItem().(utils.SimpleItem) + if ok { + if i.Activate != nil { + newModel, cmd := i.Activate(msg, m) + if newModel != nil || cmd != nil { + if newModel == nil { + newModel = m + } + return newModel, cmd + } + return m, nil + } + } + return m, tea.Quit + default: + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + top, right, bottom, left := lipgloss.NewStyle().Margin(m.root.Height(), 2, 0).GetMargin() + m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom) + m.root.SetSize(msg) + case spinner.TickMsg: + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd + case utils.TickMsg: + select { + case items := <-m.items: + m.list.StopSpinner() + cmd := m.list.SetItems(items) + // Done to refresh keymap + m.list.SetFilteringEnabled(m.list.FilteringEnabled()) + return m, cmd + default: + start := m.list.StartSpinner() + return m, tea.Batch(utils.Ticker(), start) + } + } + + return m, nil +} + +func (m selectModVersionList) View() string { + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View()) +} diff --git a/tea/scenes/types.go b/tea/scenes/types.go deleted file mode 100644 index ab18300..0000000 --- a/tea/scenes/types.go +++ /dev/null @@ -1,16 +0,0 @@ -package scenes - -import ( - tea "github.com/charmbracelet/bubbletea" - "github.com/satisfactorymodding/ficsit-cli/cli" -) - -type RootModel interface { - ChangeScene(model tea.Model) - - GetCurrentProfile() *cli.Profile - SetCurrentProfile(profile *cli.Profile) - - GetCurrentInstallation() *cli.Installation - SetCurrentInstallation(installation *cli.Installation) -} diff --git a/tea/utils/basic_list.go b/tea/utils/basic_list.go new file mode 100644 index 0000000..f468104 --- /dev/null +++ b/tea/utils/basic_list.go @@ -0,0 +1,51 @@ +package utils + +import ( + "fmt" + "io" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var _ ListItem = (*SimpleItem)(nil) +var _ list.Item = (*SimpleItem)(nil) + +type SimpleItem struct { + Title string + Activate func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) +} + +func (n SimpleItem) FilterValue() string { + return n.Title +} + +func (n SimpleItem) GetTitle() string { + return n.Title +} + +type ListItem interface { + GetTitle() string +} + +type ItemDelegate struct{} + +func (d ItemDelegate) Height() int { return 1 } +func (d ItemDelegate) Spacing() int { return 0 } +func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } +func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(ListItem) + if !ok { + return + } + + style := lipgloss.NewStyle().PaddingLeft(2) + + str := style.Render("o " + i.GetTitle()) + if index == m.Index() { + str = style.Foreground(lipgloss.Color("202")).Render("• " + i.GetTitle()) + } + + fmt.Fprint(w, str) +} diff --git a/tea/utils/styles.go b/tea/utils/styles.go new file mode 100644 index 0000000..2fbe83d --- /dev/null +++ b/tea/utils/styles.go @@ -0,0 +1,19 @@ +package utils + +import ( + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/lipgloss" +) + +var ( + ListStyles list.Styles + LabelStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("202")) + TitleStyle = list.DefaultStyles().Title.Background(lipgloss.Color("22")) +) + +func init() { + ListStyles = list.DefaultStyles() + ListStyles.Title = TitleStyle + ListStyles.HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(2).PaddingBottom(1) + ListStyles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(2) +} diff --git a/tea/utils/tick.go b/tea/utils/tick.go new file mode 100644 index 0000000..779de3c --- /dev/null +++ b/tea/utils/tick.go @@ -0,0 +1,15 @@ +package utils + +import ( + "time" + + tea "github.com/charmbracelet/bubbletea" +) + +type TickMsg struct{} + +func Ticker() tea.Cmd { + return tea.Tick(time.Millisecond*50, func(time.Time) tea.Msg { + return TickMsg{} + }) +} diff --git a/tea/utils/types.go b/tea/utils/types.go new file mode 100644 index 0000000..97faf40 --- /dev/null +++ b/tea/utils/types.go @@ -0,0 +1,7 @@ +package utils + +type Mod struct { + Name string + ID string + Reference string +} diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..ef095b2 --- /dev/null +++ b/tools.go @@ -0,0 +1,8 @@ +//go:build tools +// +build tools + +package smr + +import _ "github.com/Khan/genqlient" + +//go:generate go run github.com/Khan/genqlient diff --git a/utils/version.go b/utils/version.go new file mode 100644 index 0000000..c209acd --- /dev/null +++ b/utils/version.go @@ -0,0 +1,5 @@ +package utils + +import "regexp" + +var SemVerRegex = regexp.MustCompile(`^(<=|<|>|>=|\^)?(0|[1-9]\d*)\.(0|[1-9]d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)