From ea8002572aa46192f766492f58cef273168f8b09 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Wed, 20 Nov 2024 12:23:54 -0800 Subject: [PATCH 01/28] feat: set group with from size msg --- group.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/group.go b/group.go index d890751b..2d8aa11a 100644 --- a/group.go +++ b/group.go @@ -269,6 +269,7 @@ func (g *Group) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: g.WithHeight(max(g.height, min(g.fullHeight(), msg.Height-1))) + g.WithWidth(max(g.width, min(g.fullWidth(), msg.Width-1))) case nextFieldMsg: cmds = append(cmds, g.nextField()...) case prevFieldMsg: @@ -290,6 +291,16 @@ func (g *Group) fullHeight() int { return height } +// width returns the full width of the group. +func (g *Group) fullWidth() int { + width := g.selector.Total() + g.selector.Range(func(_ int, field Field) bool { + width += lipgloss.Width(field.View()) + return true + }) + return width +} + func (g *Group) getContent() (int, string) { var fields strings.Builder offset := 0 From a748847dab43acbaf13d36fb0416cf5566552b75 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Wed, 20 Nov 2024 12:26:28 -0800 Subject: [PATCH 02/28] feat: handle WindowSizeMsg + base height on rendered content --- field_multiselect.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/field_multiselect.go b/field_multiselect.go index faa33ee5..f9249e33 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -267,6 +267,10 @@ func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } switch msg := msg.(type) { + case tea.WindowSizeMsg: + rendered := m.View() + m.WithHeight(max(m.height, min(lipgloss.Height(rendered), msg.Height-1))) + m.WithWidth(max(m.width, min(lipgloss.Width(rendered), msg.Width-1))) case updateFieldMsg: var fieldCmds []tea.Cmd if ok, hash := m.title.shouldUpdate(); ok { @@ -456,7 +460,7 @@ func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *MultiSelect[T]) updateViewportHeight() { // If no height is set size the viewport to the number of options. if m.height <= 0 { - m.viewport.Height = len(m.options.val) + m.viewport.Height = lipgloss.Height(m.optionsView()) return } From 152c08719eb8657131bb987a29af121dd21e7bb3 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Wed, 20 Nov 2024 12:27:39 -0800 Subject: [PATCH 03/28] feat: render prefix before newlines --- field_multiselect.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/field_multiselect.go b/field_multiselect.go index f9249e33..d19feebf 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -559,13 +559,18 @@ func (m *MultiSelect[T]) optionsView() string { } else { sb.WriteString(strings.Repeat(" ", lipgloss.Width(c))) } + prefixWidth := lipgloss.Width(styles.SelectedPrefix.String()) + contentWidth := m.width - prefixWidth if m.filteredOptions[i].selected { - sb.WriteString(styles.SelectedPrefix.String()) - sb.WriteString(styles.SelectedOption.Render(option.Key)) + // add a prefix after each newline except the last one. + content := styles.SelectedOption.Width(contentWidth).Render(option.Key) + strings.Replace(content, "\n", "\n"+styles.SelectedPrefix.String(), strings.Count(content, "\n")-1) + sb.WriteString(content) } else { - sb.WriteString(styles.UnselectedPrefix.String()) - sb.WriteString(styles.UnselectedOption.Render(option.Key)) + content := styles.UnselectedOption.Width(contentWidth).Render(option.Key) + strings.Replace(content, "\n", "\n"+styles.UnselectedPrefix.String(), strings.Count(content, "\n")-1) + sb.WriteString(content) } if i < len(m.options.val)-1 { sb.WriteString("\n") From ae0674bdbcc8fdf084b0b2db98d55e0a711e4c86 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Wed, 20 Nov 2024 13:51:01 -0800 Subject: [PATCH 04/28] feat: update viewport on resize, cursor != height so no scroll... --- field_multiselect.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/field_multiselect.go b/field_multiselect.go index d19feebf..fc0021ef 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -268,9 +268,7 @@ func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: - rendered := m.View() - m.WithHeight(max(m.height, min(lipgloss.Height(rendered), msg.Height-1))) - m.WithWidth(max(m.width, min(lipgloss.Width(rendered), msg.Width-1))) + m.updateViewportHeight() case updateFieldMsg: var fieldCmds []tea.Cmd if ok, hash := m.title.shouldUpdate(); ok { @@ -560,7 +558,7 @@ func (m *MultiSelect[T]) optionsView() string { sb.WriteString(strings.Repeat(" ", lipgloss.Width(c))) } prefixWidth := lipgloss.Width(styles.SelectedPrefix.String()) - contentWidth := m.width - prefixWidth + contentWidth := m.width - prefixWidth - lipgloss.Width(c) if m.filteredOptions[i].selected { // add a prefix after each newline except the last one. From c586d4036274fb539589e84c54d0533d7034f977 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 21 Nov 2024 07:45:44 -0800 Subject: [PATCH 05/28] fix: always update viewport height --- field_multiselect.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/field_multiselect.go b/field_multiselect.go index fc0021ef..10992136 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -267,8 +267,6 @@ func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.updateViewportHeight() case updateFieldMsg: var fieldCmds []tea.Cmd if ok, hash := m.title.shouldUpdate(); ok { From 18a0ca9dd5a102cdca64a1048bd4437d9c67764e Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 21 Nov 2024 08:21:54 -0800 Subject: [PATCH 06/28] fix: add option prefix --- field_multiselect.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/field_multiselect.go b/field_multiselect.go index 10992136..14458368 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -555,15 +555,17 @@ func (m *MultiSelect[T]) optionsView() string { } else { sb.WriteString(strings.Repeat(" ", lipgloss.Width(c))) } - prefixWidth := lipgloss.Width(styles.SelectedPrefix.String()) + prefixWidth := max(lipgloss.Width(styles.SelectedPrefix.String()), lipgloss.Width(styles.UnselectedPrefix.String())) contentWidth := m.width - prefixWidth - lipgloss.Width(c) if m.filteredOptions[i].selected { // add a prefix after each newline except the last one. + sb.WriteString(styles.SelectedPrefix.String()) content := styles.SelectedOption.Width(contentWidth).Render(option.Key) strings.Replace(content, "\n", "\n"+styles.SelectedPrefix.String(), strings.Count(content, "\n")-1) sb.WriteString(content) } else { + sb.WriteString(styles.UnselectedPrefix.String()) content := styles.UnselectedOption.Width(contentWidth).Render(option.Key) strings.Replace(content, "\n", "\n"+styles.UnselectedPrefix.String(), strings.Count(content, "\n")-1) sb.WriteString(content) From d206fc6f07f2aefc528fccc4e8a86110600a6c7b Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 21 Nov 2024 08:47:02 -0800 Subject: [PATCH 07/28] fix(multiselect): width calculation --- field_multiselect.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/field_multiselect.go b/field_multiselect.go index 14458368..aadc87e1 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -555,20 +555,17 @@ func (m *MultiSelect[T]) optionsView() string { } else { sb.WriteString(strings.Repeat(" ", lipgloss.Width(c))) } + // Calculate width constraints. prefixWidth := max(lipgloss.Width(styles.SelectedPrefix.String()), lipgloss.Width(styles.UnselectedPrefix.String())) - contentWidth := m.width - prefixWidth - lipgloss.Width(c) + frameSize := styles.Base.GetHorizontalFrameSize() + contentWidth := m.width - frameSize - lipgloss.Width(c) - prefixWidth if m.filteredOptions[i].selected { - // add a prefix after each newline except the last one. sb.WriteString(styles.SelectedPrefix.String()) - content := styles.SelectedOption.Width(contentWidth).Render(option.Key) - strings.Replace(content, "\n", "\n"+styles.SelectedPrefix.String(), strings.Count(content, "\n")-1) - sb.WriteString(content) + sb.WriteString(styles.SelectedOption.Width(contentWidth).Render(option.Key)) } else { sb.WriteString(styles.UnselectedPrefix.String()) - content := styles.UnselectedOption.Width(contentWidth).Render(option.Key) - strings.Replace(content, "\n", "\n"+styles.UnselectedPrefix.String(), strings.Count(content, "\n")-1) - sb.WriteString(content) + sb.WriteString(styles.UnselectedOption.Width(contentWidth).Render(option.Key)) } if i < len(m.options.val)-1 { sb.WriteString("\n") From b41c5e7b7109638d27b6324aff64db27a13188cd Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 21 Nov 2024 20:09:52 -0800 Subject: [PATCH 08/28] fix: calculate offset from previous field heights --- group.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/group.go b/group.go index 2d8aa11a..6b00dd86 100644 --- a/group.go +++ b/group.go @@ -303,18 +303,19 @@ func (g *Group) fullWidth() int { func (g *Group) getContent() (int, string) { var fields strings.Builder - offset := 0 + var offset int gap := "\n\n" - // if the focused field is requesting it be zoomed, only show that field. + // 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()) } else { g.selector.Range(func(i int, field Field) bool { fields.WriteString(field.View()) - if i == g.selector.Index() { - offset = lipgloss.Height(fields.String()) - lipgloss.Height(field.View()) + // Set the offset to the height of the previous field(s). + if i < g.selector.Index() { + offset += lipgloss.Height(field.View()) } if i < g.selector.Total()-1 { fields.WriteString(gap) From 98f01b6120d0f5e18114cf417381ea4f39d3b774 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 21 Nov 2024 20:11:57 -0800 Subject: [PATCH 09/28] feat: add calculate wrapping helper --- field_multiselect.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/field_multiselect.go b/field_multiselect.go index aadc87e1..6992599a 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -511,6 +511,10 @@ func (m *MultiSelect[T]) activeStyles() *FieldStyles { return &theme.Blurred } +func (m *MultiSelect[T]) calculateWrapping() int { + return m.width - m.activeStyles().Base.GetHorizontalFrameSize() +} + func (m *MultiSelect[T]) titleView() string { if m.title.val == "" { return "" @@ -522,9 +526,9 @@ func (m *MultiSelect[T]) titleView() string { if m.filtering { sb.WriteString(m.filter.View()) } else if m.filter.Value() != "" { - sb.WriteString(styles.Title.Render(m.title.val) + styles.Description.Render("/"+m.filter.Value())) + sb.WriteString(styles.Title.Width(m.calculateWrapping()).Render(m.title.val) + styles.Description.Render("/"+m.filter.Value())) } else { - sb.WriteString(styles.Title.Render(m.title.val)) + sb.WriteString(styles.Title.Width(m.calculateWrapping()).Render(m.title.val)) } if m.err != nil { sb.WriteString(styles.ErrorIndicator.String()) @@ -533,7 +537,7 @@ func (m *MultiSelect[T]) titleView() string { } func (m *MultiSelect[T]) descriptionView() string { - return m.activeStyles().Description.Render(m.description.val) + return m.activeStyles().Description.Width(m.calculateWrapping()).Render(m.description.val) } func (m *MultiSelect[T]) optionsView() string { @@ -556,9 +560,7 @@ func (m *MultiSelect[T]) optionsView() string { sb.WriteString(strings.Repeat(" ", lipgloss.Width(c))) } // Calculate width constraints. - prefixWidth := max(lipgloss.Width(styles.SelectedPrefix.String()), lipgloss.Width(styles.UnselectedPrefix.String())) - frameSize := styles.Base.GetHorizontalFrameSize() - contentWidth := m.width - frameSize - lipgloss.Width(c) - prefixWidth + contentWidth := m.calculateWrapping() - lipgloss.Width(c) - max(lipgloss.Width(styles.SelectedPrefix.String()), lipgloss.Width(styles.UnselectedPrefix.String())) if m.filteredOptions[i].selected { sb.WriteString(styles.SelectedPrefix.String()) From ea01c18bc56fbd64f1515e77e457674d2eada5fd Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 21 Nov 2024 20:14:00 -0800 Subject: [PATCH 10/28] fix(select): wrap select field --- field_select.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/field_select.go b/field_select.go index 81e57d9e..edc5b7f1 100644 --- a/field_select.go +++ b/field_select.go @@ -504,7 +504,7 @@ func (s *Select[T]) updateValue() { func (s *Select[T]) updateViewportHeight() { // If no height is set size the viewport to the number of options. if s.height <= 0 { - s.viewport.Height = len(s.options.val) + s.viewport.Height = lipgloss.Height(s.optionsView()) return } @@ -524,6 +524,10 @@ func (s *Select[T]) activeStyles() *FieldStyles { return &theme.Blurred } +func (s *Select[T]) calculateWrapping() int { + return s.width - s.activeStyles().Base.GetHorizontalFrameSize() +} + func (s *Select[T]) titleView() string { var ( styles = s.activeStyles() @@ -532,9 +536,9 @@ func (s *Select[T]) titleView() string { if s.filtering { sb.WriteString(s.filter.View()) } else if s.filter.Value() != "" && !s.inline { - sb.WriteString(styles.Title.Render(s.title.val) + styles.Description.Render("/"+s.filter.Value())) + sb.WriteString(styles.Title.Width(s.calculateWrapping()).Render(s.title.val) + styles.Description.Render("/"+s.filter.Value())) } else { - sb.WriteString(styles.Title.Render(s.title.val)) + sb.WriteString(styles.Title.Width(s.calculateWrapping()).Render(s.title.val)) } if s.err != nil { sb.WriteString(styles.ErrorIndicator.String()) @@ -543,7 +547,7 @@ func (s *Select[T]) titleView() string { } func (s *Select[T]) descriptionView() string { - return s.activeStyles().Description.Render(s.description.val) + return s.activeStyles().Description.Width(s.calculateWrapping()).Render(s.description.val) } func (s *Select[T]) optionsView() string { @@ -552,6 +556,7 @@ func (s *Select[T]) optionsView() string { c = styles.SelectSelector.String() sb strings.Builder ) + width := s.calculateWrapping() - lipgloss.Width(c) - max(lipgloss.Width(styles.SelectedOption.String()), lipgloss.Width(styles.UnselectedOption.String())) if s.options.loading && time.Since(s.options.loadingStart) > spinnerShowThreshold { s.spinner.Style = s.activeStyles().MultiSelectSelector.UnsetString() @@ -562,9 +567,9 @@ func (s *Select[T]) optionsView() string { if s.inline { sb.WriteString(styles.PrevIndicator.Faint(s.selected <= 0).String()) if len(s.filteredOptions) > 0 { - sb.WriteString(styles.SelectedOption.Render(s.filteredOptions[s.selected].Key)) + sb.WriteString(styles.SelectedOption.Width(width).Render(s.filteredOptions[s.selected].Key)) } else { - sb.WriteString(styles.TextInput.Placeholder.Render("No matches")) + sb.WriteString(styles.TextInput.Placeholder.Width(width).Render("No matches")) } sb.WriteString(styles.NextIndicator.Faint(s.selected == len(s.filteredOptions)-1).String()) return sb.String() @@ -572,9 +577,9 @@ func (s *Select[T]) optionsView() string { for i, option := range s.filteredOptions { if s.selected == i { - sb.WriteString(c + styles.SelectedOption.Render(option.Key)) + sb.WriteString(c + styles.SelectedOption.Width(width).Render(option.Key)) } else { - sb.WriteString(strings.Repeat(" ", lipgloss.Width(c)) + styles.UnselectedOption.Render(option.Key)) + sb.WriteString(strings.Repeat(" ", lipgloss.Width(c)) + styles.UnselectedOption.Width(width).Render(option.Key)) } if i < len(s.options.val)-1 { sb.WriteString("\n") From 262ac14e88b336cd30aa879aef5a05e0413d60d3 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Mon, 25 Nov 2024 09:20:53 -0800 Subject: [PATCH 11/28] feat: make group set field widths accounting for styles --- field_multiselect.go | 12 ++++-------- field_select.go | 12 ++++-------- group.go | 4 +++- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/field_multiselect.go b/field_multiselect.go index 6992599a..e221d414 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -511,10 +511,6 @@ func (m *MultiSelect[T]) activeStyles() *FieldStyles { return &theme.Blurred } -func (m *MultiSelect[T]) calculateWrapping() int { - return m.width - m.activeStyles().Base.GetHorizontalFrameSize() -} - func (m *MultiSelect[T]) titleView() string { if m.title.val == "" { return "" @@ -526,9 +522,9 @@ func (m *MultiSelect[T]) titleView() string { if m.filtering { sb.WriteString(m.filter.View()) } else if m.filter.Value() != "" { - sb.WriteString(styles.Title.Width(m.calculateWrapping()).Render(m.title.val) + styles.Description.Render("/"+m.filter.Value())) + sb.WriteString(styles.Title.Width(m.width).Render(m.title.val) + styles.Description.Render("/"+m.filter.Value())) } else { - sb.WriteString(styles.Title.Width(m.calculateWrapping()).Render(m.title.val)) + sb.WriteString(styles.Title.Width(m.width).Render(m.title.val)) } if m.err != nil { sb.WriteString(styles.ErrorIndicator.String()) @@ -537,7 +533,7 @@ func (m *MultiSelect[T]) titleView() string { } func (m *MultiSelect[T]) descriptionView() string { - return m.activeStyles().Description.Width(m.calculateWrapping()).Render(m.description.val) + return m.activeStyles().Description.Width(m.width).Render(m.description.val) } func (m *MultiSelect[T]) optionsView() string { @@ -560,7 +556,7 @@ func (m *MultiSelect[T]) optionsView() string { sb.WriteString(strings.Repeat(" ", lipgloss.Width(c))) } // Calculate width constraints. - contentWidth := m.calculateWrapping() - lipgloss.Width(c) - max(lipgloss.Width(styles.SelectedPrefix.String()), lipgloss.Width(styles.UnselectedPrefix.String())) + contentWidth := m.width - lipgloss.Width(c) - max(lipgloss.Width(styles.SelectedPrefix.String()), lipgloss.Width(styles.UnselectedPrefix.String())) if m.filteredOptions[i].selected { sb.WriteString(styles.SelectedPrefix.String()) diff --git a/field_select.go b/field_select.go index edc5b7f1..b4337d26 100644 --- a/field_select.go +++ b/field_select.go @@ -524,10 +524,6 @@ func (s *Select[T]) activeStyles() *FieldStyles { return &theme.Blurred } -func (s *Select[T]) calculateWrapping() int { - return s.width - s.activeStyles().Base.GetHorizontalFrameSize() -} - func (s *Select[T]) titleView() string { var ( styles = s.activeStyles() @@ -536,9 +532,9 @@ func (s *Select[T]) titleView() string { if s.filtering { sb.WriteString(s.filter.View()) } else if s.filter.Value() != "" && !s.inline { - sb.WriteString(styles.Title.Width(s.calculateWrapping()).Render(s.title.val) + styles.Description.Render("/"+s.filter.Value())) + sb.WriteString(styles.Title.Width(s.width).Render(s.title.val) + styles.Description.Render("/"+s.filter.Value())) } else { - sb.WriteString(styles.Title.Width(s.calculateWrapping()).Render(s.title.val)) + sb.WriteString(styles.Title.Width(s.width).Render(s.title.val)) } if s.err != nil { sb.WriteString(styles.ErrorIndicator.String()) @@ -547,7 +543,7 @@ func (s *Select[T]) titleView() string { } func (s *Select[T]) descriptionView() string { - return s.activeStyles().Description.Width(s.calculateWrapping()).Render(s.description.val) + return s.activeStyles().Description.Width(s.width).Render(s.description.val) } func (s *Select[T]) optionsView() string { @@ -556,7 +552,7 @@ func (s *Select[T]) optionsView() string { c = styles.SelectSelector.String() sb strings.Builder ) - width := s.calculateWrapping() - lipgloss.Width(c) - max(lipgloss.Width(styles.SelectedOption.String()), lipgloss.Width(styles.UnselectedOption.String())) + width := s.width - lipgloss.Width(c) - max(lipgloss.Width(styles.SelectedOption.String()), lipgloss.Width(styles.UnselectedOption.String())) if s.options.loading && time.Since(s.options.loadingStart) > spinnerShowThreshold { s.spinner.Style = s.activeStyles().MultiSelectSelector.UnsetString() diff --git a/group.go b/group.go index 6b00dd86..08245c38 100644 --- a/group.go +++ b/group.go @@ -113,7 +113,9 @@ func (g *Group) WithWidth(width int) *Group { g.width = width g.viewport.Width = width g.selector.Range(func(_ int, field Field) bool { - field.WithWidth(width) + // TODO can I do this? All themes should have the same horizontal frame size anyway... + // If not, should the active theme be loaded per form rather than per field? + field.WithWidth(width - ThemeCharm().Focused.Base.GetHorizontalFrameSize()) return true }) return g From 4ff8255f596540e9b6673971b36279a6ec06ed04 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 26 Nov 2024 08:51:30 -0800 Subject: [PATCH 12/28] feat: set width --- field_confirm.go | 4 ++-- field_input.go | 4 ++-- field_text.go | 4 ++-- group.go | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/field_confirm.go b/field_confirm.go index c2a88ed5..df8f916a 100644 --- a/field_confirm.go +++ b/field_confirm.go @@ -235,12 +235,12 @@ func (c *Confirm) View() string { styles := c.activeStyles() var sb strings.Builder - sb.WriteString(styles.Title.Render(c.title.val)) + sb.WriteString(styles.Title.Width(c.width).Render(c.title.val)) if c.err != nil { sb.WriteString(styles.ErrorIndicator.String()) } - description := styles.Description.Render(c.description.val) + description := styles.Description.Width(c.width).Render(c.description.val) if !c.inline && (c.description.val != "" || c.description.fn != nil) { sb.WriteString("\n") diff --git a/field_input.go b/field_input.go index 3510d527..fcdcb89f 100644 --- a/field_input.go +++ b/field_input.go @@ -396,13 +396,13 @@ func (i *Input) View() string { var sb strings.Builder if i.title.val != "" || i.title.fn != nil { - sb.WriteString(styles.Title.Render(i.title.val)) + sb.WriteString(styles.Title.Width(i.width).Render(i.title.val)) if !i.inline { sb.WriteString("\n") } } if i.description.val != "" || i.description.fn != nil { - sb.WriteString(styles.Description.Render(i.description.val)) + sb.WriteString(styles.Description.Width(i.width).Render(i.description.val)) if !i.inline { sb.WriteString("\n") } diff --git a/field_text.go b/field_text.go index a5c3a40c..f2b51d0c 100644 --- a/field_text.go +++ b/field_text.go @@ -379,14 +379,14 @@ func (t *Text) View() string { var sb strings.Builder if t.title.val != "" || t.title.fn != nil { - sb.WriteString(styles.Title.Render(t.title.val)) + sb.WriteString(styles.Title.Width(t.width).Render(t.title.val)) if t.err != nil { sb.WriteString(styles.ErrorIndicator.String()) } sb.WriteString("\n") } if t.description.val != "" || t.description.fn != nil { - sb.WriteString(styles.Description.Render(t.description.val)) + sb.WriteString(styles.Description.Width(t.width).Render(t.description.val)) sb.WriteString("\n") } sb.WriteString(t.textarea.View()) diff --git a/group.go b/group.go index 08245c38..64f3dde9 100644 --- a/group.go +++ b/group.go @@ -115,6 +115,7 @@ func (g *Group) WithWidth(width int) *Group { g.selector.Range(func(_ int, field Field) bool { // TODO can I do this? All themes should have the same horizontal frame size anyway... // If not, should the active theme be loaded per form rather than per field? + // TODO field.Theme returns *Theme field.WithWidth(width - ThemeCharm().Focused.Base.GetHorizontalFrameSize()) return true }) From c5e8fb847b56c1340b5fde1645cd22a38ba2937a Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 26 Nov 2024 11:48:20 -0800 Subject: [PATCH 13/28] feat: stack buttons in narrow window --- field_confirm.go | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/field_confirm.go b/field_confirm.go index df8f916a..40ab304f 100644 --- a/field_confirm.go +++ b/field_confirm.go @@ -254,13 +254,24 @@ func (c *Confirm) View() string { var negative string var affirmative string + + // Calculate padding to make buttons equal. The narrowest button has padding + // added to its right. + negativePad := styles.BlurredButton.GetHorizontalPadding() / 2 + affirmativePad := styles.BlurredButton.GetHorizontalPadding() / 2 + if lipgloss.Width(c.negative) < lipgloss.Width(c.affirmative) { + negativePad += (lipgloss.Width(c.affirmative) - lipgloss.Width(c.negative)) + } else if lipgloss.Width(c.affirmative) < lipgloss.Width(c.negative) { + affirmativePad += (lipgloss.Width(c.negative) - lipgloss.Width(c.affirmative)) + } + if c.negative != "" { if c.accessor.Get() { - affirmative = styles.FocusedButton.Render(c.affirmative) - negative = styles.BlurredButton.Render(c.negative) + affirmative = styles.FocusedButton.PaddingRight(affirmativePad).Render(c.affirmative) + negative = styles.BlurredButton.PaddingRight(negativePad).Render(c.negative) } else { - affirmative = styles.BlurredButton.Render(c.affirmative) - negative = styles.FocusedButton.Render(c.negative) + affirmative = styles.BlurredButton.PaddingRight(affirmativePad).Render(c.affirmative) + negative = styles.FocusedButton.PaddingRight(negativePad).Render(c.negative) } c.keymap.Reject.SetHelp("n", c.negative) } else { @@ -269,20 +280,13 @@ func (c *Confirm) View() string { } c.keymap.Accept.SetHelp("y", c.affirmative) - buttonsRow := lipgloss.JoinHorizontal(c.buttonAlignment, affirmative, negative) - - promptWidth := lipgloss.Width(sb.String()) - buttonsWidth := lipgloss.Width(buttonsRow) - - renderWidth := promptWidth - if buttonsWidth > renderWidth { - renderWidth = buttonsWidth + // Align the buttons vertically if the window is too narrow. + if c.width < lipgloss.Width(buttonsRow) { + buttonsRow = lipgloss.JoinVertical(lipgloss.Left, affirmative, negative) } - style := lipgloss.NewStyle().Width(renderWidth).Align(c.buttonAlignment) - - sb.WriteString(style.Render(buttonsRow)) + sb.WriteString(buttonsRow) return styles.Base.Render(sb.String()) } From b1e646934d5db895882b99a44f3edbc6e0d87490 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 26 Nov 2024 12:02:50 -0800 Subject: [PATCH 14/28] feat: add Theme function to Fields --- field_confirm.go | 14 ++++++++++---- field_filepicker.go | 14 ++++++++++---- field_input.go | 14 ++++++++++---- field_multiselect.go | 14 ++++++++++---- field_note.go | 14 ++++++++++---- field_select.go | 14 ++++++++++---- field_text.go | 14 ++++++++++---- 7 files changed, 70 insertions(+), 28 deletions(-) diff --git a/field_confirm.go b/field_confirm.go index 40ab304f..0edf3386 100644 --- a/field_confirm.go +++ b/field_confirm.go @@ -220,16 +220,22 @@ func (c *Confirm) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (c *Confirm) activeStyles() *FieldStyles { - theme := c.theme - if theme == nil { - theme = ThemeCharm() - } + theme := c.Theme() if c.focused { return &theme.Focused } return &theme.Blurred } +// Theme returns the theme of the field. +func (c *Confirm) Theme() *Theme { + theme := c.theme + if theme == nil { + theme = ThemeCharm() + } + return theme +} + // View renders the confirm field. func (c *Confirm) View() string { styles := c.activeStyles() diff --git a/field_filepicker.go b/field_filepicker.go index 36a3385f..9533b62f 100644 --- a/field_filepicker.go +++ b/field_filepicker.go @@ -247,16 +247,22 @@ func (f *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (f *FilePicker) activeStyles() *FieldStyles { - theme := f.theme - if theme == nil { - theme = ThemeCharm() - } + theme := f.Theme() if f.focused { return &theme.Focused } return &theme.Blurred } +// Theme returns the theme of the field. +func (f *FilePicker) Theme() *Theme { + theme := f.theme + if theme == nil { + theme = ThemeCharm() + } + return theme +} + // View renders the file field. func (f *FilePicker) View() string { styles := f.activeStyles() diff --git a/field_input.go b/field_input.go index fcdcb89f..1b254fb1 100644 --- a/field_input.go +++ b/field_input.go @@ -366,16 +366,22 @@ func (i *Input) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (i *Input) activeStyles() *FieldStyles { - theme := i.theme - if theme == nil { - theme = ThemeCharm() - } + theme := i.Theme() if i.focused { return &theme.Focused } return &theme.Blurred } +// Theme returns the theme of the field. +func (i *Input) Theme() *Theme { + theme := i.theme + if theme == nil { + theme = ThemeCharm() + } + return theme +} + // View renders the input field. func (i *Input) View() string { styles := i.activeStyles() diff --git a/field_multiselect.go b/field_multiselect.go index e221d414..ebcdb616 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -501,16 +501,22 @@ func (m *MultiSelect[T]) updateValue() { } func (m *MultiSelect[T]) activeStyles() *FieldStyles { - theme := m.theme - if theme == nil { - theme = ThemeCharm() - } + theme := m.Theme() if m.focused { return &theme.Focused } return &theme.Blurred } +// Theme returns the theme of the field. +func (m *MultiSelect[T]) Theme() *Theme { + theme := m.theme + if theme == nil { + theme = ThemeCharm() + } + return theme +} + func (m *MultiSelect[T]) titleView() string { if m.title.val == "" { return "" diff --git a/field_note.go b/field_note.go index c93125f0..afbfcfdb 100644 --- a/field_note.go +++ b/field_note.go @@ -204,16 +204,22 @@ func (n *Note) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (n *Note) activeStyles() *FieldStyles { - theme := n.theme - if theme == nil { - theme = ThemeCharm() - } + theme := n.Theme() if n.focused { return &theme.Focused } return &theme.Blurred } +// Theme returns the theme of the field. +func (n *Note) Theme() *Theme { + theme := n.theme + if theme == nil { + theme = ThemeCharm() + } + return theme +} + // View renders the note field. func (n *Note) View() string { styles := n.activeStyles() diff --git a/field_select.go b/field_select.go index b4337d26..8d565467 100644 --- a/field_select.go +++ b/field_select.go @@ -514,16 +514,22 @@ func (s *Select[T]) updateViewportHeight() { } func (s *Select[T]) activeStyles() *FieldStyles { - theme := s.theme - if theme == nil { - theme = ThemeCharm() - } + theme := s.Theme() if s.focused { return &theme.Focused } return &theme.Blurred } +// Theme returns the theme of the field. +func (s *Select[T]) Theme() *Theme { + theme := s.theme + if theme == nil { + theme = ThemeCharm() + } + return theme +} + func (s *Select[T]) titleView() string { var ( styles = s.activeStyles() diff --git a/field_text.go b/field_text.go index f2b51d0c..a7667373 100644 --- a/field_text.go +++ b/field_text.go @@ -342,16 +342,22 @@ func (t *Text) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (t *Text) activeStyles() *FieldStyles { - theme := t.theme - if theme == nil { - theme = ThemeCharm() - } + theme := t.Theme() if t.focused { return &theme.Focused } return &theme.Blurred } +// Theme returns the theme of the field. +func (t *Text) Theme() *Theme { + theme := t.theme + if theme == nil { + theme = ThemeCharm() + } + return theme +} + func (t *Text) activeTextAreaStyles() *textarea.Style { if t.theme == nil { return &t.textarea.BlurredStyle From ec82a9b1e3bc1eaec946cf24d14b2b4e87b31693 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 26 Nov 2024 12:17:11 -0800 Subject: [PATCH 15/28] feat: calculate width based on field theme --- form.go | 3 +++ group.go | 5 +---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/form.go b/form.go index e1afb0d2..872d9ea2 100644 --- a/form.go +++ b/form.go @@ -154,6 +154,9 @@ type Field interface { // KeyBinds returns help keybindings. KeyBinds() []key.Binding + // Theme returns the theme on a field. + Theme() *Theme + // WithTheme sets the theme on a field. WithTheme(*Theme) Field diff --git a/group.go b/group.go index 64f3dde9..2ff44f4f 100644 --- a/group.go +++ b/group.go @@ -113,10 +113,7 @@ func (g *Group) WithWidth(width int) *Group { g.width = width g.viewport.Width = width g.selector.Range(func(_ int, field Field) bool { - // TODO can I do this? All themes should have the same horizontal frame size anyway... - // If not, should the active theme be loaded per form rather than per field? - // TODO field.Theme returns *Theme - field.WithWidth(width - ThemeCharm().Focused.Base.GetHorizontalFrameSize()) + field.WithWidth(width - field.Theme().Focused.Base.GetHorizontalFrameSize()) return true }) return g From 75664f1d1d9f72d0ffbcfa57b5ebdc45b3dde2a4 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 26 Nov 2024 12:28:09 -0800 Subject: [PATCH 16/28] fix(group): fix height calculation --- group.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/group.go b/group.go index 2ff44f4f..d975e46d 100644 --- a/group.go +++ b/group.go @@ -268,8 +268,8 @@ func (g *Group) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: - g.WithHeight(max(g.height, min(g.fullHeight(), msg.Height-1))) - g.WithWidth(max(g.width, min(g.fullWidth(), msg.Width-1))) + g.WithHeight(min(g.fullHeight(), msg.Height-1)) + g.WithWidth(min(g.fullWidth(), msg.Width-1)) case nextFieldMsg: cmds = append(cmds, g.nextField()...) case prevFieldMsg: From 70db481f12e91e449a5a380b4ab553df96e509be Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 26 Nov 2024 13:15:32 -0800 Subject: [PATCH 17/28] feat: remove FullWidth helper (not needed) --- group.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/group.go b/group.go index d975e46d..1d31d91f 100644 --- a/group.go +++ b/group.go @@ -269,7 +269,7 @@ func (g *Group) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: g.WithHeight(min(g.fullHeight(), msg.Height-1)) - g.WithWidth(min(g.fullWidth(), msg.Width-1)) + g.WithWidth(msg.Width) case nextFieldMsg: cmds = append(cmds, g.nextField()...) case prevFieldMsg: @@ -291,16 +291,6 @@ func (g *Group) fullHeight() int { return height } -// width returns the full width of the group. -func (g *Group) fullWidth() int { - width := g.selector.Total() - g.selector.Range(func(_ int, field Field) bool { - width += lipgloss.Width(field.View()) - return true - }) - return width -} - func (g *Group) getContent() (int, string) { var fields strings.Builder var offset int From c9016a4b4124316786a5cdb984d73eaa3bf173ed Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 26 Nov 2024 13:16:31 -0800 Subject: [PATCH 18/28] fix(group): fix off by one at top for overflowed content --- form.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/form.go b/form.go index 872d9ea2..79c73935 100644 --- a/form.go +++ b/form.go @@ -524,7 +524,7 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } f.selector.Range(func(_ int, group *Group) bool { if group.fullHeight() > msg.Height { - group.WithHeight(msg.Height) + group.WithHeight(msg.Height - 1) } return true }) From 5164a77a2dd4386bff9a0fe4077489c1bd26914f Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 26 Nov 2024 13:34:17 -0800 Subject: [PATCH 19/28] fix(group): add gap height to calculations --- group.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/group.go b/group.go index 1d31d91f..92bf2c04 100644 --- a/group.go +++ b/group.go @@ -10,6 +10,8 @@ import ( "github.com/charmbracelet/lipgloss" ) +const gap string = "\n\n" + // Group is a collection of fields that are displayed together with a page of // the form. While a group is displayed the form completer can switch between // fields in the group. @@ -286,6 +288,7 @@ 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(gap) return true }) return height @@ -294,7 +297,6 @@ func (g *Group) fullHeight() int { func (g *Group) getContent() (int, string) { var fields strings.Builder var offset int - gap := "\n\n" // If the focused field is requesting it be zoomed, only show that field. if g.selector.Selected().Zoom() { From 682d2c772f5451dfedbad26db0d71a67560d94fe Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 26 Nov 2024 13:35:07 -0800 Subject: [PATCH 20/28] fix: don't wrap inline content --- field_confirm.go | 14 +++++++++----- field_input.go | 8 ++++++-- field_select.go | 11 ++++++++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/field_confirm.go b/field_confirm.go index 0edf3386..9dec441a 100644 --- a/field_confirm.go +++ b/field_confirm.go @@ -241,21 +241,25 @@ func (c *Confirm) View() string { styles := c.activeStyles() var sb strings.Builder - sb.WriteString(styles.Title.Width(c.width).Render(c.title.val)) + if c.inline { + sb.WriteString(styles.Title.Render(c.title.val)) + } else { + sb.WriteString(styles.Title.Width(c.width).Render(c.title.val)) + } + if c.err != nil { sb.WriteString(styles.ErrorIndicator.String()) } - description := styles.Description.Width(c.width).Render(c.description.val) - if !c.inline && (c.description.val != "" || c.description.fn != nil) { sb.WriteString("\n") } - sb.WriteString(description) - if !c.inline { + sb.WriteString(styles.Description.Width(c.width).Render(c.description.val)) sb.WriteString("\n") sb.WriteString("\n") + } else { + sb.WriteString(styles.Description.Render(c.description.val)) } var negative string diff --git a/field_input.go b/field_input.go index 1b254fb1..db1d4240 100644 --- a/field_input.go +++ b/field_input.go @@ -402,15 +402,19 @@ func (i *Input) View() string { var sb strings.Builder if i.title.val != "" || i.title.fn != nil { - sb.WriteString(styles.Title.Width(i.width).Render(i.title.val)) if !i.inline { + sb.WriteString(styles.Title.Width(i.width).Render(i.title.val)) sb.WriteString("\n") + } else { + sb.WriteString(styles.Title.Render(i.title.val)) } } if i.description.val != "" || i.description.fn != nil { - sb.WriteString(styles.Description.Width(i.width).Render(i.description.val)) if !i.inline { + sb.WriteString(styles.Description.Width(i.width).Render(i.description.val)) sb.WriteString("\n") + } else { + sb.WriteString(styles.Description.Render(i.description.val)) } } sb.WriteString(i.textinput.View()) diff --git a/field_select.go b/field_select.go index 8d565467..c8fadcb1 100644 --- a/field_select.go +++ b/field_select.go @@ -538,7 +538,9 @@ func (s *Select[T]) titleView() string { if s.filtering { sb.WriteString(s.filter.View()) } else if s.filter.Value() != "" && !s.inline { - sb.WriteString(styles.Title.Width(s.width).Render(s.title.val) + styles.Description.Render("/"+s.filter.Value())) + sb.WriteString(styles.Title.Width(s.width).Render(s.title.val) + styles.Description.Width(s.width).Render("/"+s.filter.Value())) + } else if s.inline { + sb.WriteString(styles.Title.Render(s.title.val)) } else { sb.WriteString(styles.Title.Width(s.width).Render(s.title.val)) } @@ -549,6 +551,9 @@ func (s *Select[T]) titleView() string { } func (s *Select[T]) descriptionView() string { + if s.inline { + return s.activeStyles().Description.Render(s.description.val) + } return s.activeStyles().Description.Width(s.width).Render(s.description.val) } @@ -569,9 +574,9 @@ func (s *Select[T]) optionsView() string { if s.inline { sb.WriteString(styles.PrevIndicator.Faint(s.selected <= 0).String()) if len(s.filteredOptions) > 0 { - sb.WriteString(styles.SelectedOption.Width(width).Render(s.filteredOptions[s.selected].Key)) + sb.WriteString(styles.SelectedOption.Render(s.filteredOptions[s.selected].Key)) } else { - sb.WriteString(styles.TextInput.Placeholder.Width(width).Render("No matches")) + sb.WriteString(styles.TextInput.Placeholder.Render("No matches")) } sb.WriteString(styles.NextIndicator.Faint(s.selected == len(s.filteredOptions)-1).String()) return sb.String() From f8b3922524b93599faaa2a464a8075279f141786 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Wed, 27 Nov 2024 14:05:10 -0800 Subject: [PATCH 21/28] Revert "fix: calculate offset from previous field heights" This reverts commit b41c5e7b7109638d27b6324aff64db27a13188cd. --- group.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/group.go b/group.go index 92bf2c04..b4973b5c 100644 --- a/group.go +++ b/group.go @@ -296,18 +296,18 @@ func (g *Group) fullHeight() int { func (g *Group) getContent() (int, string) { var fields strings.Builder - var offset int + offset := 0 + gap := "\n\n" - // If the focused field is requesting it be zoomed, only show that field. + // 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()) } else { g.selector.Range(func(i int, field Field) bool { fields.WriteString(field.View()) - // Set the offset to the height of the previous field(s). - if i < g.selector.Index() { - offset += lipgloss.Height(field.View()) + if i == g.selector.Index() { + offset = lipgloss.Height(fields.String()) - lipgloss.Height(field.View()) } if i < g.selector.Total()-1 { fields.WriteString(gap) From 152f09d3782baa9b0873bc98842f524695e5a65a Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 7 Jan 2025 15:15:45 -0800 Subject: [PATCH 22/28] refactor: clean up Theme --- field_confirm.go | 7 +++---- field_filepicker.go | 7 +++---- field_input.go | 7 +++---- field_multiselect.go | 7 +++---- field_note.go | 7 +++---- field_select.go | 7 +++---- field_text.go | 7 +++---- 7 files changed, 21 insertions(+), 28 deletions(-) diff --git a/field_confirm.go b/field_confirm.go index 9dec441a..934b2cfd 100644 --- a/field_confirm.go +++ b/field_confirm.go @@ -229,11 +229,10 @@ func (c *Confirm) activeStyles() *FieldStyles { // Theme returns the theme of the field. func (c *Confirm) Theme() *Theme { - theme := c.theme - if theme == nil { - theme = ThemeCharm() + if c.theme != nil { + return c.theme } - return theme + return ThemeCharm() } // View renders the confirm field. diff --git a/field_filepicker.go b/field_filepicker.go index 9533b62f..806d6b53 100644 --- a/field_filepicker.go +++ b/field_filepicker.go @@ -256,11 +256,10 @@ func (f *FilePicker) activeStyles() *FieldStyles { // Theme returns the theme of the field. func (f *FilePicker) Theme() *Theme { - theme := f.theme - if theme == nil { - theme = ThemeCharm() + if f.theme != nil { + return f.theme } - return theme + return ThemeCharm() } // View renders the file field. diff --git a/field_input.go b/field_input.go index 93a1513b..765d8e62 100644 --- a/field_input.go +++ b/field_input.go @@ -370,11 +370,10 @@ func (i *Input) activeStyles() *FieldStyles { // Theme returns the theme of the field. func (i *Input) Theme() *Theme { - theme := i.theme - if theme == nil { - theme = ThemeCharm() + if i.theme != nil { + return i.theme } - return theme + return ThemeCharm() } // View renders the input field. diff --git a/field_multiselect.go b/field_multiselect.go index ebcdb616..c0e480de 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -510,11 +510,10 @@ func (m *MultiSelect[T]) activeStyles() *FieldStyles { // Theme returns the theme of the field. func (m *MultiSelect[T]) Theme() *Theme { - theme := m.theme - if theme == nil { - theme = ThemeCharm() + if m.theme != nil { + return m.theme } - return theme + return ThemeCharm() } func (m *MultiSelect[T]) titleView() string { diff --git a/field_note.go b/field_note.go index afbfcfdb..31c497b5 100644 --- a/field_note.go +++ b/field_note.go @@ -213,11 +213,10 @@ func (n *Note) activeStyles() *FieldStyles { // Theme returns the theme of the field. func (n *Note) Theme() *Theme { - theme := n.theme - if theme == nil { - theme = ThemeCharm() + if n.theme != nil { + return n.theme } - return theme + return ThemeCharm() } // View renders the note field. diff --git a/field_select.go b/field_select.go index c8fadcb1..ebea664b 100644 --- a/field_select.go +++ b/field_select.go @@ -523,11 +523,10 @@ func (s *Select[T]) activeStyles() *FieldStyles { // Theme returns the theme of the field. func (s *Select[T]) Theme() *Theme { - theme := s.theme - if theme == nil { - theme = ThemeCharm() + if s.theme != nil { + return s.theme } - return theme + return ThemeCharm() } func (s *Select[T]) titleView() string { diff --git a/field_text.go b/field_text.go index a7667373..fe995fc3 100644 --- a/field_text.go +++ b/field_text.go @@ -351,11 +351,10 @@ func (t *Text) activeStyles() *FieldStyles { // Theme returns the theme of the field. func (t *Text) Theme() *Theme { - theme := t.theme - if theme == nil { - theme = ThemeCharm() + if t.theme != nil { + return t.theme } - return theme + return ThemeCharm() } func (t *Text) activeTextAreaStyles() *textarea.Style { From b10915bf8cba67ebb32293f387f9f147e2bf12d8 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 7 Jan 2025 18:04:09 -0800 Subject: [PATCH 23/28] test(layout): add grid layout test --- go.mod | 2 ++ go.sum | 4 ++++ group.go | 1 - huh_test.go | 30 ++++++++++++++++++++++++++++++ testdata/TestLayoutGrid.golden | 23 +++++++++++++++++++++++ 5 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 testdata/TestLayoutGrid.golden diff --git a/go.mod b/go.mod index be0de557..4a4774c5 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/bubbletea v1.2.2 github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/x/ansi v0.4.5 + github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 github.com/mitchellh/hashstructure/v2 v2.0.2 ) @@ -15,6 +16,7 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymanbagabas/go-udiff v0.2.0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect diff --git a/go.sum b/go.sum index 4442611d..47e3b97b 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= @@ -14,6 +16,8 @@ github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= diff --git a/group.go b/group.go index b4973b5c..c22d8ef9 100644 --- a/group.go +++ b/group.go @@ -297,7 +297,6 @@ func (g *Group) fullHeight() int { func (g *Group) getContent() (int, string) { var fields strings.Builder offset := 0 - gap := "\n\n" // if the focused field is requesting it be zoomed, only show that field. if g.selector.Selected().Zoom() { diff --git a/huh_test.go b/huh_test.go index 57288b3b..83f9eef8 100644 --- a/huh_test.go +++ b/huh_test.go @@ -13,6 +13,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/exp/golden" ) var pretty = lipgloss.NewStyle(). @@ -918,6 +919,35 @@ func TestAbort(t *testing.T) { } } +func TestLayoutGrid(t *testing.T) { + form := NewForm( + NewGroup( + NewInput().Title("First"), + NewInput().Title("Second"), + NewInput().Title("Third"), + ), + NewGroup( + NewInput().Title("Fourth"), + NewInput().Title("Fifth"), + NewInput().Title("Sixth"), + ), + NewGroup( + NewInput().Title("Seventh"), + NewInput().Title("Eigth"), + NewInput().Title("Nineth"), + NewInput().Title("Tenth"), + ), + NewGroup( + NewInput().Title("Eleventh"), + NewInput().Title("Twelveth"), + NewInput().Title("Thirteenth"), + ), + ).WithLayout(LayoutGrid(2, 2)).WithShowHelp(false) + want := ansi.Strip(form.View()) + t.Log(want) + golden.RequireEqual(t, []byte(want)) +} + // formProgram returns a new Form with a nil input and output, so it can be used as a test program. func formProgram() *Form { return NewForm(NewGroup(NewInput().Title("Foo"))). diff --git a/testdata/TestLayoutGrid.golden b/testdata/TestLayoutGrid.golden new file mode 100644 index 00000000..55fac680 --- /dev/null +++ b/testdata/TestLayoutGrid.golden @@ -0,0 +1,23 @@ + First Fourth + > > + + Second Fifth + > > + + Third Sixth + > > + + Seventh Eleventh + > > + + Eigth Twelveth + > > + + Nineth Thirteenth + > > + + Tenth + > + + + \ No newline at end of file From 50eb69107640f25dc2574c461611535b87a93cb0 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Tue, 7 Jan 2025 18:25:32 -0800 Subject: [PATCH 24/28] refactor(group): clarify height subtraction --- group.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/group.go b/group.go index c22d8ef9..19b29d51 100644 --- a/group.go +++ b/group.go @@ -10,7 +10,10 @@ import ( "github.com/charmbracelet/lipgloss" ) -const gap string = "\n\n" +const ( + gap string = "\n\n" + helpMenuHeight int = 1 +) // Group is a collection of fields that are displayed together with a page of // the form. While a group is displayed the form completer can switch between @@ -270,7 +273,7 @@ func (g *Group) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: - g.WithHeight(min(g.fullHeight(), msg.Height-1)) + g.WithHeight(min(g.fullHeight(), msg.Height-helpMenuHeight)) g.WithWidth(msg.Width) case nextFieldMsg: cmds = append(cmds, g.nextField()...) From dcf48300e2de7890cb4603f61f04453b0250f8c5 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Wed, 8 Jan 2025 09:07:41 -0800 Subject: [PATCH 25/28] fix(layout): don't set group width to window width --- group.go | 1 - 1 file changed, 1 deletion(-) diff --git a/group.go b/group.go index 19b29d51..6fc86031 100644 --- a/group.go +++ b/group.go @@ -274,7 +274,6 @@ func (g *Group) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: g.WithHeight(min(g.fullHeight(), msg.Height-helpMenuHeight)) - g.WithWidth(msg.Width) case nextFieldMsg: cmds = append(cmds, g.nextField()...) case prevFieldMsg: From 831bdb8653c36363957a058b77131edeffeb8368 Mon Sep 17 00:00:00 2001 From: bashbunni <15822994+bashbunni@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:32:33 -0800 Subject: [PATCH 26/28] Update form.go Co-authored-by: Carlos Alexandro Becker --- form.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/form.go b/form.go index 79c73935..5f8f3a6f 100644 --- a/form.go +++ b/form.go @@ -524,7 +524,7 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } f.selector.Range(func(_ int, group *Group) bool { if group.fullHeight() > msg.Height { - group.WithHeight(msg.Height - 1) + group.WithHeight(msg.Height - 1) // subtracts help height } return true }) From 4d652c1c5424efcfc3e299f2baf6bbb0e74e1916 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 9 Jan 2025 06:19:58 -0800 Subject: [PATCH 27/28] refactor: remove helpMenuHeight const --- group.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/group.go b/group.go index 6fc86031..52a0109c 100644 --- a/group.go +++ b/group.go @@ -11,8 +11,7 @@ import ( ) const ( - gap string = "\n\n" - helpMenuHeight int = 1 + gap string = "\n\n" ) // Group is a collection of fields that are displayed together with a page of @@ -273,7 +272,7 @@ func (g *Group) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: - g.WithHeight(min(g.fullHeight(), msg.Height-helpMenuHeight)) + g.WithHeight(min(g.fullHeight(), msg.Height-1)) // subtracts help height case nextFieldMsg: cmds = append(cmds, g.nextField()...) case prevFieldMsg: From b633a2dfa11a1dcbe2dbb0e699c9b62b81e9df5d Mon Sep 17 00:00:00 2001 From: bashbunni Date: Thu, 9 Jan 2025 06:21:20 -0800 Subject: [PATCH 28/28] Revert "test(layout): add grid layout test" This reverts commit b10915bf8cba67ebb32293f387f9f147e2bf12d8. --- go.mod | 2 -- go.sum | 4 ---- group.go | 1 + huh_test.go | 30 ------------------------------ testdata/TestLayoutGrid.golden | 23 ----------------------- 5 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 testdata/TestLayoutGrid.golden diff --git a/go.mod b/go.mod index 4a4774c5..be0de557 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/charmbracelet/bubbletea v1.2.2 github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/x/ansi v0.4.5 - github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 github.com/mitchellh/hashstructure/v2 v2.0.2 ) @@ -16,7 +15,6 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymanbagabas/go-udiff v0.2.0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect diff --git a/go.sum b/go.sum index 47e3b97b..4442611d 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= @@ -16,8 +14,6 @@ github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= diff --git a/group.go b/group.go index 52a0109c..23bae6e5 100644 --- a/group.go +++ b/group.go @@ -298,6 +298,7 @@ func (g *Group) fullHeight() int { func (g *Group) getContent() (int, string) { var fields strings.Builder offset := 0 + gap := "\n\n" // if the focused field is requesting it be zoomed, only show that field. if g.selector.Selected().Zoom() { diff --git a/huh_test.go b/huh_test.go index 83f9eef8..57288b3b 100644 --- a/huh_test.go +++ b/huh_test.go @@ -13,7 +13,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/exp/golden" ) var pretty = lipgloss.NewStyle(). @@ -919,35 +918,6 @@ func TestAbort(t *testing.T) { } } -func TestLayoutGrid(t *testing.T) { - form := NewForm( - NewGroup( - NewInput().Title("First"), - NewInput().Title("Second"), - NewInput().Title("Third"), - ), - NewGroup( - NewInput().Title("Fourth"), - NewInput().Title("Fifth"), - NewInput().Title("Sixth"), - ), - NewGroup( - NewInput().Title("Seventh"), - NewInput().Title("Eigth"), - NewInput().Title("Nineth"), - NewInput().Title("Tenth"), - ), - NewGroup( - NewInput().Title("Eleventh"), - NewInput().Title("Twelveth"), - NewInput().Title("Thirteenth"), - ), - ).WithLayout(LayoutGrid(2, 2)).WithShowHelp(false) - want := ansi.Strip(form.View()) - t.Log(want) - golden.RequireEqual(t, []byte(want)) -} - // formProgram returns a new Form with a nil input and output, so it can be used as a test program. func formProgram() *Form { return NewForm(NewGroup(NewInput().Title("Foo"))). diff --git a/testdata/TestLayoutGrid.golden b/testdata/TestLayoutGrid.golden deleted file mode 100644 index 55fac680..00000000 --- a/testdata/TestLayoutGrid.golden +++ /dev/null @@ -1,23 +0,0 @@ - First Fourth - > > - - Second Fifth - > > - - Third Sixth - > > - - Seventh Eleventh - > > - - Eigth Twelveth - > > - - Nineth Thirteenth - > > - - Tenth - > - - - \ No newline at end of file