From 4035d52b153ce753184706cef635167f2f96fd28 Mon Sep 17 00:00:00 2001 From: Aaron Martell Date: Mon, 24 Aug 2026 18:41:28 -0500 Subject: [PATCH 1/2] bug: switch to altscreen to address bubbletea regression --- cmd/book/collection.go | 7 +- cmd/book/main.go | 19 +++++ cmd/book/mark.go | 16 ++-- cmd/book/shelf.go | 7 +- internal/model/collection_model.go | 117 +++++++++++++------------- internal/model/mark_model.go | 86 ++++++++++--------- internal/model/shelf_model.go | 131 ++++++++++++++--------------- internal/model/tea.go | 42 +++++++++ 8 files changed, 241 insertions(+), 184 deletions(-) diff --git a/cmd/book/collection.go b/cmd/book/collection.go index b55fa6d..8ffce4e 100644 --- a/cmd/book/collection.go +++ b/cmd/book/collection.go @@ -1,7 +1,6 @@ package cmd import ( - tea "charm.land/bubbletea/v2" "github.com/polymorcodeus/book/internal/book" "github.com/polymorcodeus/book/internal/model" ) @@ -20,13 +19,11 @@ func collections(bs *book.BookShelves, shelfName string, format string, config * return book.PrintCatalog(names, format) } - _, err := tea.NewProgram(collectionRootScreen(bs, "list", config)).Run() - return err + return runProgram(collectionRootScreen(bs, "list", config)) } func addCollection(bs *book.BookShelves, config *book.Config) error { - _, err := tea.NewProgram(collectionRootScreen(bs, "add", config)).Run() - return err + return runProgram(collectionRootScreen(bs, "add", config)) } func collectionRootScreen(bs *book.BookShelves, action string, config *book.Config) model.RootScreen { diff --git a/cmd/book/main.go b/cmd/book/main.go index 04db0e3..f3cf0e9 100644 --- a/cmd/book/main.go +++ b/cmd/book/main.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" + tea "charm.land/bubbletea/v2" "github.com/polymorcodeus/gofiglet" altsrc "github.com/urfave/cli-altsrc/v3" alttoml "github.com/urfave/cli-altsrc/v3/toml" @@ -18,6 +19,7 @@ import ( "github.com/polymorcodeus/book/internal/book" "github.com/polymorcodeus/book/internal/catalog" + "github.com/polymorcodeus/book/internal/model" ) var ( @@ -533,3 +535,20 @@ func Main() { log.Fatal(err) } } + +// runProgram runs a RootScreen TUI and prints its completion output after the +// program exits. The interactive form renders in the alternate screen buffer +// and is discarded on exit, so the caller prints the result below the banner +// instead of leaving selector fragments behind. +func runProgram(screen model.RootScreen) error { + m, err := tea.NewProgram(screen).Run() + if err != nil { + return err + } + if rp, ok := m.(model.ResultProvider); ok { + if v := rp.ResultView(); v != "" { + fmt.Print(v) + } + } + return nil +} diff --git a/cmd/book/mark.go b/cmd/book/mark.go index b34d7e0..b0940f6 100644 --- a/cmd/book/mark.go +++ b/cmd/book/mark.go @@ -6,7 +6,6 @@ import ( "net/url" "strings" - tea "charm.land/bubbletea/v2" "github.com/polymorcodeus/book/internal/book" "github.com/polymorcodeus/book/internal/catalog" "github.com/polymorcodeus/book/internal/model" @@ -14,8 +13,7 @@ import ( ) func mark(bs *book.BookShelves, config *book.Config) error { - _, err := tea.NewProgram(markRootScreen(bs, &book.Mark{}, "get", config)).Run() - return err + return runProgram(markRootScreen(bs, &book.Mark{}, "get", config)) } func marks(bs *book.BookShelves, shelfName string, collectionName string, format string, config *book.Config) error { @@ -31,13 +29,11 @@ func marks(bs *book.BookShelves, shelfName string, collectionName string, format } return book.PrintCatalog(collection, format) } - _, err := tea.NewProgram(markRootScreen(bs, &book.Mark{}, "list", config)).Run() - return err + return runProgram(markRootScreen(bs, &book.Mark{}, "list", config)) } func editMark(bs *book.BookShelves, config *book.Config) error { - _, err := tea.NewProgram(markRootScreen(bs, &book.Mark{}, "edit", config)).Run() - return err + return runProgram(markRootScreen(bs, &book.Mark{}, "edit", config)) } func searchMarks(query string, tags string, shelfName string, collectionName string, format string, config *book.Config) error { @@ -139,13 +135,11 @@ func addMark(bs *book.BookShelves, URL string, tags string, shelfName string, co return nil } - _, err := tea.NewProgram(markRootScreen(bs, &mark, "add", config)).Run() - return err + return runProgram(markRootScreen(bs, &mark, "add", config)) } func removeMark(bs *book.BookShelves, config *book.Config) error { - _, err := tea.NewProgram(markRootScreen(bs, &book.Mark{}, "delete", config)).Run() - return err + return runProgram(markRootScreen(bs, &book.Mark{}, "delete", config)) } func markRootScreen(bs *book.BookShelves, mark *book.Mark, action string, config *book.Config) model.RootScreen { diff --git a/cmd/book/shelf.go b/cmd/book/shelf.go index f483cf9..5bbcbd0 100644 --- a/cmd/book/shelf.go +++ b/cmd/book/shelf.go @@ -1,7 +1,6 @@ package cmd import ( - tea "charm.land/bubbletea/v2" "github.com/polymorcodeus/book/internal/book" "github.com/polymorcodeus/book/internal/model" ) @@ -19,13 +18,11 @@ func shelves(bs *book.BookShelves, format string, config *book.Config) error { return book.PrintCatalog(names, format) } - _, err := tea.NewProgram(shelfRootScreen(bs, "list", config)).Run() - return err + return runProgram(shelfRootScreen(bs, "list", config)) } func addShelf(bs *book.BookShelves, config *book.Config) error { - _, err := tea.NewProgram(shelfRootScreen(bs, "add", config)).Run() - return err + return runProgram(shelfRootScreen(bs, "add", config)) } func shelfRootScreen(bs *book.BookShelves, action string, config *book.Config) model.RootScreen { diff --git a/internal/model/collection_model.go b/internal/model/collection_model.go index c87bb86..1b95a89 100644 --- a/internal/model/collection_model.go +++ b/internal/model/collection_model.go @@ -75,13 +75,12 @@ func (m getCollectionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m getCollectionModel) View() tea.View { - s := m.get.book.styles - t := m.get.book.tmpls - - if m.action == "list" && m.get.book.form.State == huh.StateCompleted { - return renderCompletedView(s, t, "collection-list", m.get.shelf) + if m.get.book.form.State == huh.StateCompleted || m.get.book.width <= 0 { + return altScreenView("") } + s := m.get.book.styles + // Form (left side) v := strings.TrimSuffix(m.get.book.form.View(), "\n\n") form := s.Form.Render(v) @@ -100,13 +99,7 @@ func (m getCollectionModel) View() tea.View { currentShelf = lipglossDimmer(s.StatusHeader, "Picked Shelf", displayShelf) - const statusWidth = 68 - statusMarginLeft := m.get.book.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() - status = s.Status. - Height(10). - Width(statusWidth). - MarginLeft(statusMarginLeft). - Render(currentShelf) + status = m.get.book.statusPanel(form, currentShelf, 10) } errors := m.get.book.form.Errors() @@ -120,12 +113,21 @@ func (m getCollectionModel) View() tea.View { if len(errors) > 0 { footer = m.get.book.appErrorBoundaryView("") } - return tea.NewView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) + return altScreenView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) +} + +// ResultView returns the completion output for the caller to print after the +// program exits. +func (m getCollectionModel) ResultView() string { + if m.get.book.form.State != huh.StateCompleted || m.action != "list" { + return "" + } + return renderCompletedView(m.get.book.styles, m.get.book.tmpls, "collection-list", m.get.shelf).Content } // GetCollectionForm to be used for editing descriptions/names in future func GetCollectionForm(bs *book.BookShelves, config *book.Config, action string) getCollectionModel { - m := collectionModel{book: &Book{width: maxWidth}} + m := collectionModel{book: &Book{width: 0}} m.book.styles = NewStyles(config) m.book.tmpls = config.Templates m.book.shelves = bs @@ -240,56 +242,57 @@ func (m editCollectionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m editCollectionModel) View() tea.View { + if m.editor.book.form.State == huh.StateCompleted || m.editor.book.width <= 0 { + return altScreenView("") + } + s := m.editor.book.styles - t := m.editor.book.tmpls - - switch m.editor.book.form.State { - case huh.StateCompleted: - return renderCompletedView(s, t, "collection-add", m.editor.collection) - default: - // Form (left side) - v := strings.TrimSuffix(m.editor.book.form.View(), "\n\n") - form := s.Form.Render(v) - - // Status (right side) - var status string - { - var ( - editShelfName string - editCollectionName string - editCollectionDesc string - ) - - editShelfName = s.StatusHeader.Render("Picked Shelf") + "\n" + m.editor.shelf.Name + "\n\n" - editCollectionName = lipglossDimmer(s.StatusHeader, "Collection Name", m.editor.collection.Name) - editCollectionDesc = lipglossDimmer(s.StatusHeader, "Collection Description", m.editor.collection.Description) - - const statusWidth = 68 - statusMarginLeft := m.editor.book.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() - status = s.Status. - Height(10). - Width(statusWidth). - MarginLeft(statusMarginLeft). - Render(editShelfName + editCollectionName + editCollectionDesc) - } - errors := m.editor.book.form.Errors() - header := m.editor.book.appBoundaryView("book collection editing system") - if len(errors) > 0 { - header = m.editor.book.appErrorBoundaryView(m.editor.book.errorView()) - } - body := lipgloss.JoinHorizontal(lipgloss.Left, form, status) + // Form (left side) + v := strings.TrimSuffix(m.editor.book.form.View(), "\n\n") + form := s.Form.Render(v) - footer := m.editor.book.appBoundaryView(m.editor.book.form.Help().ShortHelpView(m.editor.book.form.KeyBinds())) - if len(errors) > 0 { - footer = m.editor.book.appErrorBoundaryView("") - } - return tea.NewView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) + // Status (right side) + var status string + { + var ( + editShelfName string + editCollectionName string + editCollectionDesc string + ) + + editShelfName = s.StatusHeader.Render("Picked Shelf") + "\n" + m.editor.shelf.Name + "\n\n" + editCollectionName = lipglossDimmer(s.StatusHeader, "Collection Name", m.editor.collection.Name) + editCollectionDesc = lipglossDimmer(s.StatusHeader, "Collection Description", m.editor.collection.Description) + + status = m.editor.book.statusPanel(form, editShelfName+editCollectionName+editCollectionDesc, 10) + } + + errors := m.editor.book.form.Errors() + header := m.editor.book.appBoundaryView("book collection editing system") + if len(errors) > 0 { + header = m.editor.book.appErrorBoundaryView(m.editor.book.errorView()) + } + body := lipgloss.JoinHorizontal(lipgloss.Left, form, status) + + footer := m.editor.book.appBoundaryView(m.editor.book.form.Help().ShortHelpView(m.editor.book.form.KeyBinds())) + if len(errors) > 0 { + footer = m.editor.book.appErrorBoundaryView("") + } + return altScreenView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) +} + +// ResultView returns the completion output for the caller to print after the +// program exits. +func (m editCollectionModel) ResultView() string { + if m.editor.book.form.State != huh.StateCompleted { + return "" } + return renderCompletedView(m.editor.book.styles, m.editor.book.tmpls, "collection-add", m.editor.collection).Content } func editCollectionForm(bs *book.BookShelves, shelf *book.Shelf, config *book.Config, action string) editCollectionModel { - m := collectionModel{book: &Book{width: maxWidth}} + m := collectionModel{book: &Book{width: 0}} m.book.styles = NewStyles(config) m.book.tmpls = config.Templates m.book.shelves = bs diff --git a/internal/model/mark_model.go b/internal/model/mark_model.go index 831009d..dc965bf 100644 --- a/internal/model/mark_model.go +++ b/internal/model/mark_model.go @@ -174,20 +174,11 @@ func (m getMarkModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m getMarkModel) View() tea.View { - s := m.get.book.styles - t := m.get.book.tmpls - - if m.action == "get" && m.get.book.form.State == huh.StateCompleted { - return renderCompletedView(s, t, "mark-get", m.get.mark) + if m.get.book.form.State == huh.StateCompleted || m.get.book.width <= 0 { + return altScreenView("") } - if m.action == "list" && m.get.book.form.State == huh.StateCompleted { - return renderCompletedView(s, t, "mark-list", m.get.collection) - } - - if m.action == "delete" && m.get.book.form.State == huh.StateCompleted { - return renderCompletedView(s, t, "mark-delete", m.get.collection) - } + s := m.get.book.styles // Form (left side) v := strings.TrimSuffix(m.get.book.form.View(), "\n\n") @@ -222,13 +213,7 @@ func (m getMarkModel) View() tea.View { currentCollection = lipglossDimmer(s.StatusHeader, "Picked Collection", displayCollection) currentMark = lipglossDimmer(s.StatusHeader, "Picked Mark", displayMark) - const statusWidth = 68 - statusMarginLeft := m.get.book.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() - status = s.Status. - Height(28). - Width(statusWidth). - MarginLeft(statusMarginLeft). - Render(currentShelf + currentCollection + currentMark) + status = m.get.book.statusPanel(form, currentShelf+currentCollection+currentMark, 28) } errors := m.get.book.form.Errors() @@ -245,12 +230,31 @@ func (m getMarkModel) View() tea.View { if len(errors) > 0 { footer = m.get.book.appErrorBoundaryView("") } - return tea.NewView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) + return altScreenView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) +} + +// ResultView returns the completion output for the caller to print after the +// program exits. +func (m getMarkModel) ResultView() string { + if m.get.book.form.State != huh.StateCompleted { + return "" + } + s := m.get.book.styles + t := m.get.book.tmpls + switch m.action { + case "get": + return renderCompletedView(s, t, "mark-get", m.get.mark).Content + case "list": + return renderCompletedView(s, t, "mark-list", m.get.collection).Content + case "delete": + return renderCompletedView(s, t, "mark-delete", m.get.collection).Content + } + return "" } // GetMarkForm returns a TUI model for navigating shelves, collections, and marks. func GetMarkForm(bs *book.BookShelves, mark *book.Mark, config *book.Config, action string) getMarkModel { - m := markModel{book: &Book{width: maxWidth}} + m := markModel{book: &Book{width: 0}} m.book.styles = NewStyles(config) m.book.tmpls = config.Templates m.book.shelves = bs @@ -384,16 +388,11 @@ func (m editMarkModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m editMarkModel) View() tea.View { - s := m.editor.book.styles - t := m.editor.book.tmpls - - if m.action == "add" && m.editor.book.form.State == huh.StateCompleted { - return renderCompletedView(s, t, "mark-add", m.editor.mark) + if m.editor.book.form.State == huh.StateCompleted || m.editor.book.width <= 0 { + return altScreenView("") } - if m.action == "edit" && m.editor.book.form.State == huh.StateCompleted { - return renderCompletedView(s, t, "mark-edit", m.editor.mark) - } + s := m.editor.book.styles // Form (left side) v := strings.TrimSuffix(m.editor.book.form.View(), "\n\n") @@ -416,13 +415,7 @@ func (m editMarkModel) View() tea.View { currentMark = s.StatusHeader.Render("Editing Mark") + "\n" + m.editor.mark.Name currentMark += "\n\n" + m.editor.mark.URL + "\n\n" + lipglossList(s.None, m.editor.mark.Tags) + "\n" - const statusWidth = 68 - statusMarginLeft := m.editor.book.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() - status = s.Status. - Height(28). - Width(statusWidth). - MarginLeft(statusMarginLeft). - Render(currentShelf + currentCollection + currentMark) + status = m.editor.book.statusPanel(form, currentShelf+currentCollection+currentMark, 28) } errors := m.editor.book.form.Errors() @@ -436,11 +429,28 @@ func (m editMarkModel) View() tea.View { if len(errors) > 0 { footer = m.editor.book.appErrorBoundaryView("") } - return tea.NewView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) + return altScreenView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) +} + +// ResultView returns the completion output for the caller to print after the +// program exits. +func (m editMarkModel) ResultView() string { + if m.editor.book.form.State != huh.StateCompleted { + return "" + } + s := m.editor.book.styles + t := m.editor.book.tmpls + switch m.action { + case "add": + return renderCompletedView(s, t, "mark-add", m.editor.mark).Content + case "edit": + return renderCompletedView(s, t, "mark-edit", m.editor.mark).Content + } + return "" } func editMarkForm(bs *book.BookShelves, mark *book.Mark, config *book.Config, action string) editMarkModel { - m := markModel{book: &Book{width: maxWidth}} + m := markModel{book: &Book{width: 0}} m.book.styles = NewStyles(config) m.book.tmpls = config.Templates m.book.shelves = bs diff --git a/internal/model/shelf_model.go b/internal/model/shelf_model.go index e43ff64..51d3e38 100644 --- a/internal/model/shelf_model.go +++ b/internal/model/shelf_model.go @@ -4,7 +4,6 @@ import ( "fmt" "slices" "strings" - "time" tea "charm.land/bubbletea/v2" "charm.land/huh/v2" @@ -67,7 +66,7 @@ func (m getShelfModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { editScreen := editShelfForm(m.get.book.shelves, m.get.shelf, m.get.config, m.action) return editScreen, editScreen.Init() case "list": - cmds = append(cmds, delayedQuit()) + cmds = append(cmds, tea.Quit) } } @@ -76,13 +75,12 @@ func (m getShelfModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View now returns tea.View (bubbletea v2 breaking change). func (m getShelfModel) View() tea.View { - s := m.get.book.styles - t := m.get.book.tmpls - - if m.action == "list" && m.get.book.form.State == huh.StateCompleted { - return renderCompletedView(s, t, "shelf-list", m.get.book.shelves) + if m.get.book.form.State == huh.StateCompleted || m.get.book.width <= 0 { + return altScreenView("") } + s := m.get.book.styles + // Form (left side) v := strings.TrimSuffix(m.get.book.form.View(), "\n\n") form := s.Form.Render(v) @@ -98,13 +96,7 @@ func (m getShelfModel) View() tea.View { currentShelf = s.StatusHeader.Render("Picked Shelf") + "\n" + "fake" + "\n\n" } - const statusWidth = 68 - statusMarginLeft := m.get.book.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() - status = s.Status. - Height(14). - Width(statusWidth). - MarginLeft(statusMarginLeft). - Render(currentShelf) + status = m.get.book.statusPanel(form, currentShelf, 14) } errors := m.get.book.form.Errors() @@ -118,12 +110,21 @@ func (m getShelfModel) View() tea.View { if len(errors) > 0 { footer = m.get.book.appErrorBoundaryView("") } - return tea.NewView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) + return altScreenView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) +} + +// ResultView returns the completion output for the caller to print after the +// program exits. +func (m getShelfModel) ResultView() string { + if m.get.book.form.State != huh.StateCompleted || m.action != "list" { + return "" + } + return renderCompletedView(m.get.book.styles, m.get.book.tmpls, "shelf-list", m.get.book.shelves).Content } // GetShelfForm to be used for editing descriptions/names in future func GetShelfForm(bs *book.BookShelves, config *book.Config, action string) getShelfModel { - m := shelfModel{book: &Book{width: maxWidth}} + m := shelfModel{book: &Book{width: 0}} m.book.styles = NewStyles(config) m.book.tmpls = config.Templates m.book.shelves = bs @@ -228,58 +229,59 @@ func (m editShelfModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View now returns tea.View (bubbletea v2 breaking change). func (m editShelfModel) View() tea.View { + if m.editor.book.form.State == huh.StateCompleted || m.editor.book.width <= 0 { + return altScreenView("") + } + s := m.editor.book.styles - t := m.editor.book.tmpls - - switch m.editor.book.form.State { - case huh.StateCompleted: - return renderCompletedView(s, t, "shelf-add", m.editor.collection) - default: - // Form (left side) - v := strings.TrimSuffix(m.editor.book.form.View(), "\n\n") - form := s.Form.Render(v) - - // Status (right side) - var status string - { - var ( - editShelfName string - editShelfDesc string - editCollectionName string - editCollectionDesc string - ) - - editShelfName = s.StatusHeader.Render("Shelf Name") + "\n" + m.editor.shelf.Name + "\n\n" - editShelfDesc = lipglossDimmer(s.StatusHeader, "Shelf Description", m.editor.shelf.Description) - editCollectionName = lipglossDimmer(s.StatusHeader, "Collection Name", m.editor.collection.Name) - editCollectionDesc = lipglossDimmer(s.StatusHeader, "Collection Description", m.editor.collection.Description) - - const statusWidth = 68 - statusMarginLeft := m.editor.book.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() - status = s.Status. - Height(14). - Width(statusWidth). - MarginLeft(statusMarginLeft). - Render(editShelfName + editShelfDesc + editCollectionName + editCollectionDesc) - } - errors := m.editor.book.form.Errors() - header := m.editor.book.appBoundaryView("book shelf editing system") - if len(errors) > 0 { - header = m.editor.book.appErrorBoundaryView(m.editor.book.errorView()) - } - body := lipgloss.JoinHorizontal(lipgloss.Left, form, status) + // Form (left side) + v := strings.TrimSuffix(m.editor.book.form.View(), "\n\n") + form := s.Form.Render(v) - footer := m.editor.book.appBoundaryView(m.editor.book.form.Help().ShortHelpView(m.editor.book.form.KeyBinds())) - if len(errors) > 0 { - footer = m.editor.book.appErrorBoundaryView("") - } - return tea.NewView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) + // Status (right side) + var status string + { + var ( + editShelfName string + editShelfDesc string + editCollectionName string + editCollectionDesc string + ) + + editShelfName = s.StatusHeader.Render("Shelf Name") + "\n" + m.editor.shelf.Name + "\n\n" + editShelfDesc = lipglossDimmer(s.StatusHeader, "Shelf Description", m.editor.shelf.Description) + editCollectionName = lipglossDimmer(s.StatusHeader, "Collection Name", m.editor.collection.Name) + editCollectionDesc = lipglossDimmer(s.StatusHeader, "Collection Description", m.editor.collection.Description) + + status = m.editor.book.statusPanel(form, editShelfName+editShelfDesc+editCollectionName+editCollectionDesc, 14) + } + + errors := m.editor.book.form.Errors() + header := m.editor.book.appBoundaryView("book shelf editing system") + if len(errors) > 0 { + header = m.editor.book.appErrorBoundaryView(m.editor.book.errorView()) + } + body := lipgloss.JoinHorizontal(lipgloss.Left, form, status) + + footer := m.editor.book.appBoundaryView(m.editor.book.form.Help().ShortHelpView(m.editor.book.form.KeyBinds())) + if len(errors) > 0 { + footer = m.editor.book.appErrorBoundaryView("") + } + return altScreenView(s.Base.Render(header + "\n" + body + "\n\n" + footer)) +} + +// ResultView returns the completion output for the caller to print after the +// program exits. +func (m editShelfModel) ResultView() string { + if m.editor.book.form.State != huh.StateCompleted { + return "" } + return renderCompletedView(m.editor.book.styles, m.editor.book.tmpls, "shelf-add", m.editor.collection).Content } func editShelfForm(bs *book.BookShelves, shelf *book.Shelf, config *book.Config, action string) editShelfModel { - m := shelfModel{book: &Book{width: maxWidth}} + m := shelfModel{book: &Book{width: 0}} m.book.styles = NewStyles(config) m.book.tmpls = config.Templates m.book.shelves = bs @@ -387,10 +389,3 @@ func (m *shelfModel) updateShelfFileCmd(action string) tea.Cmd { return nil } } - -// work around for https://github.com/charmbracelet/bubbletea/issues/1590 -func delayedQuit() tea.Cmd { - return tea.Tick(time.Millisecond*100, func(time.Time) tea.Msg { - return tea.QuitMsg{} - }) -} diff --git a/internal/model/tea.go b/internal/model/tea.go index d281620..aec9afe 100644 --- a/internal/model/tea.go +++ b/internal/model/tea.go @@ -13,6 +13,9 @@ import ( const maxWidth = 120 +// maxStatusWidth is the widest the side status panel is ever rendered. +const maxStatusWidth = 68 + // Styles holds the pre-built lipgloss styles for the TUI. type Styles struct { Base, @@ -108,6 +111,45 @@ func (b Book) appErrorBoundaryView(text string) string { ) } +// statusPanel renders the side status panel so it fits beside the form within +// the available width. It shrinks on narrow terminals instead of overflowing; +// rendering a frame wider than the terminal wraps lines and desyncs the +// renderer, leaving stale cells behind on the next (shorter) frame. +func (b Book) statusPanel(form, content string, height int) string { + statusWidth := maxStatusWidth + statusMarginLeft := b.width - statusWidth - lipgloss.Width(form) - b.styles.Status.GetMarginRight() + if statusMarginLeft < 0 { + statusWidth += statusMarginLeft + statusMarginLeft = 0 + } + if statusWidth < 0 { + statusWidth = 0 + } + return b.styles.Status. + Height(height). + Width(statusWidth). + MarginLeft(statusMarginLeft). + Render(content) +} + +// ResultProvider is implemented by models that produce a final output string +// to be printed by the caller after the program exits. Rendering the shorter +// completion view inside the TUI leaves stale form fragments on screen, so the +// form runs in the alternate screen buffer and the result is printed after the +// program returns. +type ResultProvider interface { + ResultView() string +} + +// altScreenView returns a view rendered in the alternate screen buffer. The +// interactive form is drawn there and discarded on exit, leaving the main +// screen clean for the caller to print the result. +func altScreenView(s string) tea.View { + v := tea.NewView(s) + v.AltScreen = true + return v +} + // RootScreen wraps a tea.Model and delegates the BubbleTea lifecycle to it. type RootScreen struct { Model tea.Model From 54601e3fb76aa077b0c33c914470fc14279bd41e Mon Sep 17 00:00:00 2001 From: Aaron Martell Date: Mon, 24 Aug 2026 18:42:38 -0500 Subject: [PATCH 2/2] chore: version bump --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 795460f..56130fb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.1.0 +v1.1.1