From d2fcce2d944f031b92d14c54a8894abf54cc7caf Mon Sep 17 00:00:00 2001 From: The Magician Date: Mon, 10 Jun 2024 13:32:45 +0100 Subject: [PATCH] Add multi-view UI and update screen --- sevenkeys/constants/constants.go | 19 +++ sevenkeys/figlet/figlet.go | 23 ++++ sevenkeys/main.go | 32 ++++- sevenkeys/tui/constants.go | 50 -------- sevenkeys/tui/home.go | 189 +++++++++++++++++++++++++++++ sevenkeys/tui/home/init.go | 9 -- sevenkeys/tui/home/model.go | 31 ----- sevenkeys/tui/home/update.go | 28 ----- sevenkeys/tui/home/view.go | 34 ------ sevenkeys/tui/searchui/searchui.go | 22 ++++ sevenkeys/tui/updateui/updateui.go | 137 +++++++++++++++++++++ 11 files changed, 416 insertions(+), 158 deletions(-) create mode 100644 sevenkeys/constants/constants.go create mode 100644 sevenkeys/figlet/figlet.go delete mode 100644 sevenkeys/tui/constants.go create mode 100644 sevenkeys/tui/home.go delete mode 100644 sevenkeys/tui/home/init.go delete mode 100644 sevenkeys/tui/home/model.go delete mode 100644 sevenkeys/tui/home/update.go delete mode 100644 sevenkeys/tui/home/view.go create mode 100644 sevenkeys/tui/searchui/searchui.go create mode 100644 sevenkeys/tui/updateui/updateui.go diff --git a/sevenkeys/constants/constants.go b/sevenkeys/constants/constants.go new file mode 100644 index 0000000..22780c2 --- /dev/null +++ b/sevenkeys/constants/constants.go @@ -0,0 +1,19 @@ +package constants + +import ( + "database/sql" + + tea "github.com/charmbracelet/bubbletea" + "github.com/lukesampson/figlet/figletlib" +) + +var ( + Program *tea.Program + Database *sql.DB + + WindowWidth int + WindowHeight int + + FigletFontSlant *figletlib.Font + FigletFontTerm *figletlib.Font +) diff --git a/sevenkeys/figlet/figlet.go b/sevenkeys/figlet/figlet.go new file mode 100644 index 0000000..a8147ca --- /dev/null +++ b/sevenkeys/figlet/figlet.go @@ -0,0 +1,23 @@ +package figlet + +import ( + "sevenkeys/constants" + + "github.com/lukesampson/figlet/figletlib" +) + +func SprintMsgSlant(msg, alignment string) string { + return figletlib.SprintMsg(msg, + constants.FigletFontSlant, + constants.WindowWidth, + constants.FigletFontSlant.Settings(), + alignment) +} + +func SprintMsgTerm(msg, alignment string) string { + return figletlib.SprintMsg(msg, + constants.FigletFontTerm, + constants.WindowWidth, + constants.FigletFontTerm.Settings(), + alignment) +} diff --git a/sevenkeys/main.go b/sevenkeys/main.go index 9af34a9..40257df 100644 --- a/sevenkeys/main.go +++ b/sevenkeys/main.go @@ -1,21 +1,41 @@ package main import ( + "os" + "path/filepath" + "sevenkeys/constants" + "sevenkeys/database" "sevenkeys/logic" - "sevenkeys/tui/home" + "sevenkeys/tui" tea "github.com/charmbracelet/bubbletea" + "github.com/lukesampson/figlet/figletlib" ) +func readFigletFonts() { + cwd, err := os.Getwd() + logic.Check(err) + + fontsdir := filepath.Join(cwd, "fonts") + + constants.FigletFontSlant, err = figletlib.GetFontByName(fontsdir, "slant") + logic.Check(err) + + constants.FigletFontTerm, err = figletlib.GetFontByName(fontsdir, "term") + logic.Check(err) +} + func main() { - m := home.NewModel() - p := tea.NewProgram(m, tea.WithAltScreen()) - _, err := p.Run() + constants.Database = database.GetDatabaseFromConfig("config.json") + + readFigletFonts() + + m := tui.NewMainModel() + constants.Program = tea.NewProgram(m, tea.WithAltScreen()) + _, err := constants.Program.Run() logic.Check(err) /* - db := database.GetDatabaseFromConfig("config.json") - fmt.Println("Checking for updates...") bulkData, err := scryfall.GetBulkDataByType(scryfall.BulkDataTypeAllCards) logic.Check(err) diff --git a/sevenkeys/tui/constants.go b/sevenkeys/tui/constants.go deleted file mode 100644 index 2e3288b..0000000 --- a/sevenkeys/tui/constants.go +++ /dev/null @@ -1,50 +0,0 @@ -package tui - -import "github.com/charmbracelet/bubbles/key" - -type KeyMappings struct { - Update key.Binding - SearchCriteria key.Binding - Search key.Binding - StorageOptions key.Binding - Find key.Binding - Quit key.Binding -} - -func (k KeyMappings) ShortHelp() []key.Binding { - return []key.Binding{k.Update, k.SearchCriteria, k.Search, k.StorageOptions, k.Find, k.Quit} -} - -func (k KeyMappings) FullHelp() [][]key.Binding { - return [][]key.Binding{ - {k.Update, k.SearchCriteria, k.Search}, - {k.StorageOptions, k.Find, k.Quit}, - } -} - -var KeyMap = KeyMappings{ - Update: key.NewBinding( - key.WithKeys("u"), - key.WithHelp("u", "update database"), - ), - SearchCriteria: key.NewBinding( - key.WithKeys("c"), - key.WithHelp("c", "card printing search criteria"), - ), - Search: key.NewBinding( - key.WithKeys("s"), - key.WithHelp("s", "card printing search"), - ), - StorageOptions: key.NewBinding( - key.WithKeys("o"), - key.WithHelp("o", "card storage options"), - ), - Find: key.NewBinding( - key.WithKeys("f"), - key.WithHelp("f", "find in storage"), - ), - Quit: key.NewBinding( - key.WithKeys("ctrl+c", "q"), - key.WithHelp("q", "quit program"), - ), -} diff --git a/sevenkeys/tui/home.go b/sevenkeys/tui/home.go new file mode 100644 index 0000000..46a7527 --- /dev/null +++ b/sevenkeys/tui/home.go @@ -0,0 +1,189 @@ +package tui + +import ( + "sevenkeys/constants" + "sevenkeys/figlet" + "sevenkeys/tui/searchui" + "sevenkeys/tui/updateui" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +type sessionState int + +const ( + homeView sessionState = iota + updateView + searchView +) + +type homeKeyMappings struct { + Update key.Binding + SearchCriteria key.Binding + Search key.Binding + StorageOptions key.Binding + Find key.Binding + Quit key.Binding +} + +func (k homeKeyMappings) ShortHelp() []key.Binding { + return []key.Binding{k.Update, k.SearchCriteria, k.Search, k.StorageOptions, k.Find, k.Quit} +} + +func (k homeKeyMappings) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Update, k.SearchCriteria, k.Search}, + {k.StorageOptions, k.Find, k.Quit}, + } +} + +type MainModel struct { + state sessionState + updateModel tea.Model + searchModel tea.Model + + help help.Model + keyMappings homeKeyMappings +} + +func NewMainModel() MainModel { + help := help.New() + help.ShortSeparator = help.FullSeparator + + updateModel := updateui.NewUpdateModel() + searchModel := searchui.NewSearchModel() + + keyMappings := homeKeyMappings{ + Update: key.NewBinding( + key.WithKeys("u"), + key.WithHelp("u", "update database"), + ), + SearchCriteria: key.NewBinding( + key.WithKeys("c"), + key.WithHelp("c", "card printing search criteria"), + ), + Search: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("s", "card printing search"), + ), + StorageOptions: key.NewBinding( + key.WithKeys("o"), + key.WithHelp("o", "card storage options"), + ), + Find: key.NewBinding( + key.WithKeys("f"), + key.WithHelp("f", "find in storage"), + ), + Quit: key.NewBinding( + key.WithKeys("ctrl+c", "q"), + key.WithHelp("q", "quit program"), + ), + } + + return MainModel{ + state: homeView, + updateModel: updateModel, + searchModel: searchModel, + + help: help, + keyMappings: keyMappings, + } +} + +func (m MainModel) Init() tea.Cmd { + return nil +} + +func renderHomeScreen(m MainModel) string { + var ui string + + // Display splash screen + ui += figlet.SprintMsgSlant("SEVENKEYS", "center") + ui += figlet.SprintMsgTerm("the ultimate Magic: the Gathering trading card storage system", "center") + + // Display help + ui += strings.Repeat("\n", constants.WindowHeight-8) // TODO: Avoid hardcoding height somehow + ui += m.help.View(m.keyMappings) + + return ui +} + +func (m MainModel) View() string { + var ui string + + if constants.WindowWidth <= 0 { + return ui + } + + switch m.state { + case homeView: + ui += renderHomeScreen(m) + break + case updateView: + ui += m.updateModel.View() + break + case searchView: + ui += m.searchModel.View() + break + } + + return ui +} + +func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + constants.WindowHeight = msg.Height + constants.WindowWidth = msg.Width + m.help.Width = msg.Width + case updateui.BackMsg: + m.state = homeView + } + + switch m.state { + case homeView: + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.keyMappings.Update): + m.state = updateView + break + case key.Matches(msg, m.keyMappings.Search): + m.state = searchView + break + case key.Matches(msg, m.keyMappings.Quit): + return m, tea.Quit + } + break + } + break + case updateView: + newUpdate, newCmd := m.updateModel.Update(msg) + + newUpdateModel, ok := newUpdate.(updateui.Model) + if !ok { + panic("Could not perform assertion on updateui.Model") + } + + m.updateModel = newUpdateModel + cmd = newCmd + break + case searchView: + newSearch, newCmd := m.searchModel.Update(msg) + newSearchModel, ok := newSearch.(searchui.Model) + if !ok { + panic("Could not perform assertion on searchui.Model") + } + + m.searchModel = newSearchModel + cmd = newCmd + break + } + + return m, cmd +} diff --git a/sevenkeys/tui/home/init.go b/sevenkeys/tui/home/init.go deleted file mode 100644 index d60be8d..0000000 --- a/sevenkeys/tui/home/init.go +++ /dev/null @@ -1,9 +0,0 @@ -package home - -import ( - tea "github.com/charmbracelet/bubbletea" -) - -func (m Model) Init() tea.Cmd { - return nil -} diff --git a/sevenkeys/tui/home/model.go b/sevenkeys/tui/home/model.go deleted file mode 100644 index 071f07d..0000000 --- a/sevenkeys/tui/home/model.go +++ /dev/null @@ -1,31 +0,0 @@ -package home - -import ( - "github.com/charmbracelet/bubbles/help" -) - -type SessionState int - -const ( - Home SessionState = iota - Update -) - -type HomeModel struct { - WindowHeight int - WindowWidth int - - State SessionState - - Help help.Model -} - -func NewHomeModel() HomeModel { - help := help.New() - help.ShortSeparator = help.FullSeparator - - return Model{ - Help: help, - SessionState: Home, - } -} diff --git a/sevenkeys/tui/home/update.go b/sevenkeys/tui/home/update.go deleted file mode 100644 index c6ef769..0000000 --- a/sevenkeys/tui/home/update.go +++ /dev/null @@ -1,28 +0,0 @@ -package home - -import ( - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" - - "sevenkeys/tui" -) - -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.WindowHeight = msg.Height - m.WindowWidth = msg.Width - m.Help.Width = msg.Width - return m, nil - case tea.KeyMsg: - switch { - case key.Matches(msg, tui.KeyMap.Quit): - return m, tea.Quit - } - break - } - - return m, cmd -} diff --git a/sevenkeys/tui/home/view.go b/sevenkeys/tui/home/view.go deleted file mode 100644 index 2e26868..0000000 --- a/sevenkeys/tui/home/view.go +++ /dev/null @@ -1,34 +0,0 @@ -package home - -import ( - "os" - "path/filepath" - "sevenkeys/tui" - "strings" - - "github.com/lukesampson/figlet/figletlib" -) - -func (m Model) View() string { - var ui string - - if m.WindowWidth <= 0 { - return ui - } - - // Get fonts for figlet - cwd, _ := os.Getwd() - fontsdir := filepath.Join(cwd, "fonts") - slantFont, _ := figletlib.GetFontByName(fontsdir, "slant") - termFont, _ := figletlib.GetFontByName(fontsdir, "term") - - // Display splash screen - ui += figletlib.SprintMsg("SEVENKEYS", slantFont, m.WindowWidth, slantFont.Settings(), "center") - ui += figletlib.SprintMsg("the ultimate Magic: the Gathering trading card storage system", termFont, m.WindowWidth, termFont.Settings(), "center") - - // Display help - ui += strings.Repeat("\n", m.WindowHeight-8) // TODO: Avoid hardcoding height somehow - ui += m.Help.View(tui.KeyMap) - - return ui -} diff --git a/sevenkeys/tui/searchui/searchui.go b/sevenkeys/tui/searchui/searchui.go new file mode 100644 index 0000000..62fba71 --- /dev/null +++ b/sevenkeys/tui/searchui/searchui.go @@ -0,0 +1,22 @@ +package searchui + +import tea "github.com/charmbracelet/bubbletea" + +type Model struct { +} + +func NewSearchModel() Model { + return Model{} +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + return m, nil +} + +func (m Model) View() string { + return "Search" +} diff --git a/sevenkeys/tui/updateui/updateui.go b/sevenkeys/tui/updateui/updateui.go new file mode 100644 index 0000000..6928478 --- /dev/null +++ b/sevenkeys/tui/updateui/updateui.go @@ -0,0 +1,137 @@ +package updateui + +import ( + "database/sql" + "log" + "sevenkeys/constants" + "sevenkeys/logic" + "sevenkeys/logic/scryfall" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/lukesampson/figlet/figletlib" +) + +type BackMsg int + +type updateRequiredMsg bool +type errorMsg error + +type updateKeyMappings struct { + back key.Binding + quit key.Binding +} + +func (k updateKeyMappings) ShortHelp() []key.Binding { + return []key.Binding{k.back, k.quit} +} + +func (k updateKeyMappings) FullHelp() [][]key.Binding { + return [][]key.Binding{{k.back, k.quit}} +} + +type Model struct { + updateRequired bool + updateFinished bool + + canLeave bool + + help help.Model + keyMappings updateKeyMappings +} + +func NewUpdateModel() Model { + help := help.New() + help.ShortSeparator = help.FullSeparator + + keyMappings := updateKeyMappings{ + back: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "back"), + ), + quit: key.NewBinding( + key.WithKeys("q"), + key.WithHelp("q", "quit"), + ), + } + + return Model{ + canLeave: true, + help: help, + keyMappings: keyMappings, + } +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + if m.canLeave { + switch { + case key.Matches(msg, m.keyMappings.back): + return m, backCmd() + case key.Matches(msg, m.keyMappings.quit): + return m, tea.Quit + } + } + break + case errorMsg: + log.Fatal(msg.Error()) + break + case updateRequiredMsg: + m.updateRequired = bool(msg) + break + } + + return m, cmd +} + +func (m Model) View() string { + var ui string + + ui += figletlib.SprintMsg("Update", constants.FigletFontSlant, constants.WindowWidth, constants.FigletFontSlant.Settings(), "left") + ui += strings.Repeat("-", constants.WindowWidth) + ui += "\n" + + if m.updateRequired { + ui += "Database is out of date. Running update." + } else { + ui += "No update required." + } + + if m.canLeave { + ui += strings.Repeat("\n", constants.WindowHeight-8) // TODO: Hardcoded height (again) + ui += m.help.View(m.keyMappings) + } + + return ui +} + +func checkUpdateRequiredCmd(db *sql.DB) tea.Cmd { + return func() tea.Msg { + bulkData, err := scryfall.GetBulkDataByType(scryfall.BulkDataTypeAllCards) + if err != nil { + return errorMsg(err) + } + + needsUpdate, err := logic.CheckForUpdates(db, bulkData) + if err != nil { + return errorMsg(err) + } + + return updateRequiredMsg(needsUpdate) + } +} + +func backCmd() tea.Cmd { + return func() tea.Msg { + return BackMsg(1) + } +}