From b41c21c250ac733db5fbe15363f94a16252c4fd5 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Thu, 23 Jan 2025 16:49:26 -0500 Subject: [PATCH] refactor!: use the new bubbletea stringer api --- field_confirm.go | 8 +++- field_filepicker.go | 8 +++- field_input.go | 7 +++- field_multiselect.go | 8 +++- field_note.go | 8 +++- field_select.go | 8 +++- field_text.go | 7 +++- form.go | 11 ++++-- go.mod | 2 +- go.sum | 2 + group.go | 15 ++++---- huh_test.go | 92 ++++++++++++++++++++++---------------------- layout.go | 27 +++++++------ 13 files changed, 120 insertions(+), 83 deletions(-) diff --git a/field_confirm.go b/field_confirm.go index b7a57b40..67401cf7 100644 --- a/field_confirm.go +++ b/field_confirm.go @@ -232,7 +232,7 @@ func (c *Confirm) activeStyles() *FieldStyles { } // View renders the confirm field. -func (c *Confirm) View() string { +func (c *Confirm) View() fmt.Stringer { styles := c.activeStyles() var sb strings.Builder @@ -284,7 +284,11 @@ func (c *Confirm) View() string { style := lipgloss.NewStyle().Width(renderWidth).Align(lipgloss.Center) sb.WriteString(style.Render(buttonsRow)) - return styles.Base.Render(sb.String()) + + var s strings.Builder + s.WriteString(styles.Base.Render(sb.String())) + + return &s } // Run runs the confirm field in accessible mode. diff --git a/field_filepicker.go b/field_filepicker.go index 4dc9a982..c6b37d73 100644 --- a/field_filepicker.go +++ b/field_filepicker.go @@ -263,7 +263,7 @@ func (f *FilePicker) activeStyles() *FieldStyles { } // View renders the file field. -func (f *FilePicker) View() string { +func (f *FilePicker) View() fmt.Stringer { styles := f.activeStyles() var sb strings.Builder @@ -282,7 +282,11 @@ func (f *FilePicker) View() string { sb.WriteString(styles.TextInput.Placeholder.Render("No file selected.")) } } - return styles.Base.Render(sb.String()) + + var s strings.Builder + s.WriteString(styles.Base.Render(sb.String())) + + return &s } func (f *FilePicker) setPicking(v bool) { diff --git a/field_input.go b/field_input.go index 1029c5b9..15d6a9f3 100644 --- a/field_input.go +++ b/field_input.go @@ -380,7 +380,7 @@ func (i *Input) activeStyles() *FieldStyles { } // View renders the input field. -func (i *Input) View() string { +func (i *Input) View() fmt.Stringer { styles := i.activeStyles() // NB: since the method is on a pointer receiver these are being mutated. @@ -412,7 +412,10 @@ func (i *Input) View() string { } sb.WriteString(i.textinput.View()) - return styles.Base.Render(sb.String()) + var s strings.Builder + s.WriteString(styles.Base.Render(sb.String())) + + return &s } // Run runs the input field in accessible mode. diff --git a/field_multiselect.go b/field_multiselect.go index 2a9ef79a..48f6a84f 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -579,7 +579,7 @@ func (m *MultiSelect[T]) optionsView() string { } // View renders the multi-select field. -func (m *MultiSelect[T]) View() string { +func (m *MultiSelect[T]) View() fmt.Stringer { styles := m.activeStyles() m.viewport.SetContent(m.optionsView()) @@ -593,7 +593,11 @@ func (m *MultiSelect[T]) View() string { sb.WriteString(m.descriptionView() + "\n") } sb.WriteString(m.viewport.View()) - return styles.Base.Render(sb.String()) + + var s strings.Builder + s.WriteString(styles.Base.Render(sb.String())) + + return &s } func (m *MultiSelect[T]) printOptions() { diff --git a/field_note.go b/field_note.go index d009aa9e..6632e71e 100644 --- a/field_note.go +++ b/field_note.go @@ -218,7 +218,7 @@ func (n *Note) activeStyles() *FieldStyles { } // View renders the note field. -func (n *Note) View() string { +func (n *Note) View() fmt.Stringer { styles := n.activeStyles() sb := strings.Builder{} @@ -232,7 +232,11 @@ func (n *Note) View() string { if n.showNextButton { sb.WriteString(styles.Next.Render(n.nextLabel)) } - return styles.Card.Height(n.height).Render(sb.String()) + + var s strings.Builder + s.WriteString(styles.Card.Height(n.height).Render(sb.String())) + + return &s } // Run runs the note field. diff --git a/field_select.go b/field_select.go index b2735583..e62be2ad 100644 --- a/field_select.go +++ b/field_select.go @@ -592,7 +592,7 @@ func (s *Select[T]) optionsView() string { } // View renders the select field. -func (s *Select[T]) View() string { +func (s *Select[T]) View() fmt.Stringer { styles := s.activeStyles() s.viewport.SetContent(s.optionsView()) @@ -610,7 +610,11 @@ func (s *Select[T]) View() string { } } sb.WriteString(s.viewport.View()) - return styles.Base.Render(sb.String()) + + var v strings.Builder + v.WriteString(styles.Base.Render(sb.String())) + + return &v } // clearFilter clears the value of the filter. diff --git a/field_text.go b/field_text.go index dc01257d..3058c453 100644 --- a/field_text.go +++ b/field_text.go @@ -366,7 +366,7 @@ func (t *Text) activeTextAreaStyles() *textarea.StyleState { } // View renders the text field. -func (t *Text) View() string { +func (t *Text) View() fmt.Stringer { styles := t.activeStyles() textareaStyles := t.activeTextAreaStyles() @@ -392,7 +392,10 @@ func (t *Text) View() string { } sb.WriteString(t.textarea.View()) - return styles.Base.Render(sb.String()) + var v strings.Builder + v.WriteString(styles.Base.Render(sb.String())) + + return &v } // Run runs the text field. diff --git a/form.go b/form.go index 864f8462..dae2b751 100644 --- a/form.go +++ b/form.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "strings" "sync" "time" @@ -132,7 +133,7 @@ type Field interface { // Bubble Tea Model Init() (tea.Model, tea.Cmd) Update(tea.Msg) (tea.Model, tea.Cmd) - View() string + View() fmt.Stringer // Bubble Tea Events Blur() tea.Cmd @@ -608,12 +609,14 @@ func (f *Form) isGroupHidden(group *Group) bool { } // View renders the form. -func (f *Form) View() string { +func (f *Form) View() fmt.Stringer { + var s strings.Builder if f.quitting { - return "" + return &s } - return f.layout.View(f) + s.WriteString(f.layout.View(f).String()) + return &s } // Run runs the form. diff --git a/go.mod b/go.mod index 61de1585..cff53de6 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/catppuccin/go v0.2.0 github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.2.0.20250115152509-0c83e6f4c8d3 - github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250115153415-046a5ddf5b4a + github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123211610-443afa6fa0c1 github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250114171829-b67eb015d607 github.com/charmbracelet/x/ansi v0.7.0 github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 diff --git a/go.sum b/go.sum index a4970ad9..23b0b3f6 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.2.0.20250115152509-0c83e6f4c8d3 github.com/charmbracelet/bubbles/v2 v2.0.0-alpha.2.0.20250115152509-0c83e6f4c8d3/go.mod h1:QbWYWg5Y3UBTNDoTIPIgLHiGsTbBBO2D6q53lG7IR8M= github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250115153415-046a5ddf5b4a h1:Wct67LI3+EoDcAs3ujwWhTp8NNZUlkBAeOLgjyc2rOE= github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250115153415-046a5ddf5b4a/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8= +github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123211610-443afa6fa0c1 h1:tktnM4YimEWSYd58iZlPDB3Xz25/r94VYZZsHK5zWL0= +github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123211610-443afa6fa0c1/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8= github.com/charmbracelet/colorprofile v0.1.9 h1:5JnfvX+I9D6rRNu8xK3pgIqknaBVTXHU9pGu1jkZxLw= github.com/charmbracelet/colorprofile v0.1.9/go.mod h1:+jpmObxZl1Dab3H3IMVIPSZTsKcFpjJUv97G0dLqM60= github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250114171829-b67eb015d607 h1:lERE4ow371r5WMqQAt7Eqlg1A4tBNA8T4RLwdXnKyBo= diff --git a/group.go b/group.go index ffbc7fdb..f490e45d 100644 --- a/group.go +++ b/group.go @@ -1,6 +1,7 @@ package huh import ( + "fmt" "strings" "github.com/charmbracelet/bubbles/v2/help" @@ -126,7 +127,7 @@ func (g *Group) WithHeight(height int) *Group { g.viewport.SetHeight(height) g.selector.Range(func(_ int, field Field) bool { // A field height must not exceed the form height. - if height-1 <= lipgloss.Height(field.View()) { + if height-1 <= lipgloss.Height(field.View().String()) { field.WithHeight(height) } return true @@ -287,7 +288,7 @@ func (g *Group) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (g *Group) fullHeight() int { height := g.selector.Total() g.selector.Range(func(_ int, field Field) bool { - height += lipgloss.Height(field.View()) + height += lipgloss.Height(field.View().String()) return true }) return height @@ -301,12 +302,12 @@ func (g *Group) getContent() (int, string) { // if the focused field is requesting it be zoomed, only show that field. if g.selector.Selected().Zoom() { g.selector.Selected().WithHeight(g.height - 1) - fields.WriteString(g.selector.Selected().View()) + fields.WriteString(g.selector.Selected().View().String()) } else { g.selector.Range(func(i int, field Field) bool { - fields.WriteString(field.View()) + fields.WriteString(field.View().String()) if i == g.selector.Index() { - offset = lipgloss.Height(fields.String()) - lipgloss.Height(field.View()) + offset = lipgloss.Height(fields.String()) - lipgloss.Height(field.View().String()) } if i < g.selector.Total()-1 { fields.WriteString(gap) @@ -326,11 +327,11 @@ func (g *Group) buildView() { } // View renders the group. -func (g *Group) View() string { +func (g *Group) View() fmt.Stringer { var view strings.Builder view.WriteString(g.viewport.View()) view.WriteString(g.Footer()) - return view.String() + return &view } // Content renders the group's content only (no footer). diff --git a/huh_test.go b/huh_test.go index 9857874a..a07329b4 100644 --- a/huh_test.go +++ b/huh_test.go @@ -109,7 +109,7 @@ func TestForm(t *testing.T) { _, cmd := f.Init() f.Update(cmd) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) // // ┃ Shell? @@ -146,7 +146,7 @@ func TestForm(t *testing.T) { m, _ = m.Update(tea.KeyPressMsg(tea.Key{ Code: tea.KeyTab, })) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !strings.Contains(view, "* we're out of hard shells, sorry") { t.Log(pretty.Render(view)) @@ -160,7 +160,7 @@ func TestForm(t *testing.T) { })) m = batchUpdate(m, cmd) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !strings.Contains(view, "┃ > Chicken") { t.Log(pretty.Render(view)) @@ -172,7 +172,7 @@ func TestForm(t *testing.T) { Code: tea.KeyEnter, })) m = batchUpdate(m, cmd) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) // // ┃ Toppings @@ -208,7 +208,7 @@ func TestForm(t *testing.T) { m, _ = m.Update(keypress('j')) m, _ = m.Update(keypress('j')) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !strings.Contains(view, "> • Corn") { t.Log(pretty.Render(view)) @@ -216,7 +216,7 @@ func TestForm(t *testing.T) { } m, _ = m.Update(keypress('x')) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !strings.Contains(view, "> ✓ Corn") { t.Log(pretty.Render(view)) @@ -226,7 +226,7 @@ func TestForm(t *testing.T) { m = batchUpdate(m.Update(tea.KeyPressMsg(tea.Key{ Code: tea.KeyEnter, }))) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !strings.Contains(view, "What's your name?") { t.Log(pretty.Render(view)) @@ -261,7 +261,7 @@ func TestForm(t *testing.T) { for _, msg := range typeText("Glen") { m.Update(msg) } - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !strings.Contains(view, "Glen") { t.Log(pretty.Render(view)) t.Error("Expected form to accept user input") @@ -292,7 +292,7 @@ func TestInput(t *testing.T) { _, cmd := f.Init() f.Update(cmd) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !strings.Contains(view, ">") { t.Log(pretty.Render(view)) @@ -304,7 +304,7 @@ func TestInput(t *testing.T) { m, _ := f.Update(msg) f = m.(*Form) } - view = ansi.Strip(f.View()) + view = ansi.Strip(f.View().String()) if !strings.Contains(view, "Huh") { t.Log(pretty.Render(view)) @@ -332,7 +332,7 @@ func TestInlineInput(t *testing.T) { _, cmd := f.Init() f.Update(cmd) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !strings.Contains(view, "┃ Input Description:") { t.Log(pretty.Render(view)) @@ -344,7 +344,7 @@ func TestInlineInput(t *testing.T) { m, _ := f.Update(msg) f = m.(*Form) } - view = ansi.Strip(f.View()) + view = ansi.Strip(f.View().String()) if !strings.Contains(view, "Huh") { t.Log(pretty.Render(view)) @@ -377,7 +377,7 @@ func TestText(t *testing.T) { m, _ := f.Update(msg) f = m.(*Form) } - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !strings.Contains(view, "Huh") { t.Log(pretty.Render(view)) @@ -403,7 +403,7 @@ func TestConfirm(t *testing.T) { // Type Huh in the form. m, _ := f.Update(keypress('H')) f = m.(*Form) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !strings.Contains(view, "Yes") { t.Log(pretty.Render(view)) @@ -448,7 +448,7 @@ func TestSelect(t *testing.T) { _, cmd := f.Init() f.Update(cmd) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !strings.Contains(view, "Foo") { t.Log(pretty.Render(view)) @@ -469,7 +469,7 @@ func TestSelect(t *testing.T) { m, _ := f.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown})) f = m.(*Form) - view = ansi.Strip(f.View()) + view = ansi.Strip(f.View().String()) if strings.Contains(view, "> Foo") { t.Log(pretty.Render(view)) @@ -501,7 +501,7 @@ func TestMultiSelect(t *testing.T) { _, cmd := f.Init() f.Update(cmd) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !strings.Contains(view, "Foo") { t.Log(pretty.Render(view)) @@ -520,7 +520,7 @@ func TestMultiSelect(t *testing.T) { // Move selection cursor down m, _ := f.Update(keypress('j')) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if strings.Contains(view, "> • Foo") { t.Log(pretty.Render(view)) @@ -534,7 +534,7 @@ func TestMultiSelect(t *testing.T) { // Toggle m, _ = f.Update(keypress('x')) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !strings.Contains(view, "> ✓ Bar") { t.Log(pretty.Render(view)) @@ -586,7 +586,7 @@ func TestMultiSelectFiltering(t *testing.T) { m, _ := f.Update(keypress('B')) f = m.(*Form) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) // When we're filtering, the list should change. if tc.filtering && strings.Contains(view, "Foo") { t.Log(pretty.Render(view)) @@ -604,7 +604,7 @@ func TestMultiSelectFiltering(t *testing.T) { f := NewForm(NewGroup(field)) _, cmd := f.Init() f.Update(cmd) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if strings.Contains(view, "filter") { t.Log(pretty.Render(view)) t.Error("Expected list to hide filtering in help menu.") @@ -648,28 +648,28 @@ func TestSelectPageNavigation(t *testing.T) { _, cmd := f.Init() f.Update(cmd) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !reFirst.MatchString(view) { t.Log(pretty.Render(view)) t.Error("Wrong item selected") } m, _ := f.Update(keypress('G')) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !reLast.MatchString(view) { t.Log(pretty.Render(view)) t.Error("Wrong item selected") } m, _ = f.Update(keypress('g')) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !reFirst.MatchString(view) { t.Log(pretty.Render(view)) t.Error("Wrong item selected") } m, _ = f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'd'})) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !reHalfDown.MatchString(view) { t.Log(pretty.Render(view)) t.Error("Wrong item selected") @@ -679,7 +679,7 @@ func TestSelectPageNavigation(t *testing.T) { f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'u'})) f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'u'})) m, _ = f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'u'})) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !reFirst.MatchString(view) { t.Log(pretty.Render(view)) t.Error("Wrong item selected") @@ -692,7 +692,7 @@ func TestSelectPageNavigation(t *testing.T) { f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'd'})) f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'd'})) m, _ = f.Update(tea.KeyPressMsg(tea.Key{Mod: tea.ModCtrl, Code: 'd'})) - view = ansi.Strip(m.View()) + view = ansi.Strip(m.View().String()) if !reLast.MatchString(view) { t.Log(pretty.Render(view)) t.Error("Wrong item selected") @@ -706,7 +706,7 @@ func TestFile(t *testing.T) { field.Update(cmd) field.Update(cmd()) - view := ansi.Strip(field.View()) + view := ansi.Strip(field.View().String()) if !strings.Contains(view, "No file selected") { t.Log(pretty.Render(view)) @@ -734,30 +734,30 @@ func TestHideGroup(t *testing.T) { f.Update(cmd) f = batchUpdate(f, cmd).(*Form) - if v := f.View(); !strings.Contains(v, "Bar") { - t.Log(pretty.Render(v)) + if v := f.View(); !strings.Contains(v.String(), "Bar") { + t.Log(pretty.Render(v.String())) t.Error("expected Bar to not be hidden") } // should have no effect as previous group is hidden f.Update(prevGroup()) - if v := f.View(); !strings.Contains(v, "Bar") { - t.Log(pretty.Render(v)) + if v := f.View(); !strings.Contains(v.String(), "Bar") { + t.Log(pretty.Render(v.String())) t.Error("expected Bar to not be hidden") } f.Update(nextGroup()) - if v := f.View(); !strings.Contains(v, "Baz") { - t.Log(pretty.Render(v)) + if v := f.View(); !strings.Contains(v.String(), "Baz") { + t.Log(pretty.Render(v.String())) t.Error("expected Baz to not be hidden") } f.Update(nextGroup()) - if v := f.View(); strings.Contains(v, "Qux") { - t.Log(pretty.Render(v)) + if v := f.View(); strings.Contains(v.String(), "Qux") { + t.Log(pretty.Render(v.String())) t.Error("expected Qux to be hidden") } @@ -778,7 +778,7 @@ func TestHideGroupLastAndFirstGroupsNotHidden(t *testing.T) { f.Update(cmd) f = batchUpdate(f, cmd).(*Form) - if v := ansi.Strip(f.View()); !strings.Contains(v, "Bar") { + if v := ansi.Strip(f.View().String()); !strings.Contains(v, "Bar") { t.Log(pretty.Render(v)) t.Error("expected Bar to not be hidden") } @@ -786,14 +786,14 @@ func TestHideGroupLastAndFirstGroupsNotHidden(t *testing.T) { // should have no effect as there isn't any f.Update(prevGroup()) - if v := f.View(); !strings.Contains(v, "Bar") { - t.Log(pretty.Render(v)) + if v := f.View(); !strings.Contains(v.String(), "Bar") { + t.Log(pretty.Render(v.String())) t.Error("expected Bar to not be hidden") } f.Update(nextGroup()) - if v := ansi.Strip(f.View()); !strings.Contains(v, "Baz") { + if v := ansi.Strip(f.View().String()); !strings.Contains(v, "Baz") { t.Log(pretty.Render(v)) t.Error("expected Baz to not be hidden") } @@ -820,7 +820,7 @@ func TestPrevGroup(t *testing.T) { f.Update(prevGroup()) f.Update(prevGroup()) - if v := ansi.Strip(f.View()); !strings.Contains(v, "Bar") { + if v := ansi.Strip(f.View().String()); !strings.Contains(v, "Bar") { t.Log(pretty.Render(v)) t.Error("expected Bar to not be hidden") } @@ -832,7 +832,7 @@ func TestNote(t *testing.T) { _, cmd := f.Init() f.Update(cmd) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !strings.Contains(view, "Taco") { t.Log(view) @@ -866,7 +866,7 @@ func TestDynamicHelp(t *testing.T) { _, cmd := f.Init() f.Update(cmd) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !strings.Contains(view, "Dynamic Help") { t.Log(pretty.Render(view)) @@ -892,7 +892,7 @@ func TestSkip(t *testing.T) { _, cmd := f.Init() f.Update(cmd) f = batchUpdate(f, cmd).(*Form) - view := ansi.Strip(f.View()) + view := ansi.Strip(f.View().String()) if !strings.Contains(view, "┃ First") { t.Log(pretty.Render(view)) @@ -901,7 +901,7 @@ func TestSkip(t *testing.T) { // next field should skip both of the notes and proceed to the last input. f.Update(NextField()) - view = ansi.Strip(f.View()) + view = ansi.Strip(f.View().String()) if strings.Contains(view, "┃ First") { t.Log(pretty.Render(view)) @@ -915,7 +915,7 @@ func TestSkip(t *testing.T) { // previous field should skip both of the notes and focus the first input. f.Update(PrevField()) - view = ansi.Strip(f.View()) + view = ansi.Strip(f.View().String()) if strings.Contains(view, "┃ Second") { t.Log(pretty.Render(view)) diff --git a/layout.go b/layout.go index 3dfbd112..6a3f5e3d 100644 --- a/layout.go +++ b/layout.go @@ -1,6 +1,7 @@ package huh import ( + "fmt" "strings" "github.com/charmbracelet/lipgloss/v2" @@ -8,7 +9,7 @@ import ( // A Layout is responsible for laying out groups in a form. type Layout interface { - View(f *Form) string + View(f *Form) fmt.Stringer GroupWidth(f *Form, g *Group, w int) int } @@ -30,7 +31,7 @@ func LayoutGrid(rows int, columns int) Layout { type layoutDefault struct{} -func (l *layoutDefault) View(f *Form) string { +func (l *layoutDefault) View(f *Form) fmt.Stringer { return f.selector.Selected().View() } @@ -64,10 +65,11 @@ func (l *layoutColumns) visibleGroups(f *Form) []*Group { return groups } -func (l *layoutColumns) View(f *Form) string { +func (l *layoutColumns) View(f *Form) fmt.Stringer { + var s strings.Builder groups := l.visibleGroups(f) if len(groups) == 0 { - return "" + return &s } columns := make([]string, 0, len(groups)) @@ -76,10 +78,11 @@ func (l *layoutColumns) View(f *Form) string { } footer := f.selector.Selected().Footer() - return lipgloss.JoinVertical(lipgloss.Left, + s.WriteString(lipgloss.JoinVertical(lipgloss.Left, lipgloss.JoinHorizontal(lipgloss.Top, columns...), footer, - ) + )) + return &s } func (l *layoutColumns) GroupWidth(_ *Form, _ *Group, w int) int { @@ -88,7 +91,7 @@ func (l *layoutColumns) GroupWidth(_ *Form, _ *Group, w int) int { type layoutStack struct{} -func (l *layoutStack) View(f *Form) string { +func (l *layoutStack) View(f *Form) fmt.Stringer { var columns []string f.selector.Range(func(_ int, group *Group) bool { columns = append(columns, group.Content()) @@ -99,7 +102,7 @@ func (l *layoutStack) View(f *Form) string { var view strings.Builder view.WriteString(strings.Join(columns, "\n")) view.WriteString(footer) - return view.String() + return &view } func (l *layoutStack) GroupWidth(_ *Form, _ *Group, w int) int { @@ -143,10 +146,11 @@ func (l *layoutGrid) visibleGroups(f *Form) [][]*Group { return grid } -func (l *layoutGrid) View(f *Form) string { +func (l *layoutGrid) View(f *Form) fmt.Stringer { + var s strings.Builder grid := l.visibleGroups(f) if len(grid) == 0 { - return "" + return &s } rows := make([]string, 0, len(grid)) @@ -159,7 +163,8 @@ func (l *layoutGrid) View(f *Form) string { } footer := f.selector.Selected().Footer() - return lipgloss.JoinVertical(lipgloss.Left, strings.Join(rows, "\n"), footer) + s.WriteString(lipgloss.JoinVertical(lipgloss.Left, strings.Join(rows, "\n"), footer)) + return &s } func (l *layoutGrid) GroupWidth(_ *Form, _ *Group, w int) int {