installed mods, versioning, async mods

This commit is contained in:
Vilsol 2022-06-07 02:55:26 +03:00
parent a5e08cea62
commit 9d7b5730a2
16 changed files with 617 additions and 259 deletions

View file

@ -60,11 +60,16 @@ var rootCmd = &cobra.Command{
log.Logger = zerolog.New(io.MultiWriter(writers...)).With().Timestamp().Logger() log.Logger = zerolog.New(io.MultiWriter(writers...)).With().Timestamp().Logger()
log.Info().
Str("version", viper.GetString("version")).
Str("commit", viper.GetString("commit")).
Msg("initialized")
return nil return nil
}, },
} }
func Execute() { func Execute(version string, commit string) {
// Execute tea as default // Execute tea as default
cmd, _, err := rootCmd.Find(os.Args[1:]) cmd, _, err := rootCmd.Find(os.Args[1:])
@ -83,6 +88,9 @@ func Execute() {
viper.Set("quiet", true) viper.Set("quiet", true)
} }
viper.Set("version", version)
viper.Set("commit", commit)
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
panic(err) panic(err)
} }

View file

@ -29,8 +29,8 @@ func TestMods(t *testing.T) {
response, err := Mods(context.Background(), client, ModFilter{}) response, err := Mods(context.Background(), client, ModFilter{})
testza.AssertNoError(t, err) testza.AssertNoError(t, err)
testza.AssertNotNil(t, response) testza.AssertNotNil(t, response)
testza.AssertNotNil(t, response.GetMods) testza.AssertNotNil(t, response.Mods)
testza.AssertNotNil(t, response.GetMods.Mods) testza.AssertNotNil(t, response.Mods.Mods)
testza.AssertNotZero(t, response.GetMods.Count) testza.AssertNotZero(t, response.Mods.Count)
testza.AssertNotZero(t, len(response.GetMods.Mods)) testza.AssertNotZero(t, len(response.Mods.Mods))
} }

View file

@ -1,5 +1,5 @@
query GetMod ($modId: ModID!) { query GetMod ($modId: String!) {
getMod(modId: $modId) { mod: getModByIdOrReference(modIdOrReference: $modId) {
id id
mod_reference mod_reference
name name

View file

@ -1,6 +1,6 @@
# @genqlient(omitempty: true) # @genqlient(omitempty: true)
query Mods ($filter: ModFilter) { query Mods ($filter: ModFilter) {
getMods (filter: $filter) { mods: getMods (filter: $filter) {
count count
mods { mods {
id id

View file

@ -12,58 +12,58 @@ import (
"github.com/satisfactorymodding/ficsit-cli/ficsit/utils" "github.com/satisfactorymodding/ficsit-cli/ficsit/utils"
) )
// GetModGetMod includes the requested fields of the GraphQL type Mod. // GetModMod includes the requested fields of the GraphQL type Mod.
type GetModGetMod struct { type GetModMod struct {
Id string `json:"id"` Id string `json:"id"`
Mod_reference string `json:"mod_reference"` Mod_reference string `json:"mod_reference"`
Name string `json:"name"` Name string `json:"name"`
Views int `json:"views"` Views int `json:"views"`
Downloads int `json:"downloads"` Downloads int `json:"downloads"`
Authors []GetModGetModAuthorsUserMod `json:"authors"` Authors []GetModModAuthorsUserMod `json:"authors"`
Full_description string `json:"full_description"` Full_description string `json:"full_description"`
Source_url string `json:"source_url"` Source_url string `json:"source_url"`
Created_at time.Time `json:"-"` Created_at time.Time `json:"-"`
} }
// GetId returns GetModGetMod.Id, and is useful for accessing the field via an interface. // GetId returns GetModMod.Id, and is useful for accessing the field via an interface.
func (v *GetModGetMod) GetId() string { return v.Id } func (v *GetModMod) GetId() string { return v.Id }
// GetMod_reference returns GetModGetMod.Mod_reference, and is useful for accessing the field via an interface. // GetMod_reference returns GetModMod.Mod_reference, and is useful for accessing the field via an interface.
func (v *GetModGetMod) GetMod_reference() string { return v.Mod_reference } func (v *GetModMod) GetMod_reference() string { return v.Mod_reference }
// GetName returns GetModGetMod.Name, and is useful for accessing the field via an interface. // GetName returns GetModMod.Name, and is useful for accessing the field via an interface.
func (v *GetModGetMod) GetName() string { return v.Name } func (v *GetModMod) GetName() string { return v.Name }
// GetViews returns GetModGetMod.Views, and is useful for accessing the field via an interface. // GetViews returns GetModMod.Views, and is useful for accessing the field via an interface.
func (v *GetModGetMod) GetViews() int { return v.Views } func (v *GetModMod) GetViews() int { return v.Views }
// GetDownloads returns GetModGetMod.Downloads, and is useful for accessing the field via an interface. // GetDownloads returns GetModMod.Downloads, and is useful for accessing the field via an interface.
func (v *GetModGetMod) GetDownloads() int { return v.Downloads } func (v *GetModMod) GetDownloads() int { return v.Downloads }
// GetAuthors returns GetModGetMod.Authors, and is useful for accessing the field via an interface. // GetAuthors returns GetModMod.Authors, and is useful for accessing the field via an interface.
func (v *GetModGetMod) GetAuthors() []GetModGetModAuthorsUserMod { return v.Authors } func (v *GetModMod) GetAuthors() []GetModModAuthorsUserMod { return v.Authors }
// GetFull_description returns GetModGetMod.Full_description, and is useful for accessing the field via an interface. // GetFull_description returns GetModMod.Full_description, and is useful for accessing the field via an interface.
func (v *GetModGetMod) GetFull_description() string { return v.Full_description } func (v *GetModMod) GetFull_description() string { return v.Full_description }
// GetSource_url returns GetModGetMod.Source_url, and is useful for accessing the field via an interface. // GetSource_url returns GetModMod.Source_url, and is useful for accessing the field via an interface.
func (v *GetModGetMod) GetSource_url() string { return v.Source_url } func (v *GetModMod) GetSource_url() string { return v.Source_url }
// GetCreated_at returns GetModGetMod.Created_at, and is useful for accessing the field via an interface. // GetCreated_at returns GetModMod.Created_at, and is useful for accessing the field via an interface.
func (v *GetModGetMod) GetCreated_at() time.Time { return v.Created_at } func (v *GetModMod) GetCreated_at() time.Time { return v.Created_at }
func (v *GetModGetMod) UnmarshalJSON(b []byte) error { func (v *GetModMod) UnmarshalJSON(b []byte) error {
if string(b) == "null" { if string(b) == "null" {
return nil return nil
} }
var firstPass struct { var firstPass struct {
*GetModGetMod *GetModMod
Created_at json.RawMessage `json:"created_at"` Created_at json.RawMessage `json:"created_at"`
graphql.NoUnmarshalJSON graphql.NoUnmarshalJSON
} }
firstPass.GetModGetMod = v firstPass.GetModMod = v
err := json.Unmarshal(b, &firstPass) err := json.Unmarshal(b, &firstPass)
if err != nil { if err != nil {
@ -78,14 +78,14 @@ func (v *GetModGetMod) UnmarshalJSON(b []byte) error {
src, dst) src, dst)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf(
"Unable to unmarshal GetModGetMod.Created_at: %w", err) "Unable to unmarshal GetModMod.Created_at: %w", err)
} }
} }
} }
return nil return nil
} }
type __premarshalGetModGetMod struct { type __premarshalGetModMod struct {
Id string `json:"id"` Id string `json:"id"`
Mod_reference string `json:"mod_reference"` Mod_reference string `json:"mod_reference"`
@ -96,7 +96,7 @@ type __premarshalGetModGetMod struct {
Downloads int `json:"downloads"` Downloads int `json:"downloads"`
Authors []GetModGetModAuthorsUserMod `json:"authors"` Authors []GetModModAuthorsUserMod `json:"authors"`
Full_description string `json:"full_description"` Full_description string `json:"full_description"`
@ -105,7 +105,7 @@ type __premarshalGetModGetMod struct {
Created_at json.RawMessage `json:"created_at"` Created_at json.RawMessage `json:"created_at"`
} }
func (v *GetModGetMod) MarshalJSON() ([]byte, error) { func (v *GetModMod) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON() premarshaled, err := v.__premarshalJSON()
if err != nil { if err != nil {
return nil, err return nil, err
@ -113,8 +113,8 @@ func (v *GetModGetMod) MarshalJSON() ([]byte, error) {
return json.Marshal(premarshaled) return json.Marshal(premarshaled)
} }
func (v *GetModGetMod) __premarshalJSON() (*__premarshalGetModGetMod, error) { func (v *GetModMod) __premarshalJSON() (*__premarshalGetModMod, error) {
var retval __premarshalGetModGetMod var retval __premarshalGetModMod
retval.Id = v.Id retval.Id = v.Id
retval.Mod_reference = v.Mod_reference retval.Mod_reference = v.Mod_reference
@ -133,39 +133,39 @@ func (v *GetModGetMod) __premarshalJSON() (*__premarshalGetModGetMod, error) {
&src) &src)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"Unable to marshal GetModGetMod.Created_at: %w", err) "Unable to marshal GetModMod.Created_at: %w", err)
} }
} }
return &retval, nil return &retval, nil
} }
// GetModGetModAuthorsUserMod includes the requested fields of the GraphQL type UserMod. // GetModModAuthorsUserMod includes the requested fields of the GraphQL type UserMod.
type GetModGetModAuthorsUserMod struct { type GetModModAuthorsUserMod struct {
Role string `json:"role"` Role string `json:"role"`
User GetModGetModAuthorsUserModUser `json:"user"` User GetModModAuthorsUserModUser `json:"user"`
} }
// GetRole returns GetModGetModAuthorsUserMod.Role, and is useful for accessing the field via an interface. // GetRole returns GetModModAuthorsUserMod.Role, and is useful for accessing the field via an interface.
func (v *GetModGetModAuthorsUserMod) GetRole() string { return v.Role } func (v *GetModModAuthorsUserMod) GetRole() string { return v.Role }
// GetUser returns GetModGetModAuthorsUserMod.User, and is useful for accessing the field via an interface. // GetUser returns GetModModAuthorsUserMod.User, and is useful for accessing the field via an interface.
func (v *GetModGetModAuthorsUserMod) GetUser() GetModGetModAuthorsUserModUser { return v.User } func (v *GetModModAuthorsUserMod) GetUser() GetModModAuthorsUserModUser { return v.User }
// GetModGetModAuthorsUserModUser includes the requested fields of the GraphQL type User. // GetModModAuthorsUserModUser includes the requested fields of the GraphQL type User.
type GetModGetModAuthorsUserModUser struct { type GetModModAuthorsUserModUser struct {
Username string `json:"username"` Username string `json:"username"`
} }
// GetUsername returns GetModGetModAuthorsUserModUser.Username, and is useful for accessing the field via an interface. // GetUsername returns GetModModAuthorsUserModUser.Username, and is useful for accessing the field via an interface.
func (v *GetModGetModAuthorsUserModUser) GetUsername() string { return v.Username } func (v *GetModModAuthorsUserModUser) GetUsername() string { return v.Username }
// GetModResponse is returned by GetMod on success. // GetModResponse is returned by GetMod on success.
type GetModResponse struct { type GetModResponse struct {
GetMod GetModGetMod `json:"getMod"` Mod GetModMod `json:"mod"`
} }
// GetGetMod returns GetModResponse.GetMod, and is useful for accessing the field via an interface. // GetMod returns GetModResponse.Mod, and is useful for accessing the field via an interface.
func (v *GetModResponse) GetGetMod() GetModGetMod { return v.GetMod } func (v *GetModResponse) GetMod() GetModMod { return v.Mod }
type ModFields string type ModFields string
@ -259,20 +259,20 @@ type ModVersionsResponse struct {
// GetMod returns ModVersionsResponse.Mod, and is useful for accessing the field via an interface. // GetMod returns ModVersionsResponse.Mod, and is useful for accessing the field via an interface.
func (v *ModVersionsResponse) GetMod() ModVersionsMod { return v.Mod } func (v *ModVersionsResponse) GetMod() ModVersionsMod { return v.Mod }
// ModsGetMods includes the requested fields of the GraphQL type GetMods. // ModsModsGetMods includes the requested fields of the GraphQL type GetMods.
type ModsGetMods struct { type ModsModsGetMods struct {
Count int `json:"count"` Count int `json:"count"`
Mods []ModsGetModsModsMod `json:"mods"` Mods []ModsModsGetModsModsMod `json:"mods"`
} }
// GetCount returns ModsGetMods.Count, and is useful for accessing the field via an interface. // GetCount returns ModsModsGetMods.Count, and is useful for accessing the field via an interface.
func (v *ModsGetMods) GetCount() int { return v.Count } func (v *ModsModsGetMods) GetCount() int { return v.Count }
// GetMods returns ModsGetMods.Mods, and is useful for accessing the field via an interface. // GetMods returns ModsModsGetMods.Mods, and is useful for accessing the field via an interface.
func (v *ModsGetMods) GetMods() []ModsGetModsModsMod { return v.Mods } func (v *ModsModsGetMods) GetMods() []ModsModsGetModsModsMod { return v.Mods }
// ModsGetModsModsMod includes the requested fields of the GraphQL type Mod. // ModsModsGetModsModsMod includes the requested fields of the GraphQL type Mod.
type ModsGetModsModsMod struct { type ModsModsGetModsModsMod struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Mod_reference string `json:"mod_reference"` Mod_reference string `json:"mod_reference"`
@ -284,46 +284,46 @@ type ModsGetModsModsMod struct {
Hotness int `json:"hotness"` Hotness int `json:"hotness"`
} }
// GetId returns ModsGetModsModsMod.Id, and is useful for accessing the field via an interface. // GetId returns ModsModsGetModsModsMod.Id, and is useful for accessing the field via an interface.
func (v *ModsGetModsModsMod) GetId() string { return v.Id } func (v *ModsModsGetModsModsMod) GetId() string { return v.Id }
// GetName returns ModsGetModsModsMod.Name, and is useful for accessing the field via an interface. // GetName returns ModsModsGetModsModsMod.Name, and is useful for accessing the field via an interface.
func (v *ModsGetModsModsMod) GetName() string { return v.Name } func (v *ModsModsGetModsModsMod) GetName() string { return v.Name }
// GetMod_reference returns ModsGetModsModsMod.Mod_reference, and is useful for accessing the field via an interface. // GetMod_reference returns ModsModsGetModsModsMod.Mod_reference, and is useful for accessing the field via an interface.
func (v *ModsGetModsModsMod) GetMod_reference() string { return v.Mod_reference } func (v *ModsModsGetModsModsMod) GetMod_reference() string { return v.Mod_reference }
// GetLast_version_date returns ModsGetModsModsMod.Last_version_date, and is useful for accessing the field via an interface. // GetLast_version_date returns ModsModsGetModsModsMod.Last_version_date, and is useful for accessing the field via an interface.
func (v *ModsGetModsModsMod) GetLast_version_date() time.Time { return v.Last_version_date } func (v *ModsModsGetModsModsMod) GetLast_version_date() time.Time { return v.Last_version_date }
// GetCreated_at returns ModsGetModsModsMod.Created_at, and is useful for accessing the field via an interface. // GetCreated_at returns ModsModsGetModsModsMod.Created_at, and is useful for accessing the field via an interface.
func (v *ModsGetModsModsMod) GetCreated_at() time.Time { return v.Created_at } func (v *ModsModsGetModsModsMod) GetCreated_at() time.Time { return v.Created_at }
// GetViews returns ModsGetModsModsMod.Views, and is useful for accessing the field via an interface. // GetViews returns ModsModsGetModsModsMod.Views, and is useful for accessing the field via an interface.
func (v *ModsGetModsModsMod) GetViews() int { return v.Views } func (v *ModsModsGetModsModsMod) GetViews() int { return v.Views }
// GetDownloads returns ModsGetModsModsMod.Downloads, and is useful for accessing the field via an interface. // GetDownloads returns ModsModsGetModsModsMod.Downloads, and is useful for accessing the field via an interface.
func (v *ModsGetModsModsMod) GetDownloads() int { return v.Downloads } func (v *ModsModsGetModsModsMod) GetDownloads() int { return v.Downloads }
// GetPopularity returns ModsGetModsModsMod.Popularity, and is useful for accessing the field via an interface. // GetPopularity returns ModsModsGetModsModsMod.Popularity, and is useful for accessing the field via an interface.
func (v *ModsGetModsModsMod) GetPopularity() int { return v.Popularity } func (v *ModsModsGetModsModsMod) GetPopularity() int { return v.Popularity }
// GetHotness returns ModsGetModsModsMod.Hotness, and is useful for accessing the field via an interface. // GetHotness returns ModsModsGetModsModsMod.Hotness, and is useful for accessing the field via an interface.
func (v *ModsGetModsModsMod) GetHotness() int { return v.Hotness } func (v *ModsModsGetModsModsMod) GetHotness() int { return v.Hotness }
func (v *ModsGetModsModsMod) UnmarshalJSON(b []byte) error { func (v *ModsModsGetModsModsMod) UnmarshalJSON(b []byte) error {
if string(b) == "null" { if string(b) == "null" {
return nil return nil
} }
var firstPass struct { var firstPass struct {
*ModsGetModsModsMod *ModsModsGetModsModsMod
Last_version_date json.RawMessage `json:"last_version_date"` Last_version_date json.RawMessage `json:"last_version_date"`
Created_at json.RawMessage `json:"created_at"` Created_at json.RawMessage `json:"created_at"`
graphql.NoUnmarshalJSON graphql.NoUnmarshalJSON
} }
firstPass.ModsGetModsModsMod = v firstPass.ModsModsGetModsModsMod = v
err := json.Unmarshal(b, &firstPass) err := json.Unmarshal(b, &firstPass)
if err != nil { if err != nil {
@ -338,7 +338,7 @@ func (v *ModsGetModsModsMod) UnmarshalJSON(b []byte) error {
src, dst) src, dst)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf(
"Unable to unmarshal ModsGetModsModsMod.Last_version_date: %w", err) "Unable to unmarshal ModsModsGetModsModsMod.Last_version_date: %w", err)
} }
} }
} }
@ -351,14 +351,14 @@ func (v *ModsGetModsModsMod) UnmarshalJSON(b []byte) error {
src, dst) src, dst)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf(
"Unable to unmarshal ModsGetModsModsMod.Created_at: %w", err) "Unable to unmarshal ModsModsGetModsModsMod.Created_at: %w", err)
} }
} }
} }
return nil return nil
} }
type __premarshalModsGetModsModsMod struct { type __premarshalModsModsGetModsModsMod struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -378,7 +378,7 @@ type __premarshalModsGetModsModsMod struct {
Hotness int `json:"hotness"` Hotness int `json:"hotness"`
} }
func (v *ModsGetModsModsMod) MarshalJSON() ([]byte, error) { func (v *ModsModsGetModsModsMod) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON() premarshaled, err := v.__premarshalJSON()
if err != nil { if err != nil {
return nil, err return nil, err
@ -386,8 +386,8 @@ func (v *ModsGetModsModsMod) MarshalJSON() ([]byte, error) {
return json.Marshal(premarshaled) return json.Marshal(premarshaled)
} }
func (v *ModsGetModsModsMod) __premarshalJSON() (*__premarshalModsGetModsModsMod, error) { func (v *ModsModsGetModsModsMod) __premarshalJSON() (*__premarshalModsModsGetModsModsMod, error) {
var retval __premarshalModsGetModsModsMod var retval __premarshalModsModsGetModsModsMod
retval.Id = v.Id retval.Id = v.Id
retval.Name = v.Name retval.Name = v.Name
@ -401,7 +401,7 @@ func (v *ModsGetModsModsMod) __premarshalJSON() (*__premarshalModsGetModsModsMod
&src) &src)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"Unable to marshal ModsGetModsModsMod.Last_version_date: %w", err) "Unable to marshal ModsModsGetModsModsMod.Last_version_date: %w", err)
} }
} }
{ {
@ -413,7 +413,7 @@ func (v *ModsGetModsModsMod) __premarshalJSON() (*__premarshalModsGetModsModsMod
&src) &src)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"Unable to marshal ModsGetModsModsMod.Created_at: %w", err) "Unable to marshal ModsModsGetModsModsMod.Created_at: %w", err)
} }
} }
retval.Views = v.Views retval.Views = v.Views
@ -425,11 +425,11 @@ func (v *ModsGetModsModsMod) __premarshalJSON() (*__premarshalModsGetModsModsMod
// ModsResponse is returned by Mods on success. // ModsResponse is returned by Mods on success.
type ModsResponse struct { type ModsResponse struct {
GetMods ModsGetMods `json:"getMods"` Mods ModsModsGetMods `json:"mods"`
} }
// GetGetMods returns ModsResponse.GetMods, and is useful for accessing the field via an interface. // GetMods returns ModsResponse.Mods, and is useful for accessing the field via an interface.
func (v *ModsResponse) GetGetMods() ModsGetMods { return v.GetMods } func (v *ModsResponse) GetMods() ModsModsGetMods { return v.Mods }
type Order string type Order string
@ -644,8 +644,8 @@ func GetMod(
ctx, ctx,
"GetMod", "GetMod",
` `
query GetMod ($modId: ModID!) { query GetMod ($modId: String!) {
getMod(modId: $modId) { mod: getModByIdOrReference(modIdOrReference: $modId) {
id id
mod_reference mod_reference
name name
@ -718,7 +718,7 @@ func Mods(
"Mods", "Mods",
` `
query Mods ($filter: ModFilter) { query Mods ($filter: ModFilter) {
getMods(filter: $filter) { mods: getMods(filter: $filter) {
count count
mods { mods {
id id

4
go.mod
View file

@ -12,9 +12,11 @@ require (
github.com/charmbracelet/bubbletea v0.21.0 github.com/charmbracelet/bubbletea v0.21.0
github.com/charmbracelet/glamour v0.5.0 github.com/charmbracelet/glamour v0.5.0
github.com/charmbracelet/lipgloss v0.5.0 github.com/charmbracelet/lipgloss v0.5.0
github.com/muesli/reflow v0.3.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pterm/pterm v0.12.41 github.com/pterm/pterm v0.12.41
github.com/rs/zerolog v1.26.1 github.com/rs/zerolog v1.26.1
github.com/sahilm/fuzzy v0.1.0
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.12.0 github.com/spf13/viper v1.12.0
) )
@ -48,13 +50,11 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.0 // indirect github.com/muesli/cancelreader v0.2.0 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/afero v1.8.2 // indirect github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect

View file

@ -2,6 +2,11 @@ package main
import "github.com/satisfactorymodding/ficsit-cli/cmd" import "github.com/satisfactorymodding/ficsit-cli/cmd"
var (
version = "dev"
commit = "none"
)
func main() { func main() {
cmd.Execute() cmd.Execute(version, commit)
} }

View file

@ -43,7 +43,7 @@ func (m *rootModel) SetCurrentProfile(profile *cli.Profile) error {
return errors.Wrap(err, "failed setting profile on installation") return errors.Wrap(err, "failed setting profile on installation")
} }
return m.global.Save() return nil
} }
func (m *rootModel) GetCurrentInstallation() *cli.Installation { func (m *rootModel) GetCurrentInstallation() *cli.Installation {
@ -53,7 +53,7 @@ func (m *rootModel) GetCurrentInstallation() *cli.Installation {
func (m *rootModel) SetCurrentInstallation(installation *cli.Installation) error { func (m *rootModel) SetCurrentInstallation(installation *cli.Installation) error {
m.global.Installations.SelectedInstallation = installation.Path m.global.Installations.SelectedInstallation = installation.Path
m.global.Profiles.SelectedProfile = installation.Profile m.global.Profiles.SelectedProfile = installation.Profile
return m.global.Save() return nil
} }
func (m *rootModel) GetAPIClient() graphql.Client { func (m *rootModel) GetAPIClient() graphql.Client {

View file

@ -0,0 +1,219 @@
package scenes
import (
"context"
"sort"
"time"
"github.com/charmbracelet/bubbles/key"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
)
var _ tea.Model = (*installedModsList)(nil)
type installedModsList struct {
root components.RootModel
list list.Model
parent tea.Model
items chan []list.Item
err chan string
error *components.ErrorComponent
}
func NewInstalledMods(root components.RootModel, parent tea.Model) tea.Model {
currentProfile := root.GetCurrentProfile()
if currentProfile == nil {
return parent
}
items := make([]list.Item, len(currentProfile.Mods))
i := 0
for reference := range currentProfile.Mods {
r := reference
items[i] = utils.SimpleItem[installedModsList]{
ItemTitle: reference,
Activate: func(msg tea.Msg, currentModel installedModsList) (tea.Model, tea.Cmd) {
return NewModMenu(root, currentModel, utils.Mod{
Name: r,
Reference: r,
}), nil
},
}
i++
}
sort.Slice(items, func(i, j int) bool {
a := items[i].(utils.SimpleItem[installedModsList])
b := items[j].(utils.SimpleItem[installedModsList])
return ascDesc(sortOrderDesc, a.ItemTitle < b.ItemTitle)
})
l := list.New(items, utils.NewItemDelegate(), root.Size().Width, root.Size().Height-root.Height())
l.SetShowStatusBar(true)
l.SetShowFilter(true)
l.SetFilteringEnabled(true)
l.SetSpinner(spinner.MiniDot)
l.Title = "Installed Mods"
l.Styles = utils.ListStyles
l.SetSize(l.Width(), l.Height())
l.KeyMap.Quit.SetHelp("q", "back")
l.DisableQuitKeybindings()
l.AdditionalShortHelpKeys = func() []key.Binding {
return []key.Binding{
key.NewBinding(key.WithHelp("q", "back")),
}
}
l.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
key.NewBinding(key.WithHelp("q", "back")),
}
}
m := &installedModsList{
root: root,
list: l,
parent: parent,
items: make(chan []list.Item),
err: make(chan string),
}
go func() {
references := make([]string, len(currentProfile.Mods))
i := 0
for reference := range currentProfile.Mods {
references[i] = reference
i++
}
mods, err := ficsit.Mods(context.TODO(), root.GetAPIClient(), ficsit.ModFilter{
References: references,
})
if err != nil {
m.err <- err.Error()
return
}
if len(mods.Mods.Mods) == 0 {
return
}
items := make([]list.Item, len(mods.Mods.Mods))
for i, mod := range mods.Mods.Mods {
// Re-reference struct
mod := mod
items[i] = utils.SimpleItemExtra[installedModsList, ficsit.ModsModsGetModsModsMod]{
SimpleItem: utils.SimpleItem[installedModsList]{
ItemTitle: mods.Mods.Mods[i].Name,
Activate: func(msg tea.Msg, currentModel installedModsList) (tea.Model, tea.Cmd) {
return NewModMenu(root, currentModel, utils.Mod{
Name: mod.Name,
Reference: mod.Mod_reference,
}), nil
},
},
Extra: mod,
}
}
sort.Slice(items, func(i, j int) bool {
a := items[i].(utils.SimpleItemExtra[installedModsList, ficsit.ModsModsGetModsModsMod])
b := items[j].(utils.SimpleItemExtra[installedModsList, ficsit.ModsModsGetModsModsMod])
return ascDesc(sortOrderDesc, a.Extra.Mod_reference < b.Extra.Mod_reference)
})
m.items <- items
}()
return m
}
func (m installedModsList) Init() tea.Cmd {
return utils.Ticker()
}
func (m installedModsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// List enables its own keybindings when they were previously disabled
m.list.DisableQuitKeybindings()
switch msg := msg.(type) {
case tea.KeyMsg:
if m.list.SettingFilter() {
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
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[installedModsList])
if ok {
return m.processActivation(i, msg)
}
i2, ok := m.list.SelectedItem().(utils.SimpleItemExtra[installedModsList, ficsit.ModsModsGetModsModsMod])
if ok {
return m.processActivation(i2.SimpleItem, msg)
}
return m, nil
}
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 utils.TickMsg:
select {
case items := <-m.items:
cmd := m.list.SetItems(items)
m.list.StopSpinner()
return m, cmd
case err := <-m.err:
errorComponent, cmd := components.NewErrorComponent(err, time.Second*5)
m.error = errorComponent
return m, cmd
default:
start := m.list.StartSpinner()
return m, tea.Batch(utils.Ticker(), start)
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m installedModsList) View() string {
m.list.SetSize(m.list.Width(), m.root.Size().Height-m.root.Height())
return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View())
}
func (m installedModsList) processActivation(item utils.SimpleItem[installedModsList], msg tea.Msg) (tea.Model, tea.Cmd) {
if item.Activate != nil {
newModel, cmd := item.Activate(msg, m)
if newModel != nil || cmd != nil {
if newModel == nil {
newModel = m
}
return newModel, cmd
}
return m, nil
}
return m, nil
}

View file

@ -10,6 +10,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils" "github.com/satisfactorymodding/ficsit-cli/tea/utils"
"github.com/spf13/viper"
) )
var _ tea.Model = (*mainMenu)(nil) var _ tea.Model = (*mainMenu)(nil)
@ -73,12 +74,19 @@ func NewMainMenu(root components.RootModel) tea.Model {
}, },
}, },
utils.SimpleItem[mainMenu]{ utils.SimpleItem[mainMenu]{
ItemTitle: "Mods", ItemTitle: "All Mods",
Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) { Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) {
newModel := NewMods(root, currentModel) newModel := NewMods(root, currentModel)
return newModel, newModel.Init() return newModel, newModel.Init()
}, },
}, },
utils.SimpleItem[mainMenu]{
ItemTitle: "Installed Mods",
Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) {
newModel := NewInstalledMods(root, currentModel)
return newModel, newModel.Init()
},
},
utils.SimpleItem[mainMenu]{ utils.SimpleItem[mainMenu]{
ItemTitle: "Apply Changes", ItemTitle: "Apply Changes",
Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) { Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) {
@ -170,9 +178,21 @@ func (m mainMenu) View() string {
header := m.root.View() header := m.root.View()
banner := lipgloss.NewStyle().Margin(2, 0, 0, 2).Render(m.banner) banner := lipgloss.NewStyle().Margin(2, 0, 0, 2).Render(m.banner)
totalHeight := m.root.Height() + len(m.list.Items()) + lipgloss.Height(banner) + 4
commit := viper.GetString("commit")
if len(commit) > 8 {
commit = commit[:8]
}
version := "\n"
version += utils.LabelStyle.Render("Version: ")
version += viper.GetString("version") + " - " + commit
header = lipgloss.JoinVertical(lipgloss.Left, version, header)
totalHeight := lipgloss.Height(header) + len(m.list.Items()) + lipgloss.Height(banner) + 5
if totalHeight < m.root.Size().Height { if totalHeight < m.root.Size().Height {
header = lipgloss.JoinVertical(lipgloss.Left, banner, m.root.View()) header = lipgloss.JoinVertical(lipgloss.Left, banner, header)
} }
if m.error != nil { if m.error != nil {

View file

@ -30,7 +30,7 @@ type modInfo struct {
viewport viewport.Model viewport viewport.Model
spinner spinner.Model spinner spinner.Model
parent tea.Model parent tea.Model
modData chan ficsit.GetModGetMod modData chan ficsit.GetModMod
modError chan string modError chan string
ready bool ready bool
help help.Model help help.Model
@ -67,7 +67,7 @@ func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea.
viewport: viewport.Model{}, viewport: viewport.Model{},
spinner: spinner.New(), spinner: spinner.New(),
parent: parent, parent: parent,
modData: make(chan ficsit.GetModGetMod), modData: make(chan ficsit.GetModMod),
modError: make(chan string), modError: make(chan string),
ready: false, ready: false,
help: help.New(), help: help.New(),
@ -87,7 +87,7 @@ func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea.
model.help.Width = root.Size().Width model.help.Width = root.Size().Width
go func() { go func() {
fullMod, err := ficsit.GetMod(context.TODO(), root.GetAPIClient(), mod.ID) fullMod, err := ficsit.GetMod(context.TODO(), root.GetAPIClient(), mod.Reference)
if err != nil { if err != nil {
model.modError <- err.Error() model.modError <- err.Error()
@ -99,7 +99,7 @@ func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea.
return return
} }
model.modData <- fullMod.GetMod model.modData <- fullMod.Mod
}() }()
return model return model

View file

@ -2,17 +2,18 @@ package scenes
import ( import (
"context" "context"
"fmt"
"io"
"sort" "sort"
"time" "time"
"github.com/satisfactorymodding/ficsit-cli/cli"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/truncate"
"github.com/satisfactorymodding/ficsit-cli/cli"
"github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils" "github.com/satisfactorymodding/ficsit-cli/tea/utils"
@ -29,11 +30,16 @@ const (
const modsTitle = "Mods" const modsTitle = "Mods"
type listUpdate struct {
Items []list.Item
Done bool
}
type modsList struct { type modsList struct {
root components.RootModel root components.RootModel
list list.Model list list.Model
parent tea.Model parent tea.Model
items chan []list.Item items chan listUpdate
sortingField string sortingField string
sortingOrder sortOrder sortingOrder sortOrder
@ -48,29 +54,11 @@ type modsList struct {
error *components.ErrorComponent error *components.ErrorComponent
} }
var _ list.DefaultItem = (*SimpleItemMod[tea.Model])(nil)
type SimpleItemMod[T tea.Model] struct {
utils.SimpleItem[T]
Mod ficsit.ModsGetModsModsMod
Context *cli.GlobalContext
}
func (n SimpleItemMod[any]) Title() string {
if n.Context != nil {
profile := n.Context.Profiles.Profiles[n.Context.Profiles.SelectedProfile]
if profile != nil {
if profile.HasMod(n.Mod.Mod_reference) {
return lipgloss.NewStyle().Foreground(lipgloss.Color("40")).Render("✓ " + n.ItemTitle)
}
}
}
return n.ItemTitle
}
func NewMods(root components.RootModel, parent tea.Model) tea.Model { func NewMods(root components.RootModel, parent tea.Model) tea.Model {
l := list.New([]list.Item{}, utils.NewItemDelegate(), root.Size().Width, root.Size().Height-root.Height()) l := list.New([]list.Item{}, ModsListDelegate{
ItemDelegate: utils.NewItemDelegate(),
Context: root.GetGlobal(),
}, root.Size().Width, root.Size().Height-root.Height())
l.SetShowStatusBar(true) l.SetShowStatusBar(true)
l.SetShowFilter(true) l.SetShowFilter(true)
l.SetFilteringEnabled(true) l.SetFilteringEnabled(true)
@ -98,67 +86,81 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
} }
sortFieldList := list.New([]list.Item{ sortFieldList := list.New([]list.Item{
utils.SimpleItem[modsList]{ utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
ItemTitle: "Name", SimpleItem: utils.SimpleItem[modsList]{
Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { ItemTitle: "Name",
m.sortingField = "name" Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) {
cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) m.sortingField = "name"
m.list.ResetSelected() cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder))
return m, cmd m.list.ResetSelected()
return m, cmd
},
}, },
}, },
utils.SimpleItem[modsList]{ utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
ItemTitle: "Last Version Date", SimpleItem: utils.SimpleItem[modsList]{
Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { ItemTitle: "Last Version Date",
m.sortingField = "last_version_date" Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) {
cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) m.sortingField = "last_version_date"
m.list.ResetSelected() cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder))
return m, cmd m.list.ResetSelected()
return m, cmd
},
}, },
}, },
utils.SimpleItem[modsList]{ utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
ItemTitle: "Creation Date", SimpleItem: utils.SimpleItem[modsList]{
Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { ItemTitle: "Creation Date",
m.sortingField = "created_at" Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) {
cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) m.sortingField = "created_at"
m.list.ResetSelected() cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder))
return m, cmd m.list.ResetSelected()
return m, cmd
},
}, },
}, },
utils.SimpleItem[modsList]{ utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
ItemTitle: "Downloads", SimpleItem: utils.SimpleItem[modsList]{
Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { ItemTitle: "Downloads",
m.sortingField = "downloads" Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) {
cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) m.sortingField = "downloads"
m.list.ResetSelected() cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder))
return m, cmd m.list.ResetSelected()
return m, cmd
},
}, },
}, },
utils.SimpleItem[modsList]{ utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
ItemTitle: "Views", SimpleItem: utils.SimpleItem[modsList]{
Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { ItemTitle: "Views",
m.sortingField = "views" Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) {
cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) m.sortingField = "views"
m.list.ResetSelected() cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder))
return m, cmd m.list.ResetSelected()
return m, cmd
},
}, },
}, },
utils.SimpleItem[modsList]{ utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
ItemTitle: "Popularity (recent downloads)", SimpleItem: utils.SimpleItem[modsList]{
Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { ItemTitle: "Popularity (recent downloads)",
m.sortingField = "popularity" Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) {
cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) m.sortingField = "popularity"
m.list.ResetSelected() cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder))
return m, cmd m.list.ResetSelected()
return m, cmd
},
}, },
}, },
utils.SimpleItem[modsList]{ utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
ItemTitle: "Hotness (recent views)", SimpleItem: utils.SimpleItem[modsList]{
Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { ItemTitle: "Hotness (recent views)",
m.sortingField = "hotness" Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) {
cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) m.sortingField = "hotness"
m.list.ResetSelected() cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder))
return m, cmd m.list.ResetSelected()
return m, cmd
},
}, },
}, },
}, utils.NewItemDelegate(), root.Size().Width, root.Size().Height-root.Height()) }, utils.NewItemDelegate(), root.Size().Width, root.Size().Height-root.Height())
@ -172,22 +174,26 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
sortFieldList.DisableQuitKeybindings() sortFieldList.DisableQuitKeybindings()
sortOrderList := list.New([]list.Item{ sortOrderList := list.New([]list.Item{
utils.SimpleItem[modsList]{ utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
ItemTitle: "Ascending", SimpleItem: utils.SimpleItem[modsList]{
Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { ItemTitle: "Ascending",
m.sortingOrder = sortOrderAsc Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) {
cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) m.sortingOrder = sortOrderAsc
m.list.ResetSelected() cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder))
return m, cmd m.list.ResetSelected()
return m, cmd
},
}, },
}, },
utils.SimpleItem[modsList]{ utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
ItemTitle: "Descending",
Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { SimpleItem: utils.SimpleItem[modsList]{ItemTitle: "Descending",
m.sortingOrder = sortOrderDesc Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) {
cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) m.sortingOrder = sortOrderDesc
m.list.ResetSelected() cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder))
return m, cmd m.list.ResetSelected()
return m, cmd
},
}, },
}, },
}, utils.NewItemDelegate(), root.Size().Width, root.Size().Height-root.Height()) }, utils.NewItemDelegate(), root.Size().Width, root.Size().Height-root.Height())
@ -204,7 +210,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
root: root, root: root,
list: l, list: l,
parent: parent, parent: parent,
items: make(chan []list.Item), items: make(chan listUpdate),
sortingField: "last_version_date", sortingField: "last_version_date",
sortingOrder: sortOrderDesc, sortingOrder: sortOrderDesc,
sortFieldList: sortFieldList, sortFieldList: sortFieldList,
@ -214,7 +220,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
go func() { go func() {
items := make([]list.Item, 0) items := make([]list.Item, 0)
allMods := make([]ficsit.ModsGetModsModsMod, 0) allMods := make([]ficsit.ModsModsGetModsModsMod, 0)
offset := 0 offset := 0
for { for {
mods, err := ficsit.Mods(context.TODO(), root.GetAPIClient(), ficsit.ModFilter{ mods, err := ficsit.Mods(context.TODO(), root.GetAPIClient(), ficsit.ModFilter{
@ -229,36 +235,42 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
return return
} }
if len(mods.GetMods.Mods) == 0 { if len(mods.Mods.Mods) == 0 {
break break
} }
allMods = append(allMods, mods.GetMods.Mods...) allMods = append(allMods, mods.Mods.Mods...)
for i := 0; i < len(mods.GetMods.Mods); i++ { for i := 0; i < len(mods.Mods.Mods); i++ {
currentOffset := offset currentOffset := offset
currentI := i currentI := i
items = append(items, SimpleItemMod[modsList]{ items = append(items, utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{
SimpleItem: utils.SimpleItem[modsList]{ SimpleItem: utils.SimpleItem[modsList]{
ItemTitle: mods.GetMods.Mods[i].Name, ItemTitle: mods.Mods.Mods[i].Name,
Activate: func(msg tea.Msg, currentModel modsList) (tea.Model, tea.Cmd) { Activate: func(msg tea.Msg, currentModel modsList) (tea.Model, tea.Cmd) {
mod := allMods[currentOffset+currentI] mod := allMods[currentOffset+currentI]
return NewModMenu(root, currentModel, utils.Mod{ return NewModMenu(root, currentModel, utils.Mod{
Name: mod.Name, Name: mod.Name,
ID: mod.Id,
Reference: mod.Mod_reference, Reference: mod.Mod_reference,
}), nil }), nil
}, },
}, },
Mod: allMods[currentOffset+currentI], Extra: allMods[currentOffset+currentI],
Context: root.GetGlobal(),
}) })
} }
offset += len(mods.GetMods.Mods) offset += len(mods.Mods.Mods)
m.items <- listUpdate{
Items: items,
Done: false,
}
} }
m.items <- items m.items <- listUpdate{
Items: items,
Done: true,
}
}() }()
return m return m
@ -308,7 +320,7 @@ func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case KeyEnter: case KeyEnter:
if m.showSortFieldList { if m.showSortFieldList {
m.showSortFieldList = false m.showSortFieldList = false
i, ok := m.sortFieldList.SelectedItem().(utils.SimpleItem[modsList]) i, ok := m.sortFieldList.SelectedItem().(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
if ok { if ok {
return m.processActivation(i, msg) return m.processActivation(i, msg)
} }
@ -317,16 +329,16 @@ func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.showSortOrderList { if m.showSortOrderList {
m.showSortOrderList = false m.showSortOrderList = false
i, ok := m.sortOrderList.SelectedItem().(utils.SimpleItem[modsList]) i, ok := m.sortOrderList.SelectedItem().(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
if ok { if ok {
return m.processActivation(i, msg) return m.processActivation(i, msg)
} }
return m, nil return m, nil
} }
i, ok := m.list.SelectedItem().(SimpleItemMod[modsList]) i, ok := m.list.SelectedItem().(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
if ok { if ok {
return m.processActivation(i.SimpleItem, msg) return m.processActivation(i, msg)
} }
return m, nil return m, nil
} }
@ -337,9 +349,12 @@ func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case utils.TickMsg: case utils.TickMsg:
select { select {
case items := <-m.items: case items := <-m.items:
m.list.StopSpinner() cmd := m.list.SetItems(items.Items)
cmd := m.list.SetItems(items) if items.Done {
return m, cmd m.list.StopSpinner()
return m, cmd
}
return m, tea.Batch(utils.Ticker(), cmd)
case err := <-m.err: case err := <-m.err:
errorComponent, cmd := components.NewErrorComponent(err, time.Second*5) errorComponent, cmd := components.NewErrorComponent(err, time.Second*5)
m.error = errorComponent m.error = errorComponent
@ -392,52 +407,52 @@ func sortItems(items []list.Item, field string, direction sortOrder) []list.Item
switch field { switch field {
case "last_version_date": case "last_version_date":
sort.Slice(sortedItems, func(i, j int) bool { sort.Slice(sortedItems, func(i, j int) bool {
a := sortedItems[i].(SimpleItemMod[modsList]) a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
b := sortedItems[j].(SimpleItemMod[modsList]) b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
return ascDesc(direction, a.Mod.Last_version_date.Before(b.Mod.Last_version_date)) return ascDesc(direction, a.Extra.Last_version_date.Before(b.Extra.Last_version_date))
}) })
case "created_at": case "created_at":
sort.Slice(sortedItems, func(i, j int) bool { sort.Slice(sortedItems, func(i, j int) bool {
a := sortedItems[i].(SimpleItemMod[modsList]) a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
b := sortedItems[j].(SimpleItemMod[modsList]) b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
return ascDesc(direction, a.Mod.Created_at.Before(b.Mod.Created_at)) return ascDesc(direction, a.Extra.Created_at.Before(b.Extra.Created_at))
}) })
case "name": case "name":
sort.Slice(sortedItems, func(i, j int) bool { sort.Slice(sortedItems, func(i, j int) bool {
a := sortedItems[i].(SimpleItemMod[modsList]) a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
b := sortedItems[j].(SimpleItemMod[modsList]) b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
return ascDesc(direction, a.Mod.Name < b.Mod.Name) return ascDesc(direction, a.Extra.Name < b.Extra.Name)
}) })
case "downloads": case "downloads":
sort.Slice(sortedItems, func(i, j int) bool { sort.Slice(sortedItems, func(i, j int) bool {
a := sortedItems[i].(SimpleItemMod[modsList]) a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
b := sortedItems[j].(SimpleItemMod[modsList]) b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
return ascDesc(direction, a.Mod.Downloads < b.Mod.Downloads) return ascDesc(direction, a.Extra.Downloads < b.Extra.Downloads)
}) })
case "views": case "views":
sort.Slice(sortedItems, func(i, j int) bool { sort.Slice(sortedItems, func(i, j int) bool {
a := sortedItems[i].(SimpleItemMod[modsList]) a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
b := sortedItems[j].(SimpleItemMod[modsList]) b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
return ascDesc(direction, a.Mod.Views < b.Mod.Views) return ascDesc(direction, a.Extra.Views < b.Extra.Views)
}) })
case "popularity": case "popularity":
sort.Slice(sortedItems, func(i, j int) bool { sort.Slice(sortedItems, func(i, j int) bool {
a := sortedItems[i].(SimpleItemMod[modsList]) a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
b := sortedItems[j].(SimpleItemMod[modsList]) b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
return ascDesc(direction, a.Mod.Popularity < b.Mod.Popularity) return ascDesc(direction, a.Extra.Popularity < b.Extra.Popularity)
}) })
case "hotness": case "hotness":
sort.Slice(sortedItems, func(i, j int) bool { sort.Slice(sortedItems, func(i, j int) bool {
a := sortedItems[i].(SimpleItemMod[modsList]) a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
b := sortedItems[j].(SimpleItemMod[modsList]) b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
return ascDesc(direction, a.Mod.Hotness < b.Mod.Hotness) return ascDesc(direction, a.Extra.Hotness < b.Extra.Hotness)
}) })
} }
return sortedItems return sortedItems
} }
func (m modsList) processActivation(item utils.SimpleItem[modsList], msg tea.Msg) (tea.Model, tea.Cmd) { func (m modsList) processActivation(item utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod], msg tea.Msg) (tea.Model, tea.Cmd) {
if item.Activate != nil { if item.Activate != nil {
newModel, cmd := item.Activate(msg, m) newModel, cmd := item.Activate(msg, m)
if newModel != nil || cmd != nil { if newModel != nil || cmd != nil {
@ -457,3 +472,83 @@ func ascDesc(order sortOrder, result bool) bool {
} }
return !result return !result
} }
type ModsListDelegate struct {
list.ItemDelegate
Context *cli.GlobalContext
}
func (c ModsListDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
realItem := item.(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod])
realDelegate := c.ItemDelegate.(list.DefaultDelegate)
title := realItem.Title()
s := &realDelegate.Styles
if m.Width() <= 0 {
return
}
textwidth := uint(m.Width() - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight())
title = truncate.StringWithTail(title, textwidth, "…")
var (
isSelected = index == m.Index()
emptyFilter = m.FilterState() == list.Filtering && m.FilterValue() == ""
isFiltered = m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied
)
var matchedRunes []int
if isFiltered && index < len(m.VisibleItems()) {
// Get indices of matched characters
matchedRunes = m.MatchesForItem(index)
}
isInstalled := false
if c.Context != nil {
profile := c.Context.Profiles.Profiles[c.Context.Profiles.SelectedProfile]
if profile != nil {
if profile.HasMod(realItem.Extra.Mod_reference) {
isInstalled = true
}
}
}
if emptyFilter {
if isInstalled {
title = lipgloss.NewStyle().Foreground(lipgloss.Color("40")).Render("✓ " + title)
}
title = s.DimmedTitle.Render(title)
} else if isSelected && m.FilterState() != list.Filtering {
if isFiltered {
unmatched := s.SelectedTitle.Inline(true)
matched := unmatched.Copy().Inherit(s.FilterMatch)
if isInstalled {
unmatched = unmatched.Foreground(lipgloss.Color("40"))
matched = matched.Foreground(lipgloss.Color("40"))
}
title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
}
if isInstalled {
title = lipgloss.NewStyle().Foreground(lipgloss.Color("40")).Render("✓ ") + title
}
title = s.SelectedTitle.Render(title)
} else {
if isFiltered {
unmatched := s.NormalTitle.Inline(true)
matched := unmatched.Copy().Inherit(s.FilterMatch)
if isInstalled {
unmatched = unmatched.Foreground(lipgloss.Color("40"))
matched = matched.Foreground(lipgloss.Color("40"))
}
title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
}
if isInstalled {
title = lipgloss.NewStyle().Foreground(lipgloss.Color("40")).Render("✓ ") + title
}
title = s.NormalTitle.Render(title)
}
fmt.Fprintf(w, "%s", title)
}

View file

@ -32,7 +32,7 @@ type newInstallation struct {
} }
func NewNewInstallation(root components.RootModel, parent tea.Model) tea.Model { func NewNewInstallation(root components.RootModel, parent tea.Model) tea.Model {
listDelegate := CustomDelegate{ItemDelegate: utils.NewItemDelegate()} listDelegate := NewInstallListDelegate{ItemDelegate: utils.NewItemDelegate()}
l := list.New([]list.Item{}, listDelegate, root.Size().Width, root.Size().Height-root.Height()) l := list.New([]list.Item{}, listDelegate, root.Size().Width, root.Size().Height-root.Height())
l.SetShowStatusBar(true) l.SetShowStatusBar(true)
@ -229,11 +229,11 @@ func getDirItems(inputValue string) []list.Item {
return newItems return newItems
} }
type CustomDelegate struct { type NewInstallListDelegate struct {
list.ItemDelegate list.ItemDelegate
} }
func (c CustomDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) { func (c NewInstallListDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
realItem := item.(utils.SimpleItemExtra[newInstallation, string]) realItem := item.(utils.SimpleItemExtra[newInstallation, string])
realDelegate := c.ItemDelegate.(list.DefaultDelegate) realDelegate := c.ItemDelegate.(list.DefaultDelegate)

View file

@ -62,7 +62,7 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
allVersions := make([]ficsit.ModVersionsModVersionsVersion, 0) allVersions := make([]ficsit.ModVersionsModVersionsVersion, 0)
offset := 0 offset := 0
for { for {
versions, err := ficsit.ModVersions(context.TODO(), root.GetAPIClient(), mod.ID, ficsit.VersionFilter{ versions, err := ficsit.ModVersions(context.TODO(), root.GetAPIClient(), mod.Reference, ficsit.VersionFilter{
Limit: 100, Limit: 100,
Offset: offset, Offset: offset,
Order: ficsit.OrderDesc, Order: ficsit.OrderDesc,

View file

@ -2,6 +2,5 @@ package utils
type Mod struct { type Mod struct {
Name string Name string
ID string
Reference string Reference string
} }

View file

@ -1,9 +1,21 @@
package utils package utils
func CopyMap[T comparable, M any](m map[T]M) map[T]M { import (
m2 := make(map[T]M, len(m)) "encoding/json"
for k, v := range m {
m2[k] = v "github.com/pkg/errors"
)
func Copy[T any](obj T) (*T, error) {
marshal, err := json.Marshal(obj)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal object")
} }
return m2
out := new(T)
if err := json.Unmarshal(marshal, out); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal object")
}
return out, nil
} }