From 2aab496fbdb5d6c04916c97f738d99de331de2fe Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Thu, 18 Apr 2024 21:45:32 +0200 Subject: [PATCH 1/8] feat: popups with title on the top border --- internal/ui/browser/browser.go | 21 +++------- internal/ui/popup/choice.go | 15 +++----- internal/ui/popup/fancy_border.go | 64 +++++++++++++++++++++++++++++++ internal/ui/popup/style.go | 14 ++----- internal/ui/tab/category/popup.go | 34 +++++++++++----- internal/ui/tab/category/style.go | 27 ++++--------- internal/ui/tab/overview/popup.go | 33 ++++++++++++---- internal/ui/tab/overview/style.go | 24 +++--------- 8 files changed, 143 insertions(+), 89 deletions(-) create mode 100644 internal/ui/popup/fancy_border.go diff --git a/internal/ui/browser/browser.go b/internal/ui/browser/browser.go index 437e6e7..4e36233 100644 --- a/internal/ui/browser/browser.go +++ b/internal/ui/browser/browser.go @@ -164,15 +164,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.createNewTab(msg) case backend.NewItemMsg: - bg := m.View() - width := m.width / 2 - height := 17 - switch msg.Sender.(type) { case overview.Model: - m.popup = overview.NewPopup(m.style.colors, bg, width, height, "", "") + m.popup = overview.NewPopup(m.style.colors, m.View(), "", "") case category.Model: - m.popup = category.NewPopup(m.style.colors, bg, width, height, "", "", msg.Sender.Title()) + m.popup = category.NewPopup(m.style.colors, m.View(), "", "", msg.Sender.Title()) case feed.Model: } @@ -180,16 +176,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.popup.Init() case backend.EditItemMsg: - bg := m.View() - width := m.width / 2 - height := 17 oldName, oldDesc := msg.OldFields[0], msg.OldFields[1] switch msg.Sender.(type) { case overview.Model: - m.popup = overview.NewPopup(m.style.colors, bg, width, height, oldName, oldDesc) + m.popup = overview.NewPopup(m.style.colors, m.View(), oldName, oldDesc) case category.Model: - m.popup = category.NewPopup(m.style.colors, bg, width, height, oldName, oldDesc, msg.Sender.Title()) + m.popup = category.NewPopup(m.style.colors, m.View(), oldName, oldDesc, msg.Sender.Title()) case feed.Model: } @@ -211,10 +204,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case backend.MakeChoiceMsg: - bg := m.View() - width := m.width / 2 - m.popup = popup.NewChoice(m.style.colors, bg, width, msg.Question, msg.Default) - + m.popup = popup.NewChoice(m.style.colors, m.View(), msg.Question, msg.Default) m.keymap.SetEnabled(false) return m, m.popup.Init() @@ -222,6 +212,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.keymap.SetEnabled(true) m.popup = nil + // TODO: Resize the popup if necessary case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height diff --git a/internal/ui/popup/choice.go b/internal/ui/popup/choice.go index daa3c43..10d7e76 100644 --- a/internal/ui/popup/choice.go +++ b/internal/ui/popup/choice.go @@ -20,17 +20,13 @@ type Choice struct { } // NewChoice creates a new Choice popup. -func NewChoice(colors *theme.Colors, bgRaw string, width int, question string, defaultChoice bool) Choice { - optWidth := len(question) + 16 - if optWidth > width { - optWidth = width - } - +func NewChoice(colors *theme.Colors, bgRaw string, question string, defaultChoice bool) Choice { + width := len(question) + 16 height := 7 return Choice{ - style: newStyle(colors, optWidth, height), - overlay: NewOverlay(bgRaw, optWidth, height), + style: newStyle(colors, width, height), + overlay: NewOverlay(bgRaw, width, height), question: question, selected: defaultChoice, } @@ -80,8 +76,7 @@ func (c Choice) View() string { buttons := lipgloss.JoinHorizontal(lipgloss.Top, okButton, cancelButton) ui := lipgloss.JoinVertical(lipgloss.Center, question, buttons) dialog := lipgloss.Place(c.overlay.width-2, c.overlay.height-2, lipgloss.Center, lipgloss.Center, ui) - - return c.overlay.WrapView(c.style.general.Render(dialog)) + return c.overlay.WrapView(c.style.border.Render(dialog)) } // makeChoice returns a tea.Cmd that tells the parent model about the choice. diff --git a/internal/ui/popup/fancy_border.go b/internal/ui/popup/fancy_border.go new file mode 100644 index 0000000..f684b46 --- /dev/null +++ b/internal/ui/popup/fancy_border.go @@ -0,0 +1,64 @@ +package popup + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// TitleBorder creates a fancy border style to wrap a popup +type TitleBorder struct { + bottomBorder lipgloss.Style + topBorder string + + text string + borderType lipgloss.Border + color lipgloss.Color +} + +// NewTitleBorder creates a new fancy border with a specific title +func NewTitleBorder(text string, width, height int, color lipgloss.Color, border lipgloss.Border) TitleBorder { + tb := TitleBorder{ + text: strings.Clone(text), + borderType: border, + color: color, + } + + tb.Resize(width, height) + return tb +} + +// Resize allows resizing the border and adjusting the top border +func (tb *TitleBorder) Resize(width, height int) { + tb.bottomBorder = lipgloss.NewStyle(). + Width(width-2). + Height(height-2). + Border(lipgloss.NormalBorder(), false, true, true, true). + BorderForeground(tb.color) + + textCopy := " " + strings.Clone(tb.text) + " " + if width-len(textCopy) < 2 { + textCopy = textCopy[:width-2] + } + + textWidth := len(textCopy) + if textWidth%2 == 1 { + textCopy += " " + textWidth++ + } + + fill := (width - 2 - textWidth) / 2 + + title := strings.Builder{} + title.WriteString(tb.borderType.TopLeft) + title.WriteString(strings.Repeat(tb.borderType.Top, fill+width%2)) + title.WriteString(textCopy) + title.WriteString(strings.Repeat(tb.borderType.Top, fill)) + title.WriteString(tb.borderType.TopRight) + tb.topBorder = lipgloss.NewStyle().Foreground(tb.color).Render(title.String()) +} + +// Render renders the contnent with the fancy border +func (tb TitleBorder) Render(view string) string { + return lipgloss.JoinVertical(lipgloss.Top, tb.topBorder, tb.bottomBorder.Render(view)) +} diff --git a/internal/ui/popup/style.go b/internal/ui/popup/style.go index 4c96e8e..c3005c7 100644 --- a/internal/ui/popup/style.go +++ b/internal/ui/popup/style.go @@ -7,10 +7,10 @@ import ( // style is the style of the choice popup type style struct { + border TitleBorder button lipgloss.Style activeButton lipgloss.Style question lipgloss.Style - general lipgloss.Style } // newStyle creates a new style for the choice popup @@ -23,15 +23,7 @@ func newStyle(colors *theme.Colors, width, height int) style { activeButtonStyle := buttonStyle.Copy(). Foreground(colors.Text). - Background(colors.Color3). - Underline(true) - - general := lipgloss.NewStyle(). - Foreground(colors.Text). - Width(width - 2). - Height(height - 2). - Border(lipgloss.NormalBorder()). - BorderForeground(colors.Color1) + Background(colors.Color3) question := lipgloss.NewStyle(). Width(width). @@ -40,9 +32,9 @@ func newStyle(colors *theme.Colors, width, height int) style { Align(lipgloss.Center) return style{ + border: NewTitleBorder("Confirm choice", width, height, colors.Color1, lipgloss.NormalBorder()), button: buttonStyle, activeButton: activeButtonStyle, question: question, - general: general, } } diff --git a/internal/ui/tab/category/popup.go b/internal/ui/tab/category/popup.go index 6d25dd0..d851380 100644 --- a/internal/ui/tab/category/popup.go +++ b/internal/ui/tab/category/popup.go @@ -35,14 +35,17 @@ type Popup struct { parent string overlay popup.Overlay focused focusedField + editing bool } // NewPopup returns a new feed popup. -func NewPopup(colors *theme.Colors, bgRaw string, width, height int, - oldName, oldURL, parent string) Popup { +func NewPopup(colors *theme.Colors, bgRaw string, oldName, oldURL, parent string) Popup { + width := 40 + height := 7 - style := newPopupStyle(colors, width, height) overlay := popup.NewOverlay(bgRaw, width, height) + editing := oldName != "" || oldURL != "" + nameInput := textinput.New() nameInput.CharLimit = 30 nameInput.Prompt = "Name: " @@ -52,7 +55,14 @@ func NewPopup(colors *theme.Colors, bgRaw string, width, height int, urlInput.Width = width - 20 urlInput.Prompt = "URL: " - if oldName != "" || oldURL != "" { + style := popupStyle{} + if editing { + style = newPopupStyle(colors, width, height, "Edit feed") + } else { + style = newPopupStyle(colors, width, height, "New feed") + } + + if editing { nameInput.SetValue(oldName) urlInput.SetValue(oldURL) } @@ -67,6 +77,7 @@ func NewPopup(colors *theme.Colors, bgRaw string, width, height int, oldName: oldName, oldURL: oldURL, parent: parent, + editing: editing, } } @@ -122,13 +133,18 @@ func (p Popup) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View renders the popup. func (p Popup) View() string { - question := p.style.heading.Render("Choose a feed") - title := p.style.itemTitle.Render("New Feed") + itemText := "" + if p.editing { + itemText = "Your feed" + } else { + itemText = "New feed" + } + + itemTitle := p.style.itemTitle.Render(itemText) name := p.style.itemField.Render(p.nameInput.View()) url := p.style.itemField.Render(p.urlInput.View()) - item := p.style.item.Render(lipgloss.JoinVertical(lipgloss.Left, title, name, url)) - popup := lipgloss.JoinVertical(lipgloss.Left, question, item) - return p.overlay.WrapView(p.style.general.Render(popup)) + listItem := p.style.listItem.Render(lipgloss.JoinVertical(lipgloss.Left, itemTitle, name, url)) + return p.overlay.WrapView(p.style.border.Render(listItem)) } // confirm creates a message that confirms the user's choice. diff --git a/internal/ui/tab/category/style.go b/internal/ui/tab/category/style.go index 8ef2768..8b04ec8 100644 --- a/internal/ui/tab/category/style.go +++ b/internal/ui/tab/category/style.go @@ -2,35 +2,25 @@ package category import ( "github.com/TypicalAM/goread/internal/theme" + "github.com/TypicalAM/goread/internal/ui/popup" "github.com/charmbracelet/lipgloss" ) // popupStyle is the style of the popup window. type popupStyle struct { - general lipgloss.Style + border popup.TitleBorder heading lipgloss.Style - item lipgloss.Style + listItem lipgloss.Style itemTitle lipgloss.Style itemField lipgloss.Style } // newPopupStyle creates a new popup style. -func newPopupStyle(colors *theme.Colors, width, height int) popupStyle { - general := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFFFF")). - Width(width - 2). - Height(height - 2). - Border(lipgloss.NormalBorder()). - BorderForeground(colors.Color1) - - heading := lipgloss.NewStyle(). - Margin(1, 0, 1, 0). - Width(width - 2). - Align(lipgloss.Center). - Italic(true) +func newPopupStyle(colors *theme.Colors, width, height int, headingText string) popupStyle { + border := popup.NewTitleBorder(headingText, width, height, colors.Color1, lipgloss.NormalBorder()) item := lipgloss.NewStyle(). - Margin(0, 4). + Margin(1, 4). PaddingLeft(1). Border(lipgloss.RoundedBorder(), false, false, false, true). BorderForeground(colors.Color3). @@ -43,9 +33,8 @@ func newPopupStyle(colors *theme.Colors, width, height int) popupStyle { Foreground(colors.Color2) return popupStyle{ - general: general, - heading: heading, - item: item, + border: border, + listItem: item, itemTitle: itemTitle, itemField: itemField, } diff --git a/internal/ui/tab/overview/popup.go b/internal/ui/tab/overview/popup.go index f7e9e8a..3a830de 100644 --- a/internal/ui/tab/overview/popup.go +++ b/internal/ui/tab/overview/popup.go @@ -35,12 +35,17 @@ type Popup struct { oldName string overlay popup.Overlay focused focusedField + editing bool } // NewPopup creates a new popup window in which the user can choose a new category. -func NewPopup(colors *theme.Colors, bgRaw string, width, height int, oldName, oldDesc string) Popup { +func NewPopup(colors *theme.Colors, bgRaw string, oldName, oldDesc string) Popup { + width := 46 + height := 14 + overlay := popup.NewOverlay(bgRaw, width, height) - style := newPopupStyle(colors, width, height) + editing := oldName != "" || oldDesc != "" + nameInput := textinput.New() nameInput.CharLimit = 30 nameInput.Width = width - 15 @@ -51,7 +56,14 @@ func NewPopup(colors *theme.Colors, bgRaw string, width, height int, oldName, ol descInput.Prompt = "Description: " focusedField := allField - if oldName != "" || oldDesc != "" { + style := popupStyle{} + if editing { + style = newPopupStyle(colors, width, height, "Edit category") + } else { + style = newPopupStyle(colors, width, height, "New category") + } + + if editing { nameInput.SetValue(oldName) descInput.SetValue(oldDesc) focusedField = nameField @@ -65,6 +77,7 @@ func NewPopup(colors *theme.Colors, bgRaw string, width, height int, oldName, ol descInput: descInput, oldName: oldName, focused: focusedField, + editing: editing, } } @@ -91,6 +104,10 @@ func (p Popup) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p.nameInput.Blur() cmds = append(cmds, p.descInput.Focus()) case descField: + if p.editing { + return p, nil + } + p.focused = allField p.descInput.Blur() } @@ -103,6 +120,10 @@ func (p Popup) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case downloadedField: p.focused = allField case nameField: + if p.editing { + return p, nil + } + p.focused = downloadedField p.nameInput.Blur() case descField: @@ -142,11 +163,10 @@ func (p Popup) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View renders the popup window. func (p Popup) View() string { - question := p.style.heading.Render("Choose a category") renderedChoices := make([]string, 3) titles := []string{rss.AllFeedsName, rss.DownloadedFeedsName, "New category"} - descs := []string{"All the feeds", "Saved Feeds", p.nameInput.View() + "\n" + p.descInput.View()} + descs := []string{"All available articles", "Downloaded articles", p.nameInput.View() + "\n" + p.descInput.View()} var focused int switch p.focused { @@ -175,8 +195,7 @@ func (p Popup) View() string { } toList := p.style.list.Render(lipgloss.JoinVertical(lipgloss.Top, renderedChoices...)) - popup := lipgloss.JoinVertical(lipgloss.Top, question, toList) - return p.overlay.WrapView(p.style.general.Render(popup)) + return p.overlay.WrapView(p.style.border.Render(toList)) } // confirm creates a message that confirms the user's choice. diff --git a/internal/ui/tab/overview/style.go b/internal/ui/tab/overview/style.go index b04e46e..0533c8c 100644 --- a/internal/ui/tab/overview/style.go +++ b/internal/ui/tab/overview/style.go @@ -2,13 +2,13 @@ package overview import ( "github.com/TypicalAM/goread/internal/theme" + "github.com/TypicalAM/goread/internal/ui/popup" "github.com/charmbracelet/lipgloss" ) // popupStyle is the style of the popup window. type popupStyle struct { - general lipgloss.Style - heading lipgloss.Style + border popup.TitleBorder list lipgloss.Style choice lipgloss.Style choiceTitle lipgloss.Style @@ -19,22 +19,11 @@ type popupStyle struct { } // newPopupStyle creates a new popup style. -func newPopupStyle(colors *theme.Colors, width, height int) popupStyle { - general := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFFFF")). - Width(width - 2). - Height(height - 2). - Border(lipgloss.NormalBorder()). - BorderForeground(colors.Color1) - - heading := lipgloss.NewStyle(). - Margin(1, 0, 1, 0). - Width(width - 2). - Align(lipgloss.Center). - Italic(true) +func newPopupStyle(colors *theme.Colors, width, height int, headingText string) popupStyle { + border := popup.NewTitleBorder(headingText, width, height, colors.Color1, lipgloss.NormalBorder()) list := lipgloss.NewStyle(). - Margin(0, 4). + Margin(1, 4). Width(width - 2). Height(10) @@ -60,8 +49,7 @@ func newPopupStyle(colors *theme.Colors, width, height int) popupStyle { Foreground(colors.Color2) return popupStyle{ - general: general, - heading: heading, + border: border, list: list, choice: choice, choiceTitle: choiceTitle, From 708656e57f994ce179ec8409d15224f58cbf94f1 Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Thu, 18 Apr 2024 23:27:13 +0200 Subject: [PATCH 2/8] feat: better popups for help --- internal/ui/browser/browser.go | 6 +---- internal/ui/browser/help.go | 35 ++++++++++++++++++--------- internal/ui/browser/help_style.go | 40 ------------------------------- 3 files changed, 25 insertions(+), 56 deletions(-) delete mode 100644 internal/ui/browser/help_style.go diff --git a/internal/ui/browser/browser.go b/internal/ui/browser/browser.go index 4e36233..a754a4a 100644 --- a/internal/ui/browser/browser.go +++ b/internal/ui/browser/browser.go @@ -439,11 +439,7 @@ func (m Model) downloadItem(msg backend.DownloadItemMsg) (tea.Model, tea.Cmd) { // showHelp shows the help menu as a popup. func (m Model) showHelp() (tea.Model, tea.Cmd) { - bg := m.View() - width := m.width * 2 / 3 - height := 17 - - m.popup = newHelp(m.style.colors, bg, width, height, m.FullHelp()) + m.popup = newHelp(m.style.colors, m.View(), m.FullHelp()) m.keymap.SetEnabled(false) return m, nil } diff --git a/internal/ui/browser/help.go b/internal/ui/browser/help.go index 3bf30ba..4592f24 100644 --- a/internal/ui/browser/help.go +++ b/internal/ui/browser/help.go @@ -1,31 +1,46 @@ package browser import ( + "strings" + "github.com/TypicalAM/goread/internal/theme" "github.com/TypicalAM/goread/internal/ui/popup" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/muesli/ansi" ) // Help is a popup that displays the help page. type Help struct { + border popup.TitleBorder help help.Model - style helpStyle + box lipgloss.Style keyBinds [][]key.Binding overlay popup.Overlay } // newHelp returns a new Help popup. -func newHelp(colors *theme.Colors, bgRaw string, width, height int, binds [][]key.Binding) *Help { - style := newHelpStyle(colors, width, height) - help := help.New() - help.Styles = style.help +func newHelp(colors *theme.Colors, bgRaw string, binds [][]key.Binding) *Help { + helpModel := help.New() + helpModel.Styles = help.Styles{} + helpModel.Styles.FullDesc = lipgloss.NewStyle(). + Foreground(colors.Text) + helpModel.Styles.FullKey = lipgloss.NewStyle(). + Foreground(colors.Color2) + helpModel.Styles.FullSeparator = lipgloss.NewStyle(). + Foreground(colors.TextDark) + + rendered := helpModel.FullHelpView(binds) + width := ansi.PrintableRuneWidth(rendered[:strings.IndexRune(rendered, '\n')-1]) + 6 + height := strings.Count(rendered, "\n") + 7 + border := popup.NewTitleBorder("Help", width, height, colors.Color1, lipgloss.NormalBorder()) return &Help{ - help: help, - style: style, + help: helpModel, + border: border, + box: lipgloss.NewStyle().Margin(2, 2), keyBinds: binds, overlay: popup.NewOverlay(bgRaw, width, height), } @@ -43,8 +58,6 @@ func (h Help) Update(_ tea.Msg) (tea.Model, tea.Cmd) { // View renders the popup. func (h Help) View() string { - return h.overlay.WrapView(h.style.box.Render(lipgloss.JoinVertical(lipgloss.Center, - h.style.title.Render("Help"), - h.help.FullHelpView(h.keyBinds), - ))) + list := h.box.Render(h.help.FullHelpView(h.keyBinds)) + return h.overlay.WrapView(h.border.Render(list)) } diff --git a/internal/ui/browser/help_style.go b/internal/ui/browser/help_style.go deleted file mode 100644 index 295c9d2..0000000 --- a/internal/ui/browser/help_style.go +++ /dev/null @@ -1,40 +0,0 @@ -package browser - -import ( - "github.com/TypicalAM/goread/internal/theme" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/lipgloss" -) - -// helpStyle is the style for the help popup. -type helpStyle struct { - help help.Styles - title lipgloss.Style - box lipgloss.Style -} - -// newHelpStyle creates a new style for the help popup title and the help model. -func newHelpStyle(colors *theme.Colors, width, height int) helpStyle { - styles := help.Styles{} - styles.FullDesc = lipgloss.NewStyle(). - Foreground(colors.Text) - styles.FullKey = lipgloss.NewStyle(). - Foreground(colors.Color2) - styles.FullSeparator = lipgloss.NewStyle(). - Foreground(colors.TextDark) - - return helpStyle{ - help: styles, - title: lipgloss.NewStyle(). - Align(lipgloss.Center). - Margin(1, 0). - Width(width - 2). - Foreground(colors.Text). - Italic(true), - box: lipgloss.NewStyle(). - Width(width - 2). - Height(height - 2). - Border(lipgloss.NormalBorder()). - BorderForeground(colors.Color1), - } -} From b301656b912f08b927142fb8a9502a71c697fc0e Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Fri, 19 Apr 2024 00:13:31 +0200 Subject: [PATCH 3/8] fix: non-overlapping binds --- internal/ui/browser/browser.go | 82 +++++++++++++++++++++++++++++++--- internal/ui/browser/help.go | 4 +- internal/ui/tab/feed/feed.go | 20 +++++++-- 3 files changed, 95 insertions(+), 11 deletions(-) diff --git a/internal/ui/browser/browser.go b/internal/ui/browser/browser.go index a754a4a..a6affb2 100644 --- a/internal/ui/browser/browser.go +++ b/internal/ui/browser/browser.go @@ -136,7 +136,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - log.Println(m.msg) return m, m.backend.FetchCategories("") case category.ChosenFeedMsg: @@ -157,7 +156,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - log.Println(m.msg) return m, m.backend.FetchFeeds(msg.Parent) case tab.NewTabMsg: @@ -333,9 +331,9 @@ func (m Model) ShortHelp() []key.Binding { // FullHelp returns the full help for the browser. func (m Model) FullHelp() [][]key.Binding { - result := [][]key.Binding{m.ShortHelp()} - result = append(result, m.tabs[m.activeTab].FullHelp()...) - return result + browserHelp := [][]key.Binding{m.ShortHelp()} + childHelp := m.tabs[m.activeTab].FullHelp() + return prettifyHelp(browserHelp, childHelp) } // waitForSize waits for the window size to be set and loads the tab @@ -509,3 +507,77 @@ func unwrapErrs(err error) error { } return err } + +// prettifyHelp prettifies the help columns, removes the lower level bind if a higher one precedes it +func prettifyHelp(first, second [][]key.Binding) [][]key.Binding { + toDelSecond := make([]int, 0) + toDelThird := make([]int, 0) + + for _, elem := range first[0] { + // Second col + for idx2, elem2 := range second[0] { + if strIntersect(elem.Keys(), elem2.Keys()) { + toDelSecond = append(toDelSecond, idx2) + } + } + + // Third col + for idx2, elem2 := range second[1] { + if strIntersect(elem.Keys(), elem2.Keys()) { + toDelThird = append(toDelThird, idx2) + } + } + } + + second[0] = deleteBinds(second[0], toDelSecond) + second[1] = deleteBinds(second[1], toDelThird) + + for _, elem := range second[0] { + // Third col + for idx2, elem2 := range second[1] { + if strIntersect(elem.Keys(), elem2.Keys()) { + toDelThird = append(toDelThird, idx2) + } + } + } + + second[1] = deleteBinds(second[1], toDelThird) + return append(first, second...) +} + +// deleteBinds removes items from a bind slice via a list of indices +func deleteBinds(arr []key.Binding, indices []int) []key.Binding { + if len(indices) == 0 { + return arr + } + + result := make([]key.Binding, 0, len(arr)-len(indices)) + for idx, elem := range arr { + add := true + for _, toDel := range indices { + if idx == toDel { + add = false + break + } + } + + if add { + result = append(result, elem) + } + } + + return result +} + +// strIntersect checks if two string slices have a non-empty intersection +func strIntersect(first, second []string) bool { + for _, elem := range first { + for _, elem2 := range second { + if elem == elem2 { + return true + } + } + } + + return false +} diff --git a/internal/ui/browser/help.go b/internal/ui/browser/help.go index 4592f24..8115dba 100644 --- a/internal/ui/browser/help.go +++ b/internal/ui/browser/help.go @@ -34,13 +34,13 @@ func newHelp(colors *theme.Colors, bgRaw string, binds [][]key.Binding) *Help { rendered := helpModel.FullHelpView(binds) width := ansi.PrintableRuneWidth(rendered[:strings.IndexRune(rendered, '\n')-1]) + 6 - height := strings.Count(rendered, "\n") + 7 + height := strings.Count(rendered, "\n") + 5 border := popup.NewTitleBorder("Help", width, height, colors.Color1, lipgloss.NormalBorder()) return &Help{ help: helpModel, border: border, - box: lipgloss.NewStyle().Margin(2, 2), + box: lipgloss.NewStyle().Margin(1, 2, 1, 4), keyBinds: binds, overlay: popup.NewOverlay(bgRaw, width, height), } diff --git a/internal/ui/tab/feed/feed.go b/internal/ui/tab/feed/feed.go index ad2b95f..e0ad7a0 100644 --- a/internal/ui/tab/feed/feed.go +++ b/internal/ui/tab/feed/feed.go @@ -383,10 +383,22 @@ func (m Model) ShortHelp() []key.Binding { // FullHelp returns the full help for the tab func (m Model) FullHelp() [][]key.Binding { - if !m.viewportOpen && m.viewportFocused { - result := [][]key.Binding{m.ShortHelp()} - result = append(result, m.list.FullHelp()...) - return result + if !m.viewportFocused { + listHelp := make([]key.Binding, 0) + for _, bind := range m.list.ShortHelp() { + shouldAdd := true + for _, key := range bind.Keys() { + if key == "?" { + shouldAdd = false + } + } + + if shouldAdd { + listHelp = append(listHelp, bind) + } + } + + return [][]key.Binding{m.ShortHelp(), listHelp} } return [][]key.Binding{m.ShortHelp(), { From 4ee071ffeee44e171a4893d589ba06f4030d63c3 Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Fri, 19 Apr 2024 11:15:57 +0200 Subject: [PATCH 4/8] fix: move the overlay handling to the browser --- internal/ui/browser/browser.go | 30 ++++++--- internal/ui/browser/help.go | 15 +++-- internal/ui/popup/choice.go | 17 +++-- internal/ui/popup/overlay.go | 107 ++++++++++++++++++++++++++++++ internal/ui/popup/popup.go | 107 ++---------------------------- internal/ui/tab/category/popup.go | 17 +++-- internal/ui/tab/overview/popup.go | 18 +++-- 7 files changed, 178 insertions(+), 133 deletions(-) create mode 100644 internal/ui/popup/overlay.go diff --git a/internal/ui/browser/browser.go b/internal/ui/browser/browser.go index a6affb2..808293b 100644 --- a/internal/ui/browser/browser.go +++ b/internal/ui/browser/browser.go @@ -65,7 +65,8 @@ func (k *Keymap) SetEnabled(enabled bool) { // Model is used to store the state of the application type Model struct { - popup tea.Model + popup popup.Window + overlay popup.Overlay backend *backend.Backend style style msg string @@ -162,14 +163,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.createNewTab(msg) case backend.NewItemMsg: + background := m.View() + switch msg.Sender.(type) { case overview.Model: - m.popup = overview.NewPopup(m.style.colors, m.View(), "", "") + m.popup = overview.NewPopup(m.style.colors, "", "") case category.Model: - m.popup = category.NewPopup(m.style.colors, m.View(), "", "", msg.Sender.Title()) + m.popup = category.NewPopup(m.style.colors, "", "", msg.Sender.Title()) case feed.Model: } + width, height := m.popup.GetSize() + m.overlay = popup.NewOverlay(background, width, height) m.keymap.SetEnabled(false) return m, m.popup.Init() @@ -178,9 +183,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.Sender.(type) { case overview.Model: - m.popup = overview.NewPopup(m.style.colors, m.View(), oldName, oldDesc) + m.popup = overview.NewPopup(m.style.colors, oldName, oldDesc) case category.Model: - m.popup = category.NewPopup(m.style.colors, m.View(), oldName, oldDesc, msg.Sender.Title()) + m.popup = category.NewPopup(m.style.colors, oldName, oldDesc, msg.Sender.Title()) case feed.Model: } @@ -202,7 +207,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case backend.MakeChoiceMsg: - m.popup = popup.NewChoice(m.style.colors, m.View(), msg.Question, msg.Default) + background := m.View() + m.popup = popup.NewChoice(m.style.colors, msg.Question, msg.Default) + width, height := m.popup.GetSize() + m.overlay = popup.NewOverlay(background, width, height) m.keymap.SetEnabled(false) return m, m.popup.Init() @@ -283,7 +291,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // If we are showing a popup, we need to update the popup if m.popup != nil { - m.popup, cmd = m.popup.Update(msg) + newPopup, cmd := m.popup.Update(msg) + m.popup = newPopup.(popup.Window) return m, cmd } @@ -303,7 +312,7 @@ func (m Model) View() string { } if m.popup != nil { - return m.popup.View() + return m.overlay.WrapView(m.popup.View()) } var b strings.Builder @@ -437,7 +446,10 @@ func (m Model) downloadItem(msg backend.DownloadItemMsg) (tea.Model, tea.Cmd) { // showHelp shows the help menu as a popup. func (m Model) showHelp() (tea.Model, tea.Cmd) { - m.popup = newHelp(m.style.colors, m.View(), m.FullHelp()) + background := m.View() + m.popup = newHelp(m.style.colors, m.FullHelp()) + width, height := m.popup.GetSize() + m.overlay = popup.NewOverlay(background, width, height) m.keymap.SetEnabled(false) return m, nil } diff --git a/internal/ui/browser/help.go b/internal/ui/browser/help.go index 8115dba..b127fbc 100644 --- a/internal/ui/browser/help.go +++ b/internal/ui/browser/help.go @@ -18,11 +18,12 @@ type Help struct { help help.Model box lipgloss.Style keyBinds [][]key.Binding - overlay popup.Overlay + width int + height int } // newHelp returns a new Help popup. -func newHelp(colors *theme.Colors, bgRaw string, binds [][]key.Binding) *Help { +func newHelp(colors *theme.Colors, binds [][]key.Binding) *Help { helpModel := help.New() helpModel.Styles = help.Styles{} helpModel.Styles.FullDesc = lipgloss.NewStyle(). @@ -42,10 +43,16 @@ func newHelp(colors *theme.Colors, bgRaw string, binds [][]key.Binding) *Help { border: border, box: lipgloss.NewStyle().Margin(1, 2, 1, 4), keyBinds: binds, - overlay: popup.NewOverlay(bgRaw, width, height), + width: width, + height: height, } } +// GetSize returns the size of the popup. +func (h Help) GetSize() (width int, height int) { + return h.width, h.height +} + // Init initializes the popup. func (h Help) Init() tea.Cmd { return nil @@ -59,5 +66,5 @@ func (h Help) Update(_ tea.Msg) (tea.Model, tea.Cmd) { // View renders the popup. func (h Help) View() string { list := h.box.Render(h.help.FullHelpView(h.keyBinds)) - return h.overlay.WrapView(h.border.Render(list)) + return h.border.Render(list) } diff --git a/internal/ui/popup/choice.go b/internal/ui/popup/choice.go index 10d7e76..e5a2b61 100644 --- a/internal/ui/popup/choice.go +++ b/internal/ui/popup/choice.go @@ -15,20 +15,22 @@ type ChoiceResultMsg struct { type Choice struct { style style question string - overlay Overlay selected bool + width int + height int } // NewChoice creates a new Choice popup. -func NewChoice(colors *theme.Colors, bgRaw string, question string, defaultChoice bool) Choice { +func NewChoice(colors *theme.Colors, question string, defaultChoice bool) Choice { width := len(question) + 16 height := 7 return Choice{ style: newStyle(colors, width, height), - overlay: NewOverlay(bgRaw, width, height), question: question, selected: defaultChoice, + width: width, + height: height, } } @@ -75,8 +77,13 @@ func (c Choice) View() string { question := c.style.question.Render(c.question) buttons := lipgloss.JoinHorizontal(lipgloss.Top, okButton, cancelButton) ui := lipgloss.JoinVertical(lipgloss.Center, question, buttons) - dialog := lipgloss.Place(c.overlay.width-2, c.overlay.height-2, lipgloss.Center, lipgloss.Center, ui) - return c.overlay.WrapView(c.style.border.Render(dialog)) + dialog := lipgloss.Place(c.width-2, c.height-2, lipgloss.Center, lipgloss.Center, ui) + return c.style.border.Render(dialog) +} + +// GetSize returns the size of the popup. +func (c Choice) GetSize() (width, height int) { + return c.width, c.height } // makeChoice returns a tea.Cmd that tells the parent model about the choice. diff --git a/internal/ui/popup/overlay.go b/internal/ui/popup/overlay.go new file mode 100644 index 0000000..98ecb4f --- /dev/null +++ b/internal/ui/popup/overlay.go @@ -0,0 +1,107 @@ +package popup + +import ( + "strings" + + "github.com/muesli/ansi" +) + +// Overlay allows you to overlay text on top of a background and achieve a popup. +type Overlay struct { + textAbove string + textBelow string + rowPrefix []string + rowSuffix []string + width int + height int +} + +// NewOverlay creates a new overlay and computes the necessary indices. +func NewOverlay(bgRaw string, width, height int) Overlay { + bg := strings.Split(bgRaw, "\n") + bgWidth := ansi.PrintableRuneWidth(bg[0]) + bgHeight := len(bg) + + if height > bgHeight { + height = bgHeight + } + if width > bgWidth { + width = bgWidth + } + + startRow := (bgHeight - height) / 2 + startCol := (bgWidth - width) / 2 + + rowPrefix := make([]string, height) + rowSuffix := make([]string, height) + + for i, text := range bg[startRow : startRow+height] { + popupStart := findPrintIndex(text, startCol) + popupEnd := findPrintIndex(text, startCol+width) + + if popupStart != -1 { + rowPrefix[i] = text[:popupStart] + } else { + rowPrintable := ansi.PrintableRuneWidth(text) + rowPrefix[i] = text + strings.Repeat(" ", startCol-rowPrintable) + } + + if popupEnd != -1 { + rowSuffix[i] = text[popupEnd:] + } else { + rowSuffix[i] = "" + } + } + + prefix := strings.Join(bg[:startRow], "\n") + suffix := strings.Join(bg[startRow+height:], "\n") + + return Overlay{ + rowPrefix: rowPrefix, + rowSuffix: rowSuffix, + width: width, + height: height, + textAbove: prefix, + textBelow: suffix, + } +} + +// WrapView overlays the given text on top of the background. +// TODO: Maybe handle the box here. It's a bit weird to have to do it in the view. +func (p Overlay) WrapView(view string) string { + var b strings.Builder + b.WriteString(p.textAbove) + b.WriteRune('\n') + + lines := strings.Split(view, "\n") + for i := 0; i < len(lines) && i < p.height; i++ { + b.WriteString(p.rowPrefix[i]) + b.WriteString(lines[i]) + b.WriteString(p.rowSuffix[i]) + b.WriteRune('\n') + } + + b.WriteString(p.textBelow) + return b.String() +} + +// Width returns the width of the popup window. +func (p Overlay) Width() int { + return p.width +} + +// Height returns the height of the popup window. +func (p Overlay) Height() int { + return p.height +} + +// findPrintIndex finds the print index, that is what string index corresponds to the given printable rune index. +func findPrintIndex(str string, index int) int { + for i := len(str) - 1; i >= 0; i-- { + if ansi.PrintableRuneWidth(str[:i]) == index { + return i + } + } + + return -1 +} diff --git a/internal/ui/popup/popup.go b/internal/ui/popup/popup.go index 98ecb4f..381562c 100644 --- a/internal/ui/popup/popup.go +++ b/internal/ui/popup/popup.go @@ -1,107 +1,10 @@ package popup -import ( - "strings" +import tea "github.com/charmbracelet/bubbletea" - "github.com/muesli/ansi" -) +// Window represents a popup window. +type Window interface { + tea.Model -// Overlay allows you to overlay text on top of a background and achieve a popup. -type Overlay struct { - textAbove string - textBelow string - rowPrefix []string - rowSuffix []string - width int - height int -} - -// NewOverlay creates a new overlay and computes the necessary indices. -func NewOverlay(bgRaw string, width, height int) Overlay { - bg := strings.Split(bgRaw, "\n") - bgWidth := ansi.PrintableRuneWidth(bg[0]) - bgHeight := len(bg) - - if height > bgHeight { - height = bgHeight - } - if width > bgWidth { - width = bgWidth - } - - startRow := (bgHeight - height) / 2 - startCol := (bgWidth - width) / 2 - - rowPrefix := make([]string, height) - rowSuffix := make([]string, height) - - for i, text := range bg[startRow : startRow+height] { - popupStart := findPrintIndex(text, startCol) - popupEnd := findPrintIndex(text, startCol+width) - - if popupStart != -1 { - rowPrefix[i] = text[:popupStart] - } else { - rowPrintable := ansi.PrintableRuneWidth(text) - rowPrefix[i] = text + strings.Repeat(" ", startCol-rowPrintable) - } - - if popupEnd != -1 { - rowSuffix[i] = text[popupEnd:] - } else { - rowSuffix[i] = "" - } - } - - prefix := strings.Join(bg[:startRow], "\n") - suffix := strings.Join(bg[startRow+height:], "\n") - - return Overlay{ - rowPrefix: rowPrefix, - rowSuffix: rowSuffix, - width: width, - height: height, - textAbove: prefix, - textBelow: suffix, - } -} - -// WrapView overlays the given text on top of the background. -// TODO: Maybe handle the box here. It's a bit weird to have to do it in the view. -func (p Overlay) WrapView(view string) string { - var b strings.Builder - b.WriteString(p.textAbove) - b.WriteRune('\n') - - lines := strings.Split(view, "\n") - for i := 0; i < len(lines) && i < p.height; i++ { - b.WriteString(p.rowPrefix[i]) - b.WriteString(lines[i]) - b.WriteString(p.rowSuffix[i]) - b.WriteRune('\n') - } - - b.WriteString(p.textBelow) - return b.String() -} - -// Width returns the width of the popup window. -func (p Overlay) Width() int { - return p.width -} - -// Height returns the height of the popup window. -func (p Overlay) Height() int { - return p.height -} - -// findPrintIndex finds the print index, that is what string index corresponds to the given printable rune index. -func findPrintIndex(str string, index int) int { - for i := len(str) - 1; i >= 0; i-- { - if ansi.PrintableRuneWidth(str[:i]) == index { - return i - } - } - - return -1 + GetSize() (width, height int) } diff --git a/internal/ui/tab/category/popup.go b/internal/ui/tab/category/popup.go index d851380..f7c0c6f 100644 --- a/internal/ui/tab/category/popup.go +++ b/internal/ui/tab/category/popup.go @@ -2,7 +2,6 @@ package category import ( "github.com/TypicalAM/goread/internal/theme" - "github.com/TypicalAM/goread/internal/ui/popup" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -33,17 +32,17 @@ type Popup struct { oldName string oldURL string parent string - overlay popup.Overlay focused focusedField editing bool + width int + height int } // NewPopup returns a new feed popup. -func NewPopup(colors *theme.Colors, bgRaw string, oldName, oldURL, parent string) Popup { +func NewPopup(colors *theme.Colors, oldName, oldURL, parent string) Popup { width := 40 height := 7 - overlay := popup.NewOverlay(bgRaw, width, height) editing := oldName != "" || oldURL != "" nameInput := textinput.New() @@ -70,7 +69,6 @@ func NewPopup(colors *theme.Colors, bgRaw string, oldName, oldURL, parent string nameInput.Focus() return Popup{ - overlay: overlay, style: style, nameInput: nameInput, urlInput: urlInput, @@ -78,6 +76,8 @@ func NewPopup(colors *theme.Colors, bgRaw string, oldName, oldURL, parent string oldURL: oldURL, parent: parent, editing: editing, + width: width, + height: height, } } @@ -144,7 +144,12 @@ func (p Popup) View() string { name := p.style.itemField.Render(p.nameInput.View()) url := p.style.itemField.Render(p.urlInput.View()) listItem := p.style.listItem.Render(lipgloss.JoinVertical(lipgloss.Left, itemTitle, name, url)) - return p.overlay.WrapView(p.style.border.Render(listItem)) + return p.style.border.Render(listItem) +} + +// GetSize returns the size of the popup. +func (p Popup) GetSize() (width, height int) { + return p.width, p.height } // confirm creates a message that confirms the user's choice. diff --git a/internal/ui/tab/overview/popup.go b/internal/ui/tab/overview/popup.go index 3a830de..e7a6275 100644 --- a/internal/ui/tab/overview/popup.go +++ b/internal/ui/tab/overview/popup.go @@ -3,7 +3,6 @@ package overview import ( "github.com/TypicalAM/goread/internal/backend/rss" "github.com/TypicalAM/goread/internal/theme" - "github.com/TypicalAM/goread/internal/ui/popup" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -33,19 +32,18 @@ type Popup struct { descInput textinput.Model style popupStyle oldName string - overlay popup.Overlay focused focusedField editing bool + width int + height int } // NewPopup creates a new popup window in which the user can choose a new category. -func NewPopup(colors *theme.Colors, bgRaw string, oldName, oldDesc string) Popup { +func NewPopup(colors *theme.Colors, oldName, oldDesc string) Popup { width := 46 height := 14 - overlay := popup.NewOverlay(bgRaw, width, height) editing := oldName != "" || oldDesc != "" - nameInput := textinput.New() nameInput.CharLimit = 30 nameInput.Width = width - 15 @@ -71,13 +69,14 @@ func NewPopup(colors *theme.Colors, bgRaw string, oldName, oldDesc string) Popup } return Popup{ - overlay: overlay, style: style, nameInput: nameInput, descInput: descInput, oldName: oldName, focused: focusedField, editing: editing, + width: width, + height: height, } } @@ -195,7 +194,12 @@ func (p Popup) View() string { } toList := p.style.list.Render(lipgloss.JoinVertical(lipgloss.Top, renderedChoices...)) - return p.overlay.WrapView(p.style.border.Render(toList)) + return p.style.border.Render(toList) +} + +// GetSize returns the size of the popup. +func (p Popup) GetSize() (width, height int) { + return p.width, p.height } // confirm creates a message that confirms the user's choice. From 81f73f38b5314f8c5aa8fe3168e6cfd4c66d774f Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Fri, 19 Apr 2024 12:00:10 +0200 Subject: [PATCH 5/8] feat: resizable popups --- internal/ui/browser/browser.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/ui/browser/browser.go b/internal/ui/browser/browser.go index 808293b..50b759a 100644 --- a/internal/ui/browser/browser.go +++ b/internal/ui/browser/browser.go @@ -218,7 +218,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.keymap.SetEnabled(true) m.popup = nil - // TODO: Resize the popup if necessary case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height @@ -228,6 +227,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.tabs[i] = m.tabs[i].SetSize(m.width, m.height-5) } + // Delete the popup, update the overlay and rerender + if m.popup != nil { + popupBackup := m.popup + m.popup = nil + background := m.View() + m.popup = popupBackup + width, height := m.popup.GetSize() + m.overlay = popup.NewOverlay(background, width, height) + } + case backend.SetEnableKeybindMsg: m.keymap.SetEnabled(bool(msg)) log.Println("Disabling keybinds, propagating") From 3db734733f4be694577d5ed8d00e31aae89bc896 Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Fri, 19 Apr 2024 12:40:43 +0200 Subject: [PATCH 6/8] feat: cleanup and error popup --- internal/ui/browser/browser.go | 99 +++++++++---------- internal/ui/popup/{ => lollypops}/choice.go | 6 +- .../{style.go => lollypops/choice_style.go} | 17 ++-- internal/ui/popup/lollypops/error.go | 64 ++++++++++++ internal/ui/popup/lollypops/error_style.go | 39 ++++++++ internal/ui/tab/category/category.go | 4 +- internal/ui/tab/feed/feed.go | 4 +- internal/ui/tab/overview/welcome.go | 4 +- 8 files changed, 168 insertions(+), 69 deletions(-) rename internal/ui/popup/{ => lollypops}/choice.go (95%) rename internal/ui/popup/{style.go => lollypops/choice_style.go} (58%) create mode 100644 internal/ui/popup/lollypops/error.go create mode 100644 internal/ui/popup/lollypops/error_style.go diff --git a/internal/ui/browser/browser.go b/internal/ui/browser/browser.go index 50b759a..df4c907 100644 --- a/internal/ui/browser/browser.go +++ b/internal/ui/browser/browser.go @@ -11,6 +11,7 @@ import ( "github.com/TypicalAM/goread/internal/backend/rss" "github.com/TypicalAM/goread/internal/theme" "github.com/TypicalAM/goread/internal/ui/popup" + "github.com/TypicalAM/goread/internal/ui/popup/lollypops" "github.com/TypicalAM/goread/internal/ui/tab" "github.com/TypicalAM/goread/internal/ui/tab/category" "github.com/TypicalAM/goread/internal/ui/tab/feed" @@ -116,8 +117,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { log.Printf("Error fetching data in tab %d: %v \n", m.activeTab, msg.Err) updated, _ := m.tabs[m.activeTab].Update(msg) m.tabs[m.activeTab] = updated.(tab.Tab) - m.msg = fmt.Sprintf("%s: %s", msg.Description, unwrapErrs(msg.Err)) - return m, nil + return m.displayError(fmt.Sprintf("%s: %s", msg.Description, unwrapErrs(msg.Err))) case overview.ChosenCategoryMsg: m.popup = nil @@ -125,18 +125,20 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.IsEdit { if err := m.backend.Rss.UpdateCategory(msg.OldName, msg.Name, msg.Desc); err != nil { - m.msg = fmt.Sprintf("Error updating category: %s", unwrapErrs(err)) - } else { - m.msg = fmt.Sprintf("Updated category %s", msg.Name) - } - } else { - if err := m.backend.Rss.AddCategory(msg.Name, msg.Desc); err != nil { - m.msg = fmt.Sprintf("Error adding category: %s", unwrapErrs(err)) - } else { - m.msg = fmt.Sprintf("Added category %s", msg.Name) + m, cmd := m.displayError(fmt.Sprintf("Error updating category: %s", unwrapErrs(err))) + return m, tea.Sequence(cmd, m.backend.FetchCategories("")) } + + m.msg = fmt.Sprintf("Updated category %s", msg.Name) + return m, m.backend.FetchCategories("") + } + + if err := m.backend.Rss.AddCategory(msg.Name, msg.Desc); err != nil { + m, cmd := m.displayError(fmt.Sprintf("Error adding category: %s", unwrapErrs(err))) + return m, tea.Sequence(cmd, m.backend.FetchCategories("")) } + m.msg = fmt.Sprintf("Added category %s", msg.Name) return m, m.backend.FetchCategories("") case category.ChosenFeedMsg: @@ -145,13 +147,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.IsEdit { if err := m.backend.Rss.UpdateFeed(msg.Parent, msg.OldName, msg.Name, msg.URL); err != nil { - m.msg = fmt.Sprintf("Error updating feed: %s", unwrapErrs(err)) + m.displayError(fmt.Sprintf("Error updating feed: %s", unwrapErrs(err))) } else { m.msg = fmt.Sprintf("Updated feed %s", msg.Name) } } else { if err := m.backend.Rss.AddFeed(msg.Parent, msg.Name, msg.URL); err != nil { - m.msg = fmt.Sprintf("Error adding feed: %s", unwrapErrs(err)) + m.displayError(fmt.Sprintf("Error adding feed: %s", unwrapErrs(err))) } else { m.msg = fmt.Sprintf("Added feed %s", msg.Name) } @@ -163,34 +165,31 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.createNewTab(msg) case backend.NewItemMsg: - background := m.View() + m.keymap.SetEnabled(false) switch msg.Sender.(type) { case overview.Model: - m.popup = overview.NewPopup(m.style.colors, "", "") + return m.showPopup(overview.NewPopup(m.style.colors, "", "")) case category.Model: - m.popup = category.NewPopup(m.style.colors, "", "", msg.Sender.Title()) + return m.showPopup(category.NewPopup(m.style.colors, "", "", msg.Sender.Title())) case feed.Model: } - width, height := m.popup.GetSize() - m.overlay = popup.NewOverlay(background, width, height) - m.keymap.SetEnabled(false) - return m, m.popup.Init() + return m, nil case backend.EditItemMsg: oldName, oldDesc := msg.OldFields[0], msg.OldFields[1] + m.keymap.SetEnabled(false) switch msg.Sender.(type) { case overview.Model: - m.popup = overview.NewPopup(m.style.colors, oldName, oldDesc) + return m.showPopup(overview.NewPopup(m.style.colors, oldName, oldDesc)) case category.Model: - m.popup = category.NewPopup(m.style.colors, oldName, oldDesc, msg.Sender.Title()) + return m.showPopup(category.NewPopup(m.style.colors, oldName, oldDesc, msg.Sender.Title())) case feed.Model: } - m.keymap.SetEnabled(false) - return m, m.popup.Init() + return m, nil case backend.DeleteItemMsg: return m.deleteItem(msg) @@ -207,14 +206,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case backend.MakeChoiceMsg: - background := m.View() - m.popup = popup.NewChoice(m.style.colors, msg.Question, msg.Default) - width, height := m.popup.GetSize() - m.overlay = popup.NewOverlay(background, width, height) - m.keymap.SetEnabled(false) - return m, m.popup.Init() + return m.showPopup(lollypops.NewChoice(m.style.colors, msg.Question, msg.Default)) - case popup.ChoiceResultMsg: + case lollypops.ChoiceResultMsg, lollypops.ErrorResultMsg: m.keymap.SetEnabled(true) m.popup = nil @@ -229,12 +223,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Delete the popup, update the overlay and rerender if m.popup != nil { - popupBackup := m.popup - m.popup = nil - background := m.View() - m.popup = popupBackup - width, height := m.popup.GetSize() - m.overlay = popup.NewOverlay(background, width, height) + return m.showPopup(m.popup) } case backend.SetEnableKeybindMsg: @@ -291,7 +280,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case key.Matches(msg, m.keymap.ShowHelp): - return m.showHelp() + return m.showPopup(newHelp(m.style.colors, m.FullHelp())) case key.Matches(msg, m.keymap.ToggleOfflineMode): return m.toggleOffline() @@ -419,13 +408,13 @@ func (m Model) deleteItem(msg backend.DeleteItemMsg) (tea.Model, tea.Cmd) { case overview.Model: cmd = m.backend.FetchCategories("") if err := m.backend.Rss.RemoveCategory(msg.ItemName); err != nil { - m.msg = fmt.Sprintf("Error deleting category %s: %s", msg.ItemName, unwrapErrs(err)) + m.displayError(fmt.Sprintf("Error deleting category %s: %s", msg.ItemName, unwrapErrs(err))) } case category.Model: cmd = m.backend.FetchFeeds(m.tabs[m.activeTab].Title()) if err := m.backend.Rss.RemoveFeed(m.tabs[m.activeTab].Title(), msg.ItemName); err != nil { - m.msg = fmt.Sprintf("Error deleting feed %s: %s", msg.ItemName, unwrapErrs(err)) + m.displayError(fmt.Sprintf("Error deleting feed %s: %s", msg.ItemName, unwrapErrs(err))) } case feed.Model: @@ -433,11 +422,11 @@ func (m Model) deleteItem(msg backend.DeleteItemMsg) (tea.Model, tea.Cmd) { if msg.Sender.Title() == rss.DownloadedFeedsName { index, err := strconv.Atoi(msg.ItemName) if err != nil { - m.msg = fmt.Sprintf("Error deleting download %s: %s", msg.ItemName, unwrapErrs(err)) + m.displayError(fmt.Sprintf("Error deleting download %s: %s", msg.ItemName, unwrapErrs(err))) } if err := m.backend.Cache.RemoveFromDownloaded(index); err != nil { - m.msg = fmt.Sprintf("Error deleting download %s: %s", msg.ItemName, unwrapErrs(err)) + m.displayError(fmt.Sprintf("Error deleting download %s: %s", msg.ItemName, unwrapErrs(err))) } } } @@ -453,16 +442,6 @@ func (m Model) downloadItem(msg backend.DownloadItemMsg) (tea.Model, tea.Cmd) { return m, m.backend.DownloadItem(msg.FeedName, msg.Index) } -// showHelp shows the help menu as a popup. -func (m Model) showHelp() (tea.Model, tea.Cmd) { - background := m.View() - m.popup = newHelp(m.style.colors, m.FullHelp()) - width, height := m.popup.GetSize() - m.overlay = popup.NewOverlay(background, width, height) - m.keymap.SetEnabled(false) - return m, nil -} - // toggleOffline toggles the offline mode func (m Model) toggleOffline() (tea.Model, tea.Cmd) { m.offline = !m.offline @@ -478,6 +457,22 @@ func (m Model) toggleOffline() (tea.Model, tea.Cmd) { return m, nil } +// displayError shows a popup which displays the error +func (m Model) displayError(msg string) (Model, tea.Cmd) { + m.msg = msg + return m.showPopup(lollypops.NewError(m.style.colors, msg)) +} + +// showPopup tells the model to show the popup +func (m Model) showPopup(window popup.Window) (Model, tea.Cmd) { + m.popup = nil + background := m.View() + m.popup = window + width, height := m.popup.GetSize() + m.overlay = popup.NewOverlay(background, width, height) + return m, m.popup.Init() // TODO: Maybe don't call this while resizing +} + // renderTabBar renders the tab bar at the top of the screen func (m Model) renderTabBar() string { tabs := make([]string, len(m.tabs)) diff --git a/internal/ui/popup/choice.go b/internal/ui/popup/lollypops/choice.go similarity index 95% rename from internal/ui/popup/choice.go rename to internal/ui/popup/lollypops/choice.go index e5a2b61..bd35afd 100644 --- a/internal/ui/popup/choice.go +++ b/internal/ui/popup/lollypops/choice.go @@ -1,4 +1,4 @@ -package popup +package lollypops import ( "github.com/TypicalAM/goread/internal/theme" @@ -13,7 +13,7 @@ type ChoiceResultMsg struct { // Choice is a popup that presents a yes/no choice to the user. type Choice struct { - style style + style choiceStyle question string selected bool width int @@ -26,7 +26,7 @@ func NewChoice(colors *theme.Colors, question string, defaultChoice bool) Choice height := 7 return Choice{ - style: newStyle(colors, width, height), + style: newChoiceStyle(colors, width, height), question: question, selected: defaultChoice, width: width, diff --git a/internal/ui/popup/style.go b/internal/ui/popup/lollypops/choice_style.go similarity index 58% rename from internal/ui/popup/style.go rename to internal/ui/popup/lollypops/choice_style.go index c3005c7..686b481 100644 --- a/internal/ui/popup/style.go +++ b/internal/ui/popup/lollypops/choice_style.go @@ -1,20 +1,21 @@ -package popup +package lollypops import ( "github.com/TypicalAM/goread/internal/theme" + "github.com/TypicalAM/goread/internal/ui/popup" "github.com/charmbracelet/lipgloss" ) -// style is the style of the choice popup -type style struct { - border TitleBorder +// choiceStyle is the style of the choice popup +type choiceStyle struct { + border popup.TitleBorder button lipgloss.Style activeButton lipgloss.Style question lipgloss.Style } -// newStyle creates a new style for the choice popup -func newStyle(colors *theme.Colors, width, height int) style { +// newChoiceStyle creates a new style for the choice popup +func newChoiceStyle(colors *theme.Colors, width, height int) choiceStyle { buttonStyle := lipgloss.NewStyle(). Foreground(colors.TextDark). Background(colors.BgDark). @@ -31,8 +32,8 @@ func newStyle(colors *theme.Colors, width, height int) style { Italic(true). Align(lipgloss.Center) - return style{ - border: NewTitleBorder("Confirm choice", width, height, colors.Color1, lipgloss.NormalBorder()), + return choiceStyle{ + border: popup.NewTitleBorder("Confirm choice", width, height, colors.Color1, lipgloss.NormalBorder()), button: buttonStyle, activeButton: activeButtonStyle, question: question, diff --git a/internal/ui/popup/lollypops/error.go b/internal/ui/popup/lollypops/error.go new file mode 100644 index 0000000..309705e --- /dev/null +++ b/internal/ui/popup/lollypops/error.go @@ -0,0 +1,64 @@ +package lollypops + +import ( + "github.com/TypicalAM/goread/internal/theme" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// ErrorResultMsg is the message sent when the user presses ok +type ErrorResultMsg struct{} + +// AppError is a popup that presents an error to the user. +type AppError struct { + style errorStyle + msg string + width int + height int +} + +// NewError creates a new error popup. +func NewError(colors *theme.Colors, message string) AppError { + width := len(message) + 16 + height := 7 + + return AppError{ + style: newErrorStyle(colors, width, height), + msg: message, + width: width, + height: height, + } +} + +// Init initializes the popup. +func (ae AppError) Init() tea.Cmd { + return nil +} + +// Update handles messages. +func (ae AppError) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if msg, ok := msg.(tea.KeyMsg); ok && msg.String() == "enter" { + return ae, ae.confirm() + } + + return ae, nil +} + +// View renders the popup. +func (ae AppError) View() string { + button := ae.style.activeButton.Render("OK") + msg := ae.style.msg.Render(ae.msg) + ui := lipgloss.JoinVertical(lipgloss.Center, msg, button) + dialog := lipgloss.Place(ae.width-2, ae.height-2, lipgloss.Center, lipgloss.Center, ui) + return ae.style.border.Render(dialog) +} + +// GetSize returns the size of the popup. +func (ae AppError) GetSize() (width, height int) { + return ae.width, ae.height +} + +// confirm returns a tea.Cmd that tells the parent model about the confirmation. +func (ae AppError) confirm() tea.Cmd { + return func() tea.Msg { return ErrorResultMsg{} } +} diff --git a/internal/ui/popup/lollypops/error_style.go b/internal/ui/popup/lollypops/error_style.go new file mode 100644 index 0000000..19e9461 --- /dev/null +++ b/internal/ui/popup/lollypops/error_style.go @@ -0,0 +1,39 @@ +package lollypops + +import ( + "github.com/TypicalAM/goread/internal/theme" + "github.com/TypicalAM/goread/internal/ui/popup" + "github.com/charmbracelet/lipgloss" +) + +// errorStyle is the style of the error popup +type errorStyle struct { + border popup.TitleBorder + activeButton lipgloss.Style + msg lipgloss.Style +} + +// newErrorStyle creates a new style for the error popup +func newErrorStyle(colors *theme.Colors, width, height int) errorStyle { + errorColor := lipgloss.Color("#f08ca8") + buttonStyle := lipgloss.NewStyle(). + Foreground(colors.TextDark). + Background(colors.BgDark). + Padding(0, 2). + Margin(0, 1) + + activeButtonStyle := buttonStyle.Copy(). + Foreground(colors.Text). + Background(colors.Color3) + + msg := lipgloss.NewStyle(). + Width(width). + Margin(1, 0). + Align(lipgloss.Center) + + return errorStyle{ + border: popup.NewTitleBorder("Error", width, height, errorColor, lipgloss.NormalBorder()), + activeButton: activeButtonStyle, + msg: msg, + } +} diff --git a/internal/ui/tab/category/category.go b/internal/ui/tab/category/category.go index b3b46f5..13ab40a 100644 --- a/internal/ui/tab/category/category.go +++ b/internal/ui/tab/category/category.go @@ -5,7 +5,7 @@ import ( "github.com/TypicalAM/goread/internal/backend" "github.com/TypicalAM/goread/internal/theme" - "github.com/TypicalAM/goread/internal/ui/popup" + "github.com/TypicalAM/goread/internal/ui/popup/lollypops" "github.com/TypicalAM/goread/internal/ui/simplelist" "github.com/TypicalAM/goread/internal/ui/tab" "github.com/charmbracelet/bubbles/key" @@ -85,7 +85,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.keymap.SetEnabled(bool(msg)) return m, nil - case popup.ChoiceResultMsg: + case lollypops.ChoiceResultMsg: if !msg.Result { return m, nil } diff --git a/internal/ui/tab/feed/feed.go b/internal/ui/tab/feed/feed.go index e0ad7a0..8f12d06 100644 --- a/internal/ui/tab/feed/feed.go +++ b/internal/ui/tab/feed/feed.go @@ -7,7 +7,7 @@ import ( "github.com/TypicalAM/goread/internal/backend" "github.com/TypicalAM/goread/internal/theme" - "github.com/TypicalAM/goread/internal/ui/popup" + "github.com/TypicalAM/goread/internal/ui/popup/lollypops" "github.com/TypicalAM/goread/internal/ui/tab" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" @@ -127,7 +127,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.keymap.SetEnabled(bool(msg)) return m, nil - case popup.ChoiceResultMsg: + case lollypops.ChoiceResultMsg: if !msg.Result { return m, nil } diff --git a/internal/ui/tab/overview/welcome.go b/internal/ui/tab/overview/welcome.go index b6ca7aa..d9356e2 100644 --- a/internal/ui/tab/overview/welcome.go +++ b/internal/ui/tab/overview/welcome.go @@ -5,7 +5,7 @@ import ( "github.com/TypicalAM/goread/internal/backend" "github.com/TypicalAM/goread/internal/theme" - "github.com/TypicalAM/goread/internal/ui/popup" + "github.com/TypicalAM/goread/internal/ui/popup/lollypops" "github.com/TypicalAM/goread/internal/ui/simplelist" "github.com/TypicalAM/goread/internal/ui/tab" "github.com/charmbracelet/bubbles/key" @@ -89,7 +89,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.keymap.SetEnabled(bool(msg)) return m, nil - case popup.ChoiceResultMsg: + case lollypops.ChoiceResultMsg: if !msg.Result { return m, nil } From f94cb3ca8950f2bb634bce28176308336d0e6c56 Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Fri, 19 Apr 2024 12:57:30 +0200 Subject: [PATCH 7/8] fix: better reserved category popups --- internal/ui/tab/overview/popup.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/internal/ui/tab/overview/popup.go b/internal/ui/tab/overview/popup.go index e7a6275..87c9492 100644 --- a/internal/ui/tab/overview/popup.go +++ b/internal/ui/tab/overview/popup.go @@ -36,6 +36,7 @@ type Popup struct { editing bool width int height int + reserved bool } // NewPopup creates a new popup window in which the user can choose a new category. @@ -44,6 +45,8 @@ func NewPopup(colors *theme.Colors, oldName, oldDesc string) Popup { height := 14 editing := oldName != "" || oldDesc != "" + reserved := oldName == rss.AllFeedsName || oldName == rss.DownloadedFeedsName + nameInput := textinput.New() nameInput.CharLimit = 30 nameInput.Width = width - 15 @@ -52,7 +55,11 @@ func NewPopup(colors *theme.Colors, oldName, oldDesc string) Popup { descInput.CharLimit = 30 descInput.Width = width - 22 descInput.Prompt = "Description: " - focusedField := allField + + focused := allField + if oldName == rss.DownloadedFeedsName { + focused = downloadedField + } style := popupStyle{} if editing { @@ -61,10 +68,10 @@ func NewPopup(colors *theme.Colors, oldName, oldDesc string) Popup { style = newPopupStyle(colors, width, height, "New category") } - if editing { + if editing && !reserved { nameInput.SetValue(oldName) descInput.SetValue(oldDesc) - focusedField = nameField + focused = nameField nameInput.Focus() } @@ -73,10 +80,11 @@ func NewPopup(colors *theme.Colors, oldName, oldDesc string) Popup { nameInput: nameInput, descInput: descInput, oldName: oldName, - focused: focusedField, + focused: focused, editing: editing, width: width, height: height, + reserved: reserved, } } @@ -90,6 +98,10 @@ func (p Popup) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd if msg, ok := msg.(tea.KeyMsg); ok { + if p.reserved && msg.String() == "enter" { + return p, nil + } + switch msg.String() { case "down", "tab": switch p.focused { From ee06e3ffe4978733f6a13f20c4380853db57d284 Mon Sep 17 00:00:00 2001 From: TypicalAM Date: Fri, 19 Apr 2024 13:09:04 +0200 Subject: [PATCH 8/8] refactor: cleaner error popup handling --- internal/ui/browser/browser.go | 49 +++++++++++++++++-------------- internal/ui/tab/overview/popup.go | 2 +- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/internal/ui/browser/browser.go b/internal/ui/browser/browser.go index df4c907..fc2a98b 100644 --- a/internal/ui/browser/browser.go +++ b/internal/ui/browser/browser.go @@ -117,7 +117,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { log.Printf("Error fetching data in tab %d: %v \n", m.activeTab, msg.Err) updated, _ := m.tabs[m.activeTab].Update(msg) m.tabs[m.activeTab] = updated.(tab.Tab) - return m.displayError(fmt.Sprintf("%s: %s", msg.Description, unwrapErrs(msg.Err))) + errMsg := fmt.Sprintf("%s: %s", msg.Description, unwrapErrs(msg.Err)) + return m.showPopup(lollypops.NewError(m.style.colors, errMsg)) case overview.ChosenCategoryMsg: m.popup = nil @@ -125,7 +126,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.IsEdit { if err := m.backend.Rss.UpdateCategory(msg.OldName, msg.Name, msg.Desc); err != nil { - m, cmd := m.displayError(fmt.Sprintf("Error updating category: %s", unwrapErrs(err))) + errMsg := fmt.Sprintf("Error updating category: %s", unwrapErrs(err)) + m, cmd := m.showPopup(lollypops.NewError(m.style.colors, errMsg)) return m, tea.Sequence(cmd, m.backend.FetchCategories("")) } @@ -134,7 +136,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if err := m.backend.Rss.AddCategory(msg.Name, msg.Desc); err != nil { - m, cmd := m.displayError(fmt.Sprintf("Error adding category: %s", unwrapErrs(err))) + errMsg := fmt.Sprintf("Error adding category: %s", unwrapErrs(err)) + m, cmd := m.showPopup(lollypops.NewError(m.style.colors, errMsg)) return m, tea.Sequence(cmd, m.backend.FetchCategories("")) } @@ -147,18 +150,22 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.IsEdit { if err := m.backend.Rss.UpdateFeed(msg.Parent, msg.OldName, msg.Name, msg.URL); err != nil { - m.displayError(fmt.Sprintf("Error updating feed: %s", unwrapErrs(err))) - } else { - m.msg = fmt.Sprintf("Updated feed %s", msg.Name) - } - } else { - if err := m.backend.Rss.AddFeed(msg.Parent, msg.Name, msg.URL); err != nil { - m.displayError(fmt.Sprintf("Error adding feed: %s", unwrapErrs(err))) - } else { - m.msg = fmt.Sprintf("Added feed %s", msg.Name) + errMsg := fmt.Sprintf("Error updating feed: %s", unwrapErrs(err)) + m, cmd := m.showPopup(lollypops.NewError(m.style.colors, errMsg)) + return m, tea.Batch(cmd, m.backend.FetchFeeds(msg.Parent)) } + + m.msg = fmt.Sprintf("Updated feed %s", msg.Name) + return m, m.backend.FetchFeeds(msg.Parent) + } + + if err := m.backend.Rss.AddFeed(msg.Parent, msg.Name, msg.URL); err != nil { + errMsg := fmt.Sprintf("Error adding feed: %s", unwrapErrs(err)) + m, cmd := m.showPopup(lollypops.NewError(m.style.colors, errMsg)) + return m, tea.Batch(cmd, m.backend.FetchFeeds(msg.Parent)) } + m.msg = fmt.Sprintf("Added feed %s", msg.Name) return m, m.backend.FetchFeeds(msg.Parent) case tab.NewTabMsg: @@ -408,13 +415,15 @@ func (m Model) deleteItem(msg backend.DeleteItemMsg) (tea.Model, tea.Cmd) { case overview.Model: cmd = m.backend.FetchCategories("") if err := m.backend.Rss.RemoveCategory(msg.ItemName); err != nil { - m.displayError(fmt.Sprintf("Error deleting category %s: %s", msg.ItemName, unwrapErrs(err))) + errMsg := fmt.Sprintf("Error deleting category %s: %s", msg.ItemName, unwrapErrs(err)) + return m.showPopup(lollypops.NewError(m.style.colors, errMsg)) } case category.Model: cmd = m.backend.FetchFeeds(m.tabs[m.activeTab].Title()) if err := m.backend.Rss.RemoveFeed(m.tabs[m.activeTab].Title(), msg.ItemName); err != nil { - m.displayError(fmt.Sprintf("Error deleting feed %s: %s", msg.ItemName, unwrapErrs(err))) + errMsg := fmt.Sprintf("Error deleting feed %s: %s", msg.ItemName, unwrapErrs(err)) + return m.showPopup(lollypops.NewError(m.style.colors, errMsg)) } case feed.Model: @@ -422,11 +431,13 @@ func (m Model) deleteItem(msg backend.DeleteItemMsg) (tea.Model, tea.Cmd) { if msg.Sender.Title() == rss.DownloadedFeedsName { index, err := strconv.Atoi(msg.ItemName) if err != nil { - m.displayError(fmt.Sprintf("Error deleting download %s: %s", msg.ItemName, unwrapErrs(err))) + errMsg := fmt.Sprintf("Error deleting download %s: %s", msg.ItemName, unwrapErrs(err)) + return m.showPopup(lollypops.NewError(m.style.colors, errMsg)) } if err := m.backend.Cache.RemoveFromDownloaded(index); err != nil { - m.displayError(fmt.Sprintf("Error deleting download %s: %s", msg.ItemName, unwrapErrs(err))) + errMsg := fmt.Sprintf("Error deleting download %s: %s", msg.ItemName, unwrapErrs(err)) + return m.showPopup(lollypops.NewError(m.style.colors, errMsg)) } } } @@ -457,12 +468,6 @@ func (m Model) toggleOffline() (tea.Model, tea.Cmd) { return m, nil } -// displayError shows a popup which displays the error -func (m Model) displayError(msg string) (Model, tea.Cmd) { - m.msg = msg - return m.showPopup(lollypops.NewError(m.style.colors, msg)) -} - // showPopup tells the model to show the popup func (m Model) showPopup(window popup.Window) (Model, tea.Cmd) { m.popup = nil diff --git a/internal/ui/tab/overview/popup.go b/internal/ui/tab/overview/popup.go index 87c9492..5641cd0 100644 --- a/internal/ui/tab/overview/popup.go +++ b/internal/ui/tab/overview/popup.go @@ -98,7 +98,7 @@ func (p Popup) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd if msg, ok := msg.(tea.KeyMsg); ok { - if p.reserved && msg.String() == "enter" { + if p.reserved { return p, nil }