From 30c8dcf3cdcb03f72a885e2b8720bc3f26d42217 Mon Sep 17 00:00:00 2001 From: Vilsol Date: Sat, 4 Dec 2021 20:02:05 +0200 Subject: [PATCH] Add profile management --- cli/installations_test.go | 3 +- cli/profiles.go | 77 +++++++++------ cmd/root.go | 3 + tea/root.go | 24 ++--- tea/scenes/errors.go | 5 + tea/scenes/exit_menu.go | 11 ++- tea/scenes/keys.go | 1 + tea/scenes/main_menu.go | 13 ++- tea/scenes/messages.go | 17 ++++ tea/scenes/mod.go | 13 ++- tea/scenes/mod_info.go | 8 +- tea/scenes/mod_semver.go | 8 +- tea/scenes/mod_version.go | 13 ++- tea/scenes/mods.go | 17 +++- tea/scenes/new_profile.go | 67 +++++++++++++ tea/scenes/profile.go | 134 ++++++++++++++++++++++++++ tea/scenes/profiles.go | 130 +++++++++++++++++++++++++ tea/scenes/rename_profile.go | 73 ++++++++++++++ tea/scenes/select_mod_version.go | 6 +- tea/utils/{basic_list.go => lists.go} | 0 tea/utils/styles.go | 7 +- 21 files changed, 552 insertions(+), 78 deletions(-) create mode 100644 tea/scenes/errors.go create mode 100644 tea/scenes/messages.go create mode 100644 tea/scenes/new_profile.go create mode 100644 tea/scenes/profile.go create mode 100644 tea/scenes/rename_profile.go rename tea/utils/{basic_list.go => lists.go} (100%) diff --git a/cli/installations_test.go b/cli/installations_test.go index 8c7cdaf..9b6fde8 100644 --- a/cli/installations_test.go +++ b/cli/installations_test.go @@ -22,7 +22,8 @@ func TestAddInstallation(t *testing.T) { testza.AssertNoError(t, err) profileName := "InstallationTest" - profile := ctx.Profiles.AddProfile(profileName) + profile, err := ctx.Profiles.AddProfile(profileName) + testza.AssertNoError(t, err) testza.AssertNoError(t, profile.AddMod("AreaActions", ">=1.6.5")) testza.AssertNoError(t, profile.AddMod("ArmorModules__Modpack_All", ">=1.4.1")) diff --git a/cli/profiles.go b/cli/profiles.go index 7e897bb..6bda615 100644 --- a/cli/profiles.go +++ b/cli/profiles.go @@ -12,10 +12,10 @@ import ( "github.com/spf13/viper" ) -const defaultProfileName = "Default" +const DefaultProfileName = "Default" var defaultProfile = Profile{ - Name: defaultProfileName, + Name: DefaultProfileName, } type ProfilesVersion int @@ -28,9 +28,9 @@ const ( ) type Profiles struct { - Version ProfilesVersion `json:"version"` - Profiles []*Profile `json:"profiles"` - SelectedProfile string `json:"selected_profile"` + Version ProfilesVersion `json:"version"` + Profiles map[string]*Profile `json:"profiles"` + SelectedProfile string `json:"selected_profile"` } type Profile struct { @@ -66,8 +66,10 @@ func InitProfiles() (*Profiles, error) { } emptyProfiles := Profiles{ - Version: nextProfilesVersion - 1, - Profiles: []*Profile{&defaultProfile}, + Version: nextProfilesVersion - 1, + Profiles: map[string]*Profile{ + DefaultProfileName: &defaultProfile, + }, } if err := emptyProfiles.Save(); err != nil { @@ -90,12 +92,14 @@ func InitProfiles() (*Profiles, error) { } if len(profiles.Profiles) == 0 { - profiles.Profiles = []*Profile{&defaultProfile} - profiles.SelectedProfile = defaultProfileName + profiles.Profiles = map[string]*Profile{ + DefaultProfileName: &defaultProfile, + } + profiles.SelectedProfile = DefaultProfileName } - if profiles.SelectedProfile == "" { - profiles.SelectedProfile = profiles.Profiles[0].Name + if profiles.SelectedProfile == "" || profiles.Profiles[profiles.SelectedProfile] == nil { + profiles.SelectedProfile = DefaultProfileName } return &profiles, nil @@ -125,38 +129,53 @@ func (p *Profiles) Save() error { } // AddProfile adds a new profile with the given name to the profiles list. -func (p *Profiles) AddProfile(name string) *Profile { - profile := &Profile{ +func (p *Profiles) AddProfile(name string) (*Profile, error) { + if _, ok := p.Profiles[name]; ok { + return nil, fmt.Errorf("profile with name %s already exists", name) + } + + p.Profiles[name] = &Profile{ Name: name, } - p.Profiles = append(p.Profiles, profile) - - return profile + return p.Profiles[name], nil } // DeleteProfile deletes the profile with the given name. -func (p *Profiles) DeleteProfile(name string) { - i := 0 - for _, profile := range p.Profiles { - if profile.Name == name { - break +func (p *Profiles) DeleteProfile(name string) error { + if _, ok := p.Profiles[name]; ok { + delete(p.Profiles, name) + + if p.SelectedProfile == name { + p.SelectedProfile = DefaultProfileName } - i++ + return nil } - if i < len(p.Profiles) { - p.Profiles = append(p.Profiles[:i], p.Profiles[i+1:]...) - } + return fmt.Errorf("profile with name %s does not exist", name) } // GetProfile returns the profile with the given name or nil if it doesn't exist. func (p *Profiles) GetProfile(name string) *Profile { - for _, profile := range p.Profiles { - if profile.Name == name { - return profile - } + return p.Profiles[name] +} + +func (p *Profiles) RenameProfile(oldName string, newName string) error { + if _, ok := p.Profiles[newName]; ok { + return fmt.Errorf("profile with name %s already exists", newName) + } + + if _, ok := p.Profiles[oldName]; !ok { + return fmt.Errorf("profile with name %s does not exist", oldName) + } + + p.Profiles[oldName].Name = newName + p.Profiles[newName] = p.Profiles[oldName] + delete(p.Profiles, oldName) + + if p.SelectedProfile == oldName { + p.SelectedProfile = newName } return nil diff --git a/cmd/root.go b/cmd/root.go index 6b91eae..7819f61 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -67,6 +67,9 @@ func Execute() { // Execute tea as default cmd, _, err := rootCmd.Find(os.Args[1:]) + // Allow opening via explorer + cobra.MousetrapHelpText = "" + cli := len(os.Args) >= 2 && os.Args[1] == "cli" if (len(os.Args) <= 1 || os.Args[1] != "help") && (err != nil || cmd == rootCmd) { args := append([]string{"cli"}, os.Args[1:]...) diff --git a/tea/root.go b/tea/root.go index 6b624ef..32913f8 100644 --- a/tea/root.go +++ b/tea/root.go @@ -12,20 +12,16 @@ import ( ) type rootModel struct { - currentProfile *cli.Profile - currentInstallation *cli.Installation - global *cli.GlobalContext - apiClient graphql.Client - currentSize tea.WindowSizeMsg - headerComponent tea.Model + global *cli.GlobalContext + apiClient graphql.Client + currentSize tea.WindowSizeMsg + headerComponent tea.Model } func newModel(global *cli.GlobalContext) *rootModel { m := &rootModel{ - global: global, - currentProfile: global.Profiles.GetProfile(global.Profiles.SelectedProfile), - currentInstallation: global.Installations.GetInstallation(global.Installations.SelectedInstallation), - apiClient: ficsit.InitAPI(), + global: global, + apiClient: ficsit.InitAPI(), currentSize: tea.WindowSizeMsg{ Width: 20, Height: 14, @@ -38,21 +34,19 @@ func newModel(global *cli.GlobalContext) *rootModel { } func (m *rootModel) GetCurrentProfile() *cli.Profile { - return m.currentProfile + return m.global.Profiles.GetProfile(m.global.Profiles.SelectedProfile) } func (m *rootModel) SetCurrentProfile(profile *cli.Profile) error { - m.currentProfile = profile m.global.Profiles.SelectedProfile = profile.Name return m.global.Save() } func (m *rootModel) GetCurrentInstallation() *cli.Installation { - return m.currentInstallation + return m.global.Installations.GetInstallation(m.global.Installations.SelectedInstallation) } func (m *rootModel) SetCurrentInstallation(installation *cli.Installation) error { - m.currentInstallation = installation m.global.Installations.SelectedInstallation = installation.Path return m.global.Save() } @@ -82,7 +76,7 @@ func (m *rootModel) GetGlobal() *cli.GlobalContext { } func RunTea(global *cli.GlobalContext) error { - if err := tea.NewProgram(scenes.NewMainMenu(newModel(global))).Start(); err != nil { + if err := tea.NewProgram(scenes.NewMainMenu(newModel(global)), tea.WithAltScreen(), tea.WithMouseCellMotion()).Start(); err != nil { return errors.Wrap(err, "internal tea error") } return nil diff --git a/tea/scenes/errors.go b/tea/scenes/errors.go new file mode 100644 index 0000000..bd672be --- /dev/null +++ b/tea/scenes/errors.go @@ -0,0 +1,5 @@ +package scenes + +const ( + ErrorFailedAddMod = "failed to add mod" +) diff --git a/tea/scenes/exit_menu.go b/tea/scenes/exit_menu.go index 85fabb5..4daf06d 100644 --- a/tea/scenes/exit_menu.go +++ b/tea/scenes/exit_menu.go @@ -1,10 +1,11 @@ package scenes import ( + "time" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/davecgh/go-spew/spew" "github.com/rs/zerolog/log" "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/utils" @@ -27,7 +28,11 @@ func NewExitMenu(root components.RootModel) tea.Model { ItemTitle: "Exit Saving Changes", Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { if err := root.GetGlobal().Save(); err != nil { - panic(err) // TODO + message := "failed to save" + menu := currentModel.(exitMenu) + log.Error().Err(err).Msg(message) + cmd := menu.list.NewStatusMessage(message) + return currentModel, cmd } return currentModel, tea.Quit }, @@ -47,6 +52,7 @@ func NewExitMenu(root components.RootModel) tea.Model { model.list.Styles = utils.ListStyles model.list.DisableQuitKeybindings() model.list.SetSize(model.list.Width(), model.list.Height()) + model.list.StatusMessageLifetime = time.Second * 3 return model } @@ -56,7 +62,6 @@ func (m exitMenu) Init() tea.Cmd { } func (m exitMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - log.Warn().Msg(spew.Sdump(msg)) switch msg := msg.(type) { case tea.KeyMsg: switch keypress := msg.String(); keypress { diff --git a/tea/scenes/keys.go b/tea/scenes/keys.go index 5f1dc12..163aebc 100644 --- a/tea/scenes/keys.go +++ b/tea/scenes/keys.go @@ -3,4 +3,5 @@ package scenes const ( KeyControlC = "ctrl+c" KeyEnter = "enter" + KeyEscape = "esc" ) diff --git a/tea/scenes/main_menu.go b/tea/scenes/main_menu.go index 9d3ba82..4959969 100644 --- a/tea/scenes/main_menu.go +++ b/tea/scenes/main_menu.go @@ -1,10 +1,11 @@ package scenes import ( + "time" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/davecgh/go-spew/spew" "github.com/rs/zerolog/log" "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/utils" @@ -55,7 +56,10 @@ func NewMainMenu(root components.RootModel) tea.Model { ItemTitle: "Save", Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { if err := root.GetGlobal().Save(); err != nil { - panic(err) // TODO Handle Error + menu := currentModel.(exitMenu) + log.Error().Err(err).Msg(ErrorFailedAddMod) + cmd := menu.list.NewStatusMessage(ErrorFailedAddMod) + return currentModel, cmd } return nil, nil }, @@ -75,6 +79,8 @@ func NewMainMenu(root components.RootModel) tea.Model { model.list.Title = "Main Menu" model.list.Styles = utils.ListStyles model.list.SetSize(model.list.Width(), model.list.Height()) + model.list.StatusMessageLifetime = time.Second * 3 + model.list.DisableQuitKeybindings() return model } @@ -84,7 +90,6 @@ func (m mainMenu) Init() tea.Cmd { } func (m mainMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - log.Warn().Msg(spew.Sdump(msg)) switch msg := msg.(type) { case tea.KeyMsg: switch keypress := msg.String(); keypress { @@ -107,7 +112,7 @@ func (m mainMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } } - return m, tea.Quit + return m, nil default: var cmd tea.Cmd m.list, cmd = m.list.Update(msg) diff --git a/tea/scenes/messages.go b/tea/scenes/messages.go new file mode 100644 index 0000000..c80180c --- /dev/null +++ b/tea/scenes/messages.go @@ -0,0 +1,17 @@ +package scenes + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +type updateProfileList struct{} + +func updateProfileListCmd() tea.Msg { + return updateProfileList{} +} + +type updateProfileNames struct{} + +func updateProfileNamesCmd() tea.Msg { + return updateProfileNames{} +} diff --git a/tea/scenes/mod.go b/tea/scenes/mod.go index c08d8c4..6cf2905 100644 --- a/tea/scenes/mod.go +++ b/tea/scenes/mod.go @@ -1,10 +1,11 @@ package scenes import ( + "time" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/davecgh/go-spew/spew" "github.com/rs/zerolog/log" "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/utils" @@ -49,7 +50,10 @@ func NewModMenu(root components.RootModel, parent tea.Model, mod utils.Mod) tea. Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { err := root.GetCurrentProfile().AddMod(mod.Reference, ">=0.0.0") if err != nil { - panic(err) // TODO Handle Error + menu := currentModel.(exitMenu) + log.Error().Err(err).Msg(ErrorFailedAddMod) + cmd := menu.list.NewStatusMessage(ErrorFailedAddMod) + return currentModel, cmd } return currentModel.(modMenu).parent, nil }, @@ -79,6 +83,8 @@ func NewModMenu(root components.RootModel, parent tea.Model, mod utils.Mod) tea. model.list.Styles = utils.ListStyles model.list.SetSize(model.list.Width(), model.list.Height()) model.list.KeyMap.Quit.SetHelp("q", "back") + model.list.StatusMessageLifetime = time.Second * 3 + model.list.DisableQuitKeybindings() return model } @@ -88,7 +94,6 @@ func (m modMenu) Init() tea.Cmd { } func (m modMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - log.Warn().Msg(spew.Sdump(msg)) switch msg := msg.(type) { case tea.KeyMsg: switch keypress := msg.String(); keypress { @@ -115,7 +120,7 @@ func (m modMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } } - return m, tea.Quit + return m, nil default: var cmd tea.Cmd m.list, cmd = m.list.Update(msg) diff --git a/tea/scenes/mod_info.go b/tea/scenes/mod_info.go index 50d1825..3022bdb 100644 --- a/tea/scenes/mod_info.go +++ b/tea/scenes/mod_info.go @@ -5,6 +5,8 @@ import ( "strconv" "strings" + "github.com/rs/zerolog/log" + "github.com/PuerkitoBio/goquery" md "github.com/JohannesKaufmann/html-to-markdown" @@ -185,12 +187,14 @@ func (m modInfo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { markdownDescription, err := converter.ConvertString(mod.Full_description) if err != nil { - panic(err) // TODO Handle Error + log.Error().Err(err).Msg("failed to convert html to markdown") + markdownDescription = mod.Full_description } description, err := glamour.Render(markdownDescription, "dark") if err != nil { - panic(err) // TODO Handle Error + log.Error().Err(err).Msg("failed to render markdown") + description = mod.Full_description } bottomPart := lipgloss.JoinHorizontal(lipgloss.Top, sidebar, strings.TrimSpace(description)) diff --git a/tea/scenes/mod_semver.go b/tea/scenes/mod_semver.go index 9e287f2..059d01c 100644 --- a/tea/scenes/mod_semver.go +++ b/tea/scenes/mod_semver.go @@ -4,8 +4,6 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/davecgh/go-spew/spew" - "github.com/rs/zerolog/log" "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/utils" ) @@ -41,15 +39,13 @@ func (m modSemver) Init() tea.Cmd { } func (m modSemver) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - log.Warn().Msg(spew.Sdump(msg)) switch msg := msg.(type) { case tea.KeyMsg: switch keypress := msg.String(); keypress { case KeyControlC: return m, tea.Quit - case "q": - newModel := NewExitMenu(m.root) - return newModel, newModel.Init() + case KeyEscape: + return m.parent, nil case KeyEnter: err := m.root.GetCurrentProfile().AddMod(m.mod.Reference, m.input.Value()) if err != nil { diff --git a/tea/scenes/mod_version.go b/tea/scenes/mod_version.go index 2d00b84..bb5a9c7 100644 --- a/tea/scenes/mod_version.go +++ b/tea/scenes/mod_version.go @@ -1,10 +1,11 @@ package scenes import ( + "time" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/davecgh/go-spew/spew" "github.com/rs/zerolog/log" "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/utils" @@ -48,7 +49,10 @@ func NewModVersion(root components.RootModel, parent tea.Model, mod utils.Mod) t Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { err := root.GetCurrentProfile().AddMod(mod.Reference, ">=0.0.0") if err != nil { - panic(err) // TODO Handle Error + menu := currentModel.(exitMenu) + log.Error().Err(err).Msg(ErrorFailedAddMod) + cmd := menu.list.NewStatusMessage(ErrorFailedAddMod) + return currentModel, cmd } return currentModel.(modMenu).parent, nil }, @@ -63,6 +67,8 @@ func NewModVersion(root components.RootModel, parent tea.Model, mod utils.Mod) t model.list.Styles = utils.ListStyles model.list.SetSize(model.list.Width(), model.list.Height()) model.list.KeyMap.Quit.SetHelp("q", "back") + model.list.StatusMessageLifetime = time.Second * 3 + model.list.DisableQuitKeybindings() return model } @@ -72,7 +78,6 @@ func (m modVersionMenu) Init() tea.Cmd { } func (m modVersionMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - log.Warn().Msg(spew.Sdump(msg)) switch msg := msg.(type) { case tea.KeyMsg: switch keypress := msg.String(); keypress { @@ -99,7 +104,7 @@ func (m modVersionMenu) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } } - return m, tea.Quit + return m, nil default: var cmd tea.Cmd m.list, cmd = m.list.Update(msg) diff --git a/tea/scenes/mods.go b/tea/scenes/mods.go index 973c5af..385cdbb 100644 --- a/tea/scenes/mods.go +++ b/tea/scenes/mods.go @@ -10,8 +10,6 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/davecgh/go-spew/spew" - "github.com/rs/zerolog/log" "github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/utils" @@ -55,6 +53,8 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model { 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("s", "sort")), @@ -62,6 +62,13 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model { } } + l.AdditionalFullHelpKeys = func() []key.Binding { + return []key.Binding{ + key.NewBinding(key.WithHelp("s", "sort")), + key.NewBinding(key.WithHelp("o", "order")), + } + } + sortFieldList := list.NewModel([]list.Item{ utils.SimpleItem{ ItemTitle: "Name", @@ -101,6 +108,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model { sortFieldList.Styles = utils.ListStyles sortFieldList.SetSize(l.Width(), l.Height()) sortFieldList.KeyMap.Quit.SetHelp("q", "back") + sortFieldList.DisableQuitKeybindings() sortOrderList := list.NewModel([]list.Item{ utils.SimpleItem{ @@ -131,6 +139,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model { sortOrderList.Styles = utils.ListStyles sortOrderList.SetSize(l.Width(), l.Height()) sortOrderList.KeyMap.Quit.SetHelp("q", "back") + sortOrderList.DisableQuitKeybindings() m := &modsList{ root: root, @@ -196,7 +205,9 @@ func (m modsList) Init() tea.Cmd { } func (m modsList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - log.Info().Msg(spew.Sdump(msg)) + // List enables its own keybindings when they were previously disabled + m.list.DisableQuitKeybindings() + switch msg := msg.(type) { case tea.KeyMsg: if m.list.SettingFilter() { diff --git a/tea/scenes/new_profile.go b/tea/scenes/new_profile.go new file mode 100644 index 0000000..bf9c94a --- /dev/null +++ b/tea/scenes/new_profile.go @@ -0,0 +1,67 @@ +package scenes + +import ( + "github.com/charmbracelet/bubbles/textinput" + 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 = (*newProfile)(nil) + +type newProfile struct { + root components.RootModel + parent tea.Model + input textinput.Model + title string +} + +func NewNewProfile(root components.RootModel, parent tea.Model) tea.Model { + model := newProfile{ + root: root, + parent: parent, + input: textinput.NewModel(), + title: utils.NonListTitleStyle.Render("New Profile"), + } + + model.input.Focus() + model.input.Width = root.Size().Width + + return model +} + +func (m newProfile) Init() tea.Cmd { + return nil +} + +func (m newProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case KeyControlC: + return m, tea.Quit + case KeyEscape: + return m.parent, nil + case KeyEnter: + if _, err := m.root.GetGlobal().Profiles.AddProfile(m.input.Value()); err != nil { + panic(err) // TODO Handle Error + } + + return m.parent, updateProfileListCmd + default: + var cmd tea.Cmd + m.input, cmd = m.input.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + m.root.SetSize(msg) + } + + return m, nil +} + +func (m newProfile) View() string { + inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View()) + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView) +} diff --git a/tea/scenes/profile.go b/tea/scenes/profile.go new file mode 100644 index 0000000..35167b5 --- /dev/null +++ b/tea/scenes/profile.go @@ -0,0 +1,134 @@ +package scenes + +import ( + "fmt" + "time" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/satisfactorymodding/ficsit-cli/cli" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*profile)(nil) + +type profile struct { + root components.RootModel + list list.Model + parent tea.Model + profile *cli.Profile + hadRenamed bool +} + +func NewProfile(root components.RootModel, parent tea.Model, profileData *cli.Profile) tea.Model { + model := profile{ + root: root, + parent: parent, + profile: profileData, + } + + items := []list.Item{ + utils.SimpleItem{ + ItemTitle: "Select", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + if err := root.SetCurrentProfile(profileData); err != nil { + panic(err) // TODO Handle Error + } + + return currentModel.(profile).parent, nil + }, + }, + } + + if profileData.Name != cli.DefaultProfileName { + items = append(items, + utils.SimpleItem{ + ItemTitle: "Rename", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewRenameProfile(root, currentModel, profileData) + return newModel, newModel.Init() + }, + }, + utils.SimpleItem{ + ItemTitle: "Delete", + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + if err := root.GetGlobal().Profiles.DeleteProfile(profileData.Name); err != nil { + panic(err) // TODO Handle Error + } + + return currentModel.(profile).parent, updateProfileListCmd + }, + }, + ) + } + + model.list = list.NewModel(items, utils.NewItemDelegate(), root.Size().Width, root.Size().Height-root.Height()) + model.list.SetShowStatusBar(false) + model.list.SetFilteringEnabled(false) + model.list.Title = fmt.Sprintf("Profile: %s", profileData.Name) + model.list.Styles = utils.ListStyles + model.list.SetSize(model.list.Width(), model.list.Height()) + model.list.StatusMessageLifetime = time.Second * 3 + model.list.DisableQuitKeybindings() + + return model +} + +func (m profile) Init() tea.Cmd { + return nil +} + +func (m profile) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case KeyControlC: + return m, tea.Quit + case "q": + if m.parent != nil { + m.parent.Update(m.root.Size()) + + if m.hadRenamed { + return m.parent, updateProfileNamesCmd + } + + return m.parent, nil + } + return m, nil + case KeyEnter: + i, ok := m.list.SelectedItem().(utils.SimpleItem) + if ok { + if i.Activate != nil { + newModel, cmd := i.Activate(msg, m) + if newModel != nil || cmd != nil { + if newModel == nil { + newModel = m + } + return newModel, cmd + } + return m, nil + } + } + return m, nil + default: + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + top, right, bottom, left := lipgloss.NewStyle().Margin(2, 2).GetMargin() + m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom) + m.root.SetSize(msg) + case updateProfileNames: + m.hadRenamed = true + m.list.Title = fmt.Sprintf("Profile: %s", m.profile.Name) + } + + return m, nil +} + +func (m profile) View() string { + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View()) +} diff --git a/tea/scenes/profiles.go b/tea/scenes/profiles.go index 3cd8434..ec7b662 100644 --- a/tea/scenes/profiles.go +++ b/tea/scenes/profiles.go @@ -1,10 +1,140 @@ package scenes import ( + "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/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" ) +var _ tea.Model = (*profiles)(nil) + +type profiles struct { + root components.RootModel + list list.Model + parent tea.Model +} + func NewProfiles(root components.RootModel, parent tea.Model) tea.Model { + l := list.NewModel(profilesToList(root), utils.NewItemDelegate(), root.Size().Width, root.Size().Height-root.Height()) + l.SetShowStatusBar(true) + l.SetFilteringEnabled(true) + l.SetSpinner(spinner.MiniDot) + l.Title = "Profiles" + 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("n", "new profile")), + } + } + + l.AdditionalFullHelpKeys = func() []key.Binding { + return []key.Binding{ + key.NewBinding(key.WithHelp("n", "new profile")), + } + } + + return &profiles{ + root: root, + list: l, + parent: parent, + } +} + +func (m profiles) Init() tea.Cmd { return nil } + +func (m profiles) 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 "n": + newModel := NewNewProfile(m.root, m) + return newModel, newModel.Init() + case KeyControlC: + return m, tea.Quit + case "q": + if m.parent != nil { + m.parent.Update(m.root.Size()) + return m.parent, nil + } + return m, tea.Quit + case KeyEnter: + i, ok := m.list.SelectedItem().(utils.SimpleItem) + if ok { + if i.Activate != nil { + newModel, cmd := i.Activate(msg, m) + if newModel != nil || cmd != nil { + if newModel == nil { + newModel = m + } + return newModel, cmd + } + return m, nil + } + } + return m, 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 updateProfileList: + m.list.ResetSelected() + cmd := m.list.SetItems(profilesToList(m.root)) + + // Done to refresh keymap + m.list.SetFilteringEnabled(m.list.FilteringEnabled()) + return m, cmd + case updateProfileNames: + cmd := m.list.SetItems(profilesToList(m.root)) + + // Done to refresh keymap + m.list.SetFilteringEnabled(m.list.FilteringEnabled()) + return m, cmd + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m profiles) View() string { + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.list.View()) +} + +func profilesToList(root components.RootModel) []list.Item { + items := make([]list.Item, len(root.GetGlobal().Profiles.Profiles)) + + i := 0 + for _, profile := range root.GetGlobal().Profiles.Profiles { + temp := profile + items[i] = utils.SimpleItem{ + ItemTitle: temp.Name, + Activate: func(msg tea.Msg, currentModel tea.Model) (tea.Model, tea.Cmd) { + newModel := NewProfile(root, currentModel, temp) + return newModel, newModel.Init() + }, + } + i++ + } + + return items +} diff --git a/tea/scenes/rename_profile.go b/tea/scenes/rename_profile.go new file mode 100644 index 0000000..77cde89 --- /dev/null +++ b/tea/scenes/rename_profile.go @@ -0,0 +1,73 @@ +package scenes + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/satisfactorymodding/ficsit-cli/cli" + "github.com/satisfactorymodding/ficsit-cli/tea/components" + "github.com/satisfactorymodding/ficsit-cli/tea/utils" +) + +var _ tea.Model = (*renameProfile)(nil) + +type renameProfile struct { + root components.RootModel + parent tea.Model + input textinput.Model + title string + oldName string +} + +func NewRenameProfile(root components.RootModel, parent tea.Model, profileData *cli.Profile) tea.Model { + model := renameProfile{ + root: root, + parent: parent, + input: textinput.NewModel(), + title: utils.NonListTitleStyle.Render(fmt.Sprintf("Rename Profile: %s", profileData.Name)), + oldName: profileData.Name, + } + + model.input.SetValue(profileData.Name) + model.input.Focus() + model.input.Width = root.Size().Width + + return model +} + +func (m renameProfile) Init() tea.Cmd { + return nil +} + +func (m renameProfile) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case KeyControlC: + return m, tea.Quit + case KeyEscape: + return m.parent, nil + case KeyEnter: + if err := m.root.GetGlobal().Profiles.RenameProfile(m.oldName, m.input.Value()); err != nil { + panic(err) // TODO Handle Error + } + + return m.parent, updateProfileNamesCmd + default: + var cmd tea.Cmd + m.input, cmd = m.input.Update(msg) + return m, cmd + } + case tea.WindowSizeMsg: + m.root.SetSize(msg) + } + + return m, nil +} + +func (m renameProfile) View() string { + inputView := lipgloss.NewStyle().Padding(1, 2).Render(m.input.View()) + return lipgloss.JoinVertical(lipgloss.Left, m.root.View(), m.title, inputView) +} diff --git a/tea/scenes/select_mod_version.go b/tea/scenes/select_mod_version.go index f25dad3..450de47 100644 --- a/tea/scenes/select_mod_version.go +++ b/tea/scenes/select_mod_version.go @@ -8,8 +8,6 @@ import ( "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/davecgh/go-spew/spew" - "github.com/rs/zerolog/log" "github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/utils" @@ -33,6 +31,7 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo l.Styles = utils.ListStyles l.SetSize(l.Width(), l.Height()) l.KeyMap.Quit.SetHelp("q", "back") + l.DisableQuitKeybindings() m := &selectModVersionList{ root: root, @@ -93,7 +92,6 @@ func (m selectModVersionList) Init() tea.Cmd { } func (m selectModVersionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - log.Info().Msg(spew.Sdump(msg)) switch msg := msg.(type) { case tea.KeyMsg: switch keypress := msg.String(); keypress { @@ -119,7 +117,7 @@ func (m selectModVersionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } } - return m, tea.Quit + return m, nil default: var cmd tea.Cmd m.list, cmd = m.list.Update(msg) diff --git a/tea/utils/basic_list.go b/tea/utils/lists.go similarity index 100% rename from tea/utils/basic_list.go rename to tea/utils/lists.go diff --git a/tea/utils/styles.go b/tea/utils/styles.go index 2fbe83d..611a1d4 100644 --- a/tea/utils/styles.go +++ b/tea/utils/styles.go @@ -6,9 +6,10 @@ import ( ) var ( - ListStyles list.Styles - LabelStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("202")) - TitleStyle = list.DefaultStyles().Title.Background(lipgloss.Color("22")) + ListStyles list.Styles + LabelStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("202")) + TitleStyle = list.DefaultStyles().Title.Background(lipgloss.Color("22")) + NonListTitleStyle = TitleStyle.Copy().MarginLeft(2).Background(lipgloss.Color("22")) ) func init() {