From 9d7b5730a29d66c0e215fc76eceec365cf62e2cd Mon Sep 17 00:00:00 2001 From: Vilsol Date: Tue, 7 Jun 2022 02:55:26 +0300 Subject: [PATCH] installed mods, versioning, async mods --- cmd/root.go | 10 +- ficsit/api_test.go | 8 +- ficsit/queries/mod.graphql | 4 +- ficsit/queries/mods.graphql | 2 +- ficsit/types.go | 198 ++++++++--------- go.mod | 4 +- main.go | 7 +- tea/root.go | 4 +- tea/scenes/installed_mods.go | 219 +++++++++++++++++++ tea/scenes/main_menu.go | 26 ++- tea/scenes/mod_info.go | 8 +- tea/scenes/mods.go | 355 ++++++++++++++++++++----------- tea/scenes/new_installation.go | 6 +- tea/scenes/select_mod_version.go | 2 +- tea/utils/types.go | 1 - utils/structures.go | 22 +- 16 files changed, 617 insertions(+), 259 deletions(-) create mode 100644 tea/scenes/installed_mods.go diff --git a/cmd/root.go b/cmd/root.go index cbae1bf..468eb46 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -60,11 +60,16 @@ var rootCmd = &cobra.Command{ 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 }, } -func Execute() { +func Execute(version string, commit string) { // Execute tea as default cmd, _, err := rootCmd.Find(os.Args[1:]) @@ -83,6 +88,9 @@ func Execute() { viper.Set("quiet", true) } + viper.Set("version", version) + viper.Set("commit", commit) + if err := rootCmd.Execute(); err != nil { panic(err) } diff --git a/ficsit/api_test.go b/ficsit/api_test.go index d381dcf..38bd4d2 100644 --- a/ficsit/api_test.go +++ b/ficsit/api_test.go @@ -29,8 +29,8 @@ 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)) + testza.AssertNotNil(t, response.Mods) + testza.AssertNotNil(t, response.Mods.Mods) + testza.AssertNotZero(t, response.Mods.Count) + testza.AssertNotZero(t, len(response.Mods.Mods)) } diff --git a/ficsit/queries/mod.graphql b/ficsit/queries/mod.graphql index 8e66f5c..1dec009 100644 --- a/ficsit/queries/mod.graphql +++ b/ficsit/queries/mod.graphql @@ -1,5 +1,5 @@ -query GetMod ($modId: ModID!) { - getMod(modId: $modId) { +query GetMod ($modId: String!) { + mod: getModByIdOrReference(modIdOrReference: $modId) { id mod_reference name diff --git a/ficsit/queries/mods.graphql b/ficsit/queries/mods.graphql index d89ae13..230fd16 100644 --- a/ficsit/queries/mods.graphql +++ b/ficsit/queries/mods.graphql @@ -1,6 +1,6 @@ # @genqlient(omitempty: true) query Mods ($filter: ModFilter) { - getMods (filter: $filter) { + mods: getMods (filter: $filter) { count mods { id diff --git a/ficsit/types.go b/ficsit/types.go index 58f5acb..c3de91b 100644 --- a/ficsit/types.go +++ b/ficsit/types.go @@ -12,58 +12,58 @@ import ( "github.com/satisfactorymodding/ficsit-cli/ficsit/utils" ) -// GetModGetMod includes the requested fields of the GraphQL type Mod. -type GetModGetMod struct { - Id string `json:"id"` - Mod_reference string `json:"mod_reference"` - Name string `json:"name"` - Views int `json:"views"` - Downloads int `json:"downloads"` - Authors []GetModGetModAuthorsUserMod `json:"authors"` - Full_description string `json:"full_description"` - Source_url string `json:"source_url"` - Created_at time.Time `json:"-"` +// GetModMod includes the requested fields of the GraphQL type Mod. +type GetModMod struct { + Id string `json:"id"` + Mod_reference string `json:"mod_reference"` + Name string `json:"name"` + Views int `json:"views"` + Downloads int `json:"downloads"` + Authors []GetModModAuthorsUserMod `json:"authors"` + Full_description string `json:"full_description"` + Source_url string `json:"source_url"` + Created_at time.Time `json:"-"` } -// GetId returns GetModGetMod.Id, and is useful for accessing the field via an interface. -func (v *GetModGetMod) GetId() string { return v.Id } +// GetId returns GetModMod.Id, and is useful for accessing the field via an interface. +func (v *GetModMod) GetId() string { return v.Id } -// GetMod_reference returns GetModGetMod.Mod_reference, and is useful for accessing the field via an interface. -func (v *GetModGetMod) GetMod_reference() string { return v.Mod_reference } +// GetMod_reference returns GetModMod.Mod_reference, and is useful for accessing the field via an interface. +func (v *GetModMod) GetMod_reference() string { return v.Mod_reference } -// GetName returns GetModGetMod.Name, and is useful for accessing the field via an interface. -func (v *GetModGetMod) GetName() string { return v.Name } +// GetName returns GetModMod.Name, and is useful for accessing the field via an interface. +func (v *GetModMod) GetName() string { return v.Name } -// GetViews returns GetModGetMod.Views, and is useful for accessing the field via an interface. -func (v *GetModGetMod) GetViews() int { return v.Views } +// GetViews returns GetModMod.Views, and is useful for accessing the field via an interface. +func (v *GetModMod) GetViews() int { return v.Views } -// GetDownloads returns GetModGetMod.Downloads, and is useful for accessing the field via an interface. -func (v *GetModGetMod) GetDownloads() int { return v.Downloads } +// GetDownloads returns GetModMod.Downloads, and is useful for accessing the field via an interface. +func (v *GetModMod) GetDownloads() int { return v.Downloads } -// GetAuthors returns GetModGetMod.Authors, and is useful for accessing the field via an interface. -func (v *GetModGetMod) GetAuthors() []GetModGetModAuthorsUserMod { return v.Authors } +// GetAuthors returns GetModMod.Authors, and is useful for accessing the field via an interface. +func (v *GetModMod) GetAuthors() []GetModModAuthorsUserMod { return v.Authors } -// GetFull_description returns GetModGetMod.Full_description, and is useful for accessing the field via an interface. -func (v *GetModGetMod) GetFull_description() string { return v.Full_description } +// GetFull_description returns GetModMod.Full_description, and is useful for accessing the field via an interface. +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. -func (v *GetModGetMod) GetSource_url() string { return v.Source_url } +// GetSource_url returns GetModMod.Source_url, and is useful for accessing the field via an interface. +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. -func (v *GetModGetMod) GetCreated_at() time.Time { return v.Created_at } +// GetCreated_at returns GetModMod.Created_at, and is useful for accessing the field via an interface. +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" { return nil } var firstPass struct { - *GetModGetMod + *GetModMod Created_at json.RawMessage `json:"created_at"` graphql.NoUnmarshalJSON } - firstPass.GetModGetMod = v + firstPass.GetModMod = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -78,14 +78,14 @@ func (v *GetModGetMod) UnmarshalJSON(b []byte) error { src, dst) if err != nil { return fmt.Errorf( - "Unable to unmarshal GetModGetMod.Created_at: %w", err) + "Unable to unmarshal GetModMod.Created_at: %w", err) } } } return nil } -type __premarshalGetModGetMod struct { +type __premarshalGetModMod struct { Id string `json:"id"` Mod_reference string `json:"mod_reference"` @@ -96,7 +96,7 @@ type __premarshalGetModGetMod struct { Downloads int `json:"downloads"` - Authors []GetModGetModAuthorsUserMod `json:"authors"` + Authors []GetModModAuthorsUserMod `json:"authors"` Full_description string `json:"full_description"` @@ -105,7 +105,7 @@ type __premarshalGetModGetMod struct { Created_at json.RawMessage `json:"created_at"` } -func (v *GetModGetMod) MarshalJSON() ([]byte, error) { +func (v *GetModMod) MarshalJSON() ([]byte, error) { premarshaled, err := v.__premarshalJSON() if err != nil { return nil, err @@ -113,8 +113,8 @@ func (v *GetModGetMod) MarshalJSON() ([]byte, error) { return json.Marshal(premarshaled) } -func (v *GetModGetMod) __premarshalJSON() (*__premarshalGetModGetMod, error) { - var retval __premarshalGetModGetMod +func (v *GetModMod) __premarshalJSON() (*__premarshalGetModMod, error) { + var retval __premarshalGetModMod retval.Id = v.Id retval.Mod_reference = v.Mod_reference @@ -133,39 +133,39 @@ func (v *GetModGetMod) __premarshalJSON() (*__premarshalGetModGetMod, error) { &src) if err != nil { return nil, fmt.Errorf( - "Unable to marshal GetModGetMod.Created_at: %w", err) + "Unable to marshal GetModMod.Created_at: %w", err) } } return &retval, nil } -// GetModGetModAuthorsUserMod includes the requested fields of the GraphQL type UserMod. -type GetModGetModAuthorsUserMod struct { - Role string `json:"role"` - User GetModGetModAuthorsUserModUser `json:"user"` +// GetModModAuthorsUserMod includes the requested fields of the GraphQL type UserMod. +type GetModModAuthorsUserMod struct { + Role string `json:"role"` + User GetModModAuthorsUserModUser `json:"user"` } -// GetRole returns GetModGetModAuthorsUserMod.Role, and is useful for accessing the field via an interface. -func (v *GetModGetModAuthorsUserMod) GetRole() string { return v.Role } +// GetRole returns GetModModAuthorsUserMod.Role, and is useful for accessing the field via an interface. +func (v *GetModModAuthorsUserMod) GetRole() string { return v.Role } -// GetUser returns GetModGetModAuthorsUserMod.User, and is useful for accessing the field via an interface. -func (v *GetModGetModAuthorsUserMod) GetUser() GetModGetModAuthorsUserModUser { return v.User } +// GetUser returns GetModModAuthorsUserMod.User, and is useful for accessing the field via an interface. +func (v *GetModModAuthorsUserMod) GetUser() GetModModAuthorsUserModUser { return v.User } -// GetModGetModAuthorsUserModUser includes the requested fields of the GraphQL type User. -type GetModGetModAuthorsUserModUser struct { +// GetModModAuthorsUserModUser includes the requested fields of the GraphQL type User. +type GetModModAuthorsUserModUser struct { Username string `json:"username"` } -// GetUsername returns GetModGetModAuthorsUserModUser.Username, and is useful for accessing the field via an interface. -func (v *GetModGetModAuthorsUserModUser) GetUsername() string { return v.Username } +// GetUsername returns GetModModAuthorsUserModUser.Username, and is useful for accessing the field via an interface. +func (v *GetModModAuthorsUserModUser) GetUsername() string { return v.Username } // GetModResponse is returned by GetMod on success. 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. -func (v *GetModResponse) GetGetMod() GetModGetMod { return v.GetMod } +// GetMod returns GetModResponse.Mod, and is useful for accessing the field via an interface. +func (v *GetModResponse) GetMod() GetModMod { return v.Mod } type ModFields string @@ -259,20 +259,20 @@ type ModVersionsResponse struct { // GetMod returns ModVersionsResponse.Mod, and is useful for accessing the field via an interface. func (v *ModVersionsResponse) GetMod() ModVersionsMod { return v.Mod } -// ModsGetMods includes the requested fields of the GraphQL type GetMods. -type ModsGetMods struct { - Count int `json:"count"` - Mods []ModsGetModsModsMod `json:"mods"` +// ModsModsGetMods includes the requested fields of the GraphQL type GetMods. +type ModsModsGetMods struct { + Count int `json:"count"` + Mods []ModsModsGetModsModsMod `json:"mods"` } -// GetCount returns ModsGetMods.Count, and is useful for accessing the field via an interface. -func (v *ModsGetMods) GetCount() int { return v.Count } +// GetCount returns ModsModsGetMods.Count, and is useful for accessing the field via an interface. +func (v *ModsModsGetMods) GetCount() int { return v.Count } -// GetMods returns ModsGetMods.Mods, and is useful for accessing the field via an interface. -func (v *ModsGetMods) GetMods() []ModsGetModsModsMod { return v.Mods } +// GetMods returns ModsModsGetMods.Mods, and is useful for accessing the field via an interface. +func (v *ModsModsGetMods) GetMods() []ModsModsGetModsModsMod { return v.Mods } -// ModsGetModsModsMod includes the requested fields of the GraphQL type Mod. -type ModsGetModsModsMod struct { +// ModsModsGetModsModsMod includes the requested fields of the GraphQL type Mod. +type ModsModsGetModsModsMod struct { Id string `json:"id"` Name string `json:"name"` Mod_reference string `json:"mod_reference"` @@ -284,46 +284,46 @@ type ModsGetModsModsMod struct { Hotness int `json:"hotness"` } -// GetId returns ModsGetModsModsMod.Id, and is useful for accessing the field via an interface. -func (v *ModsGetModsModsMod) GetId() string { return v.Id } +// GetId returns ModsModsGetModsModsMod.Id, and is useful for accessing the field via an interface. +func (v *ModsModsGetModsModsMod) GetId() string { return v.Id } -// GetName returns ModsGetModsModsMod.Name, and is useful for accessing the field via an interface. -func (v *ModsGetModsModsMod) GetName() string { return v.Name } +// GetName returns ModsModsGetModsModsMod.Name, and is useful for accessing the field via an interface. +func (v *ModsModsGetModsModsMod) GetName() string { return v.Name } -// GetMod_reference returns ModsGetModsModsMod.Mod_reference, and is useful for accessing the field via an interface. -func (v *ModsGetModsModsMod) GetMod_reference() string { return v.Mod_reference } +// GetMod_reference returns ModsModsGetModsModsMod.Mod_reference, and is useful for accessing the field via an interface. +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. -func (v *ModsGetModsModsMod) GetLast_version_date() time.Time { return v.Last_version_date } +// GetLast_version_date returns ModsModsGetModsModsMod.Last_version_date, and is useful for accessing the field via an interface. +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. -func (v *ModsGetModsModsMod) GetCreated_at() time.Time { return v.Created_at } +// GetCreated_at returns ModsModsGetModsModsMod.Created_at, and is useful for accessing the field via an interface. +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. -func (v *ModsGetModsModsMod) GetViews() int { return v.Views } +// GetViews returns ModsModsGetModsModsMod.Views, and is useful for accessing the field via an interface. +func (v *ModsModsGetModsModsMod) GetViews() int { return v.Views } -// GetDownloads returns ModsGetModsModsMod.Downloads, and is useful for accessing the field via an interface. -func (v *ModsGetModsModsMod) GetDownloads() int { return v.Downloads } +// GetDownloads returns ModsModsGetModsModsMod.Downloads, and is useful for accessing the field via an interface. +func (v *ModsModsGetModsModsMod) GetDownloads() int { return v.Downloads } -// GetPopularity returns ModsGetModsModsMod.Popularity, and is useful for accessing the field via an interface. -func (v *ModsGetModsModsMod) GetPopularity() int { return v.Popularity } +// GetPopularity returns ModsModsGetModsModsMod.Popularity, and is useful for accessing the field via an interface. +func (v *ModsModsGetModsModsMod) GetPopularity() int { return v.Popularity } -// GetHotness returns ModsGetModsModsMod.Hotness, and is useful for accessing the field via an interface. -func (v *ModsGetModsModsMod) GetHotness() int { return v.Hotness } +// GetHotness returns ModsModsGetModsModsMod.Hotness, and is useful for accessing the field via an interface. +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" { return nil } var firstPass struct { - *ModsGetModsModsMod + *ModsModsGetModsModsMod Last_version_date json.RawMessage `json:"last_version_date"` Created_at json.RawMessage `json:"created_at"` graphql.NoUnmarshalJSON } - firstPass.ModsGetModsModsMod = v + firstPass.ModsModsGetModsModsMod = v err := json.Unmarshal(b, &firstPass) if err != nil { @@ -338,7 +338,7 @@ func (v *ModsGetModsModsMod) UnmarshalJSON(b []byte) error { src, dst) if err != nil { 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) if err != nil { return fmt.Errorf( - "Unable to unmarshal ModsGetModsModsMod.Created_at: %w", err) + "Unable to unmarshal ModsModsGetModsModsMod.Created_at: %w", err) } } } return nil } -type __premarshalModsGetModsModsMod struct { +type __premarshalModsModsGetModsModsMod struct { Id string `json:"id"` Name string `json:"name"` @@ -378,7 +378,7 @@ type __premarshalModsGetModsModsMod struct { Hotness int `json:"hotness"` } -func (v *ModsGetModsModsMod) MarshalJSON() ([]byte, error) { +func (v *ModsModsGetModsModsMod) MarshalJSON() ([]byte, error) { premarshaled, err := v.__premarshalJSON() if err != nil { return nil, err @@ -386,8 +386,8 @@ func (v *ModsGetModsModsMod) MarshalJSON() ([]byte, error) { return json.Marshal(premarshaled) } -func (v *ModsGetModsModsMod) __premarshalJSON() (*__premarshalModsGetModsModsMod, error) { - var retval __premarshalModsGetModsModsMod +func (v *ModsModsGetModsModsMod) __premarshalJSON() (*__premarshalModsModsGetModsModsMod, error) { + var retval __premarshalModsModsGetModsModsMod retval.Id = v.Id retval.Name = v.Name @@ -401,7 +401,7 @@ func (v *ModsGetModsModsMod) __premarshalJSON() (*__premarshalModsGetModsModsMod &src) if err != nil { 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) if err != nil { return nil, fmt.Errorf( - "Unable to marshal ModsGetModsModsMod.Created_at: %w", err) + "Unable to marshal ModsModsGetModsModsMod.Created_at: %w", err) } } retval.Views = v.Views @@ -425,11 +425,11 @@ func (v *ModsGetModsModsMod) __premarshalJSON() (*__premarshalModsGetModsModsMod // ModsResponse is returned by Mods on success. 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. -func (v *ModsResponse) GetGetMods() ModsGetMods { return v.GetMods } +// GetMods returns ModsResponse.Mods, and is useful for accessing the field via an interface. +func (v *ModsResponse) GetMods() ModsModsGetMods { return v.Mods } type Order string @@ -644,8 +644,8 @@ func GetMod( ctx, "GetMod", ` -query GetMod ($modId: ModID!) { - getMod(modId: $modId) { +query GetMod ($modId: String!) { + mod: getModByIdOrReference(modIdOrReference: $modId) { id mod_reference name @@ -718,7 +718,7 @@ func Mods( "Mods", ` query Mods ($filter: ModFilter) { - getMods(filter: $filter) { + mods: getMods(filter: $filter) { count mods { id diff --git a/go.mod b/go.mod index c9c1d65..49f33e1 100644 --- a/go.mod +++ b/go.mod @@ -12,9 +12,11 @@ require ( github.com/charmbracelet/bubbletea v0.21.0 github.com/charmbracelet/glamour 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/pterm/pterm v0.12.41 github.com/rs/zerolog v1.26.1 + github.com/sahilm/fuzzy v0.1.0 github.com/spf13/cobra v1.4.0 github.com/spf13/viper v1.12.0 ) @@ -48,13 +50,11 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // 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/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // 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/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.5.0 // indirect diff --git a/main.go b/main.go index fb3418b..419f2a7 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,11 @@ package main import "github.com/satisfactorymodding/ficsit-cli/cmd" +var ( + version = "dev" + commit = "none" +) + func main() { - cmd.Execute() + cmd.Execute(version, commit) } diff --git a/tea/root.go b/tea/root.go index f116370..39a6b8c 100644 --- a/tea/root.go +++ b/tea/root.go @@ -43,7 +43,7 @@ func (m *rootModel) SetCurrentProfile(profile *cli.Profile) error { return errors.Wrap(err, "failed setting profile on installation") } - return m.global.Save() + return nil } func (m *rootModel) GetCurrentInstallation() *cli.Installation { @@ -53,7 +53,7 @@ func (m *rootModel) GetCurrentInstallation() *cli.Installation { func (m *rootModel) SetCurrentInstallation(installation *cli.Installation) error { m.global.Installations.SelectedInstallation = installation.Path m.global.Profiles.SelectedProfile = installation.Profile - return m.global.Save() + return nil } func (m *rootModel) GetAPIClient() graphql.Client { diff --git a/tea/scenes/installed_mods.go b/tea/scenes/installed_mods.go new file mode 100644 index 0000000..1f91666 --- /dev/null +++ b/tea/scenes/installed_mods.go @@ -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 +} diff --git a/tea/scenes/main_menu.go b/tea/scenes/main_menu.go index 182ab14..7218826 100644 --- a/tea/scenes/main_menu.go +++ b/tea/scenes/main_menu.go @@ -10,6 +10,7 @@ import ( "github.com/rs/zerolog/log" "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/utils" + "github.com/spf13/viper" ) var _ tea.Model = (*mainMenu)(nil) @@ -73,12 +74,19 @@ func NewMainMenu(root components.RootModel) tea.Model { }, }, utils.SimpleItem[mainMenu]{ - ItemTitle: "Mods", + ItemTitle: "All Mods", Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) { newModel := NewMods(root, currentModel) 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]{ ItemTitle: "Apply Changes", Activate: func(msg tea.Msg, currentModel mainMenu) (tea.Model, tea.Cmd) { @@ -170,9 +178,21 @@ func (m mainMenu) View() string { header := m.root.View() 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 { - header = lipgloss.JoinVertical(lipgloss.Left, banner, m.root.View()) + header = lipgloss.JoinVertical(lipgloss.Left, banner, header) } if m.error != nil { diff --git a/tea/scenes/mod_info.go b/tea/scenes/mod_info.go index e0ce1e5..f113a29 100644 --- a/tea/scenes/mod_info.go +++ b/tea/scenes/mod_info.go @@ -30,7 +30,7 @@ type modInfo struct { viewport viewport.Model spinner spinner.Model parent tea.Model - modData chan ficsit.GetModGetMod + modData chan ficsit.GetModMod modError chan string ready bool help help.Model @@ -67,7 +67,7 @@ func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea. viewport: viewport.Model{}, spinner: spinner.New(), parent: parent, - modData: make(chan ficsit.GetModGetMod), + modData: make(chan ficsit.GetModMod), modError: make(chan string), ready: false, 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 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 { model.modError <- err.Error() @@ -99,7 +99,7 @@ func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea. return } - model.modData <- fullMod.GetMod + model.modData <- fullMod.Mod }() return model diff --git a/tea/scenes/mods.go b/tea/scenes/mods.go index dee2b9d..81037ea 100644 --- a/tea/scenes/mods.go +++ b/tea/scenes/mods.go @@ -2,17 +2,18 @@ package scenes import ( "context" + "fmt" + "io" "sort" "time" - "github.com/satisfactorymodding/ficsit-cli/cli" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "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/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/utils" @@ -29,11 +30,16 @@ const ( const modsTitle = "Mods" +type listUpdate struct { + Items []list.Item + Done bool +} + type modsList struct { root components.RootModel list list.Model parent tea.Model - items chan []list.Item + items chan listUpdate sortingField string sortingOrder sortOrder @@ -48,29 +54,11 @@ type modsList struct { 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 { - 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.SetShowFilter(true) l.SetFilteringEnabled(true) @@ -98,67 +86,81 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model { } sortFieldList := list.New([]list.Item{ - utils.SimpleItem[modsList]{ - ItemTitle: "Name", - Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { - m.sortingField = "name" - cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) - m.list.ResetSelected() - return m, cmd + utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ + SimpleItem: utils.SimpleItem[modsList]{ + ItemTitle: "Name", + Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { + m.sortingField = "name" + cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) + m.list.ResetSelected() + return m, cmd + }, }, }, - utils.SimpleItem[modsList]{ - ItemTitle: "Last Version Date", - Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { - m.sortingField = "last_version_date" - cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) - m.list.ResetSelected() - return m, cmd + utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ + SimpleItem: utils.SimpleItem[modsList]{ + ItemTitle: "Last Version Date", + Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { + m.sortingField = "last_version_date" + cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) + m.list.ResetSelected() + return m, cmd + }, }, }, - utils.SimpleItem[modsList]{ - ItemTitle: "Creation Date", - Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { - m.sortingField = "created_at" - cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) - m.list.ResetSelected() - return m, cmd + utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ + SimpleItem: utils.SimpleItem[modsList]{ + ItemTitle: "Creation Date", + Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { + m.sortingField = "created_at" + cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) + m.list.ResetSelected() + return m, cmd + }, }, }, - utils.SimpleItem[modsList]{ - ItemTitle: "Downloads", - Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { - m.sortingField = "downloads" - cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) - m.list.ResetSelected() - return m, cmd + utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ + SimpleItem: utils.SimpleItem[modsList]{ + ItemTitle: "Downloads", + Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { + m.sortingField = "downloads" + cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) + m.list.ResetSelected() + return m, cmd + }, }, }, - utils.SimpleItem[modsList]{ - ItemTitle: "Views", - Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { - m.sortingField = "views" - cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) - m.list.ResetSelected() - return m, cmd + utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ + SimpleItem: utils.SimpleItem[modsList]{ + ItemTitle: "Views", + Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { + m.sortingField = "views" + cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) + m.list.ResetSelected() + return m, cmd + }, }, }, - utils.SimpleItem[modsList]{ - ItemTitle: "Popularity (recent downloads)", - Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { - m.sortingField = "popularity" - cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) - m.list.ResetSelected() - return m, cmd + utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ + SimpleItem: utils.SimpleItem[modsList]{ + ItemTitle: "Popularity (recent downloads)", + Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { + m.sortingField = "popularity" + cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) + m.list.ResetSelected() + return m, cmd + }, }, }, - utils.SimpleItem[modsList]{ - ItemTitle: "Hotness (recent views)", - Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { - m.sortingField = "hotness" - cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) - m.list.ResetSelected() - return m, cmd + utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ + SimpleItem: utils.SimpleItem[modsList]{ + ItemTitle: "Hotness (recent views)", + Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { + m.sortingField = "hotness" + cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) + m.list.ResetSelected() + return m, cmd + }, }, }, }, 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() sortOrderList := list.New([]list.Item{ - utils.SimpleItem[modsList]{ - ItemTitle: "Ascending", - Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { - m.sortingOrder = sortOrderAsc - cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) - m.list.ResetSelected() - return m, cmd + utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ + SimpleItem: utils.SimpleItem[modsList]{ + ItemTitle: "Ascending", + Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { + m.sortingOrder = sortOrderAsc + cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) + m.list.ResetSelected() + return m, cmd + }, }, }, - utils.SimpleItem[modsList]{ - ItemTitle: "Descending", - Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { - m.sortingOrder = sortOrderDesc - cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) - m.list.ResetSelected() - return m, cmd + utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ + + SimpleItem: utils.SimpleItem[modsList]{ItemTitle: "Descending", + Activate: func(msg tea.Msg, m modsList) (tea.Model, tea.Cmd) { + m.sortingOrder = sortOrderDesc + cmd := m.list.SetItems(sortItems(m.list.Items(), m.sortingField, m.sortingOrder)) + m.list.ResetSelected() + return m, cmd + }, }, }, }, 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, list: l, parent: parent, - items: make(chan []list.Item), + items: make(chan listUpdate), sortingField: "last_version_date", sortingOrder: sortOrderDesc, sortFieldList: sortFieldList, @@ -214,7 +220,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model { go func() { items := make([]list.Item, 0) - allMods := make([]ficsit.ModsGetModsModsMod, 0) + allMods := make([]ficsit.ModsModsGetModsModsMod, 0) offset := 0 for { 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 } - if len(mods.GetMods.Mods) == 0 { + if len(mods.Mods.Mods) == 0 { 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 currentI := i - items = append(items, SimpleItemMod[modsList]{ + items = append(items, utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]{ 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) { mod := allMods[currentOffset+currentI] return NewModMenu(root, currentModel, utils.Mod{ Name: mod.Name, - ID: mod.Id, Reference: mod.Mod_reference, }), nil }, }, - Mod: allMods[currentOffset+currentI], - Context: root.GetGlobal(), + Extra: allMods[currentOffset+currentI], }) } - 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 @@ -308,7 +320,7 @@ func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case KeyEnter: if m.showSortFieldList { m.showSortFieldList = false - i, ok := m.sortFieldList.SelectedItem().(utils.SimpleItem[modsList]) + i, ok := m.sortFieldList.SelectedItem().(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) if ok { return m.processActivation(i, msg) } @@ -317,16 +329,16 @@ func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.showSortOrderList { m.showSortOrderList = false - i, ok := m.sortOrderList.SelectedItem().(utils.SimpleItem[modsList]) + i, ok := m.sortOrderList.SelectedItem().(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) if ok { return m.processActivation(i, msg) } return m, nil } - i, ok := m.list.SelectedItem().(SimpleItemMod[modsList]) + i, ok := m.list.SelectedItem().(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) if ok { - return m.processActivation(i.SimpleItem, msg) + return m.processActivation(i, msg) } return m, nil } @@ -337,9 +349,12 @@ func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case utils.TickMsg: select { case items := <-m.items: - m.list.StopSpinner() - cmd := m.list.SetItems(items) - return m, cmd + cmd := m.list.SetItems(items.Items) + if items.Done { + m.list.StopSpinner() + return m, cmd + } + return m, tea.Batch(utils.Ticker(), cmd) case err := <-m.err: errorComponent, cmd := components.NewErrorComponent(err, time.Second*5) m.error = errorComponent @@ -392,52 +407,52 @@ func sortItems(items []list.Item, field string, direction sortOrder) []list.Item switch field { case "last_version_date": sort.Slice(sortedItems, func(i, j int) bool { - a := sortedItems[i].(SimpleItemMod[modsList]) - b := sortedItems[j].(SimpleItemMod[modsList]) - return ascDesc(direction, a.Mod.Last_version_date.Before(b.Mod.Last_version_date)) + a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + return ascDesc(direction, a.Extra.Last_version_date.Before(b.Extra.Last_version_date)) }) case "created_at": sort.Slice(sortedItems, func(i, j int) bool { - a := sortedItems[i].(SimpleItemMod[modsList]) - b := sortedItems[j].(SimpleItemMod[modsList]) - return ascDesc(direction, a.Mod.Created_at.Before(b.Mod.Created_at)) + a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + return ascDesc(direction, a.Extra.Created_at.Before(b.Extra.Created_at)) }) case "name": sort.Slice(sortedItems, func(i, j int) bool { - a := sortedItems[i].(SimpleItemMod[modsList]) - b := sortedItems[j].(SimpleItemMod[modsList]) - return ascDesc(direction, a.Mod.Name < b.Mod.Name) + a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + return ascDesc(direction, a.Extra.Name < b.Extra.Name) }) case "downloads": sort.Slice(sortedItems, func(i, j int) bool { - a := sortedItems[i].(SimpleItemMod[modsList]) - b := sortedItems[j].(SimpleItemMod[modsList]) - return ascDesc(direction, a.Mod.Downloads < b.Mod.Downloads) + a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + return ascDesc(direction, a.Extra.Downloads < b.Extra.Downloads) }) case "views": sort.Slice(sortedItems, func(i, j int) bool { - a := sortedItems[i].(SimpleItemMod[modsList]) - b := sortedItems[j].(SimpleItemMod[modsList]) - return ascDesc(direction, a.Mod.Views < b.Mod.Views) + a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + return ascDesc(direction, a.Extra.Views < b.Extra.Views) }) case "popularity": sort.Slice(sortedItems, func(i, j int) bool { - a := sortedItems[i].(SimpleItemMod[modsList]) - b := sortedItems[j].(SimpleItemMod[modsList]) - return ascDesc(direction, a.Mod.Popularity < b.Mod.Popularity) + a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + return ascDesc(direction, a.Extra.Popularity < b.Extra.Popularity) }) case "hotness": sort.Slice(sortedItems, func(i, j int) bool { - a := sortedItems[i].(SimpleItemMod[modsList]) - b := sortedItems[j].(SimpleItemMod[modsList]) - return ascDesc(direction, a.Mod.Hotness < b.Mod.Hotness) + a := sortedItems[i].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + b := sortedItems[j].(utils.SimpleItemExtra[modsList, ficsit.ModsModsGetModsModsMod]) + return ascDesc(direction, a.Extra.Hotness < b.Extra.Hotness) }) } 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 { newModel, cmd := item.Activate(msg, m) if newModel != nil || cmd != nil { @@ -457,3 +472,83 @@ func ascDesc(order sortOrder, result bool) bool { } 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) +} diff --git a/tea/scenes/new_installation.go b/tea/scenes/new_installation.go index 53108e5..b8fb5a4 100644 --- a/tea/scenes/new_installation.go +++ b/tea/scenes/new_installation.go @@ -32,7 +32,7 @@ type newInstallation struct { } 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.SetShowStatusBar(true) @@ -229,11 +229,11 @@ func getDirItems(inputValue string) []list.Item { return newItems } -type CustomDelegate struct { +type NewInstallListDelegate struct { 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]) realDelegate := c.ItemDelegate.(list.DefaultDelegate) diff --git a/tea/scenes/select_mod_version.go b/tea/scenes/select_mod_version.go index 347354b..2b32e51 100644 --- a/tea/scenes/select_mod_version.go +++ b/tea/scenes/select_mod_version.go @@ -62,7 +62,7 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo allVersions := make([]ficsit.ModVersionsModVersionsVersion, 0) offset := 0 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, Offset: offset, Order: ficsit.OrderDesc, diff --git a/tea/utils/types.go b/tea/utils/types.go index 97faf40..bdbcb2a 100644 --- a/tea/utils/types.go +++ b/tea/utils/types.go @@ -2,6 +2,5 @@ package utils type Mod struct { Name string - ID string Reference string } diff --git a/utils/structures.go b/utils/structures.go index b800aaf..6ca1146 100644 --- a/utils/structures.go +++ b/utils/structures.go @@ -1,9 +1,21 @@ package utils -func CopyMap[T comparable, M any](m map[T]M) map[T]M { - m2 := make(map[T]M, len(m)) - for k, v := range m { - m2[k] = v +import ( + "encoding/json" + + "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 }