diff --git a/field_confirm.go b/field_confirm.go index 54f57d7f..debed0f4 100644 --- a/field_confirm.go +++ b/field_confirm.go @@ -44,7 +44,6 @@ func NewConfirm() *Confirm { affirmative: "Yes", negative: "No", validate: func(bool) error { return nil }, - theme: ThemeCharm(), } } @@ -158,12 +157,20 @@ func (c *Confirm) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return c, tea.Batch(cmds...) } -// View renders the confirm field. -func (c *Confirm) View() string { - styles := c.theme.Blurred +func (c *Confirm) activeStyles() *FieldStyles { + theme := c.theme + if theme == nil { + theme = ThemeCharm() + } if c.focused { - styles = c.theme.Focused + return &theme.Focused } + return &theme.Blurred +} + +// View renders the confirm field. +func (c *Confirm) View() string { + styles := c.activeStyles() var sb strings.Builder sb.WriteString(styles.Title.Render(c.title)) @@ -211,10 +218,11 @@ func (c *Confirm) Run() error { // runAccessible runs the confirm field in accessible mode. func (c *Confirm) runAccessible() error { - fmt.Println(c.theme.Blurred.Base.Render(c.theme.Focused.Title.Render(c.title))) + styles := c.activeStyles() + fmt.Println(styles.Title.Render(c.title)) fmt.Println() *c.value = accessibility.PromptBool() - fmt.Println(c.theme.Focused.SelectedOption.Render("Chose: "+c.String()) + "\n") + fmt.Println(styles.SelectedOption.Render("Chose: "+c.String()) + "\n") return nil } @@ -227,6 +235,9 @@ func (c *Confirm) String() string { // WithTheme sets the theme of the confirm field. func (c *Confirm) WithTheme(theme *Theme) Field { + if c.theme != nil { + return c + } c.theme = theme return c } diff --git a/field_filepicker.go b/field_filepicker.go index 3c9ffe01..952e64a6 100644 --- a/field_filepicker.go +++ b/field_filepicker.go @@ -55,7 +55,6 @@ func NewFilePicker() *FilePicker { value: new(string), validate: func(string) error { return nil }, picker: fp, - theme: ThemeCharm(), } } @@ -235,12 +234,21 @@ func (f *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return f, cmd } -// View renders the file field. -func (f *FilePicker) View() string { - styles := f.theme.Blurred +func (f *FilePicker) activeStyles() *FieldStyles { + theme := f.theme + if theme == nil { + theme = ThemeCharm() + } if f.focused { - styles = f.theme.Focused + return &theme.Focused } + return &theme.Blurred +} + +// View renders the file field. +func (f *FilePicker) View() string { + styles := f.activeStyles() + var sb strings.Builder if f.title != "" { sb.WriteString(styles.Title.Render(f.title) + "\n") @@ -286,7 +294,8 @@ func (f *FilePicker) Run() error { // runAccessible runs an accessible file field. func (f *FilePicker) runAccessible() error { - fmt.Println(f.theme.Blurred.Base.Render(f.theme.Focused.Title.Render(f.title))) + styles := f.activeStyles() + fmt.Println(styles.Title.Render(f.title)) fmt.Println() validateFile := func(s string) error { @@ -312,12 +321,15 @@ func (f *FilePicker) runAccessible() error { } *f.value = accessibility.PromptString("File: ", validateFile) - fmt.Println(f.theme.Focused.SelectedOption.Render(*f.value + "\n")) + fmt.Println(styles.SelectedOption.Render(*f.value + "\n")) return nil } // WithTheme sets the theme of the file field. func (f *FilePicker) WithTheme(theme *Theme) Field { + if f.theme != nil || theme == nil { + return f + } f.theme = theme // XXX: add specific themes diff --git a/field_input.go b/field_input.go index 6470b305..ad0b06b1 100644 --- a/field_input.go +++ b/field_input.go @@ -47,7 +47,6 @@ func NewInput() *Input { value: new(string), textinput: input, validate: func(string) error { return nil }, - theme: ThemeCharm(), } return i @@ -230,12 +229,20 @@ func (i *Input) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return i, tea.Batch(cmds...) } -// View renders the input field. -func (i *Input) View() string { - styles := i.theme.Blurred +func (i *Input) activeStyles() *FieldStyles { + theme := i.theme + if theme == nil { + theme = ThemeCharm() + } if i.focused { - styles = i.theme.Focused + return &theme.Focused } + return &theme.Blurred +} + +// View renders the input field. +func (i *Input) View() string { + styles := i.activeStyles() // NB: since the method is on a pointer receiver these are being mutated. // Because this runs on every render this shouldn't matter in practice, @@ -278,10 +285,11 @@ func (i *Input) run() error { // runAccessible runs the input field in accessible mode. func (i *Input) runAccessible() error { - fmt.Println(i.theme.Blurred.Base.Render(i.theme.Focused.Title.Render(i.title))) + styles := i.activeStyles() + fmt.Println(styles.Title.Render(i.title)) fmt.Println() *i.value = accessibility.PromptString("Input: ", i.validate) - fmt.Println(i.theme.Focused.SelectedOption.Render("Input: " + *i.value + "\n")) + fmt.Println(styles.SelectedOption.Render("Input: " + *i.value + "\n")) return nil } @@ -300,17 +308,21 @@ func (i *Input) WithAccessible(accessible bool) Field { // WithTheme sets the theme of the input field. func (i *Input) WithTheme(theme *Theme) Field { + if i.theme != nil { + return i + } i.theme = theme return i } // WithWidth sets the width of the input field. func (i *Input) WithWidth(width int) Field { + styles := i.activeStyles() i.width = width - frameSize := i.theme.Blurred.Base.GetHorizontalFrameSize() + frameSize := styles.Base.GetHorizontalFrameSize() promptWidth := lipgloss.Width(i.textinput.PromptStyle.Render(i.textinput.Prompt)) - titleWidth := lipgloss.Width(i.theme.Focused.Title.Render(i.title)) - descriptionWidth := lipgloss.Width(i.theme.Focused.Description.Render(i.description)) + titleWidth := lipgloss.Width(styles.Title.Render(i.title)) + descriptionWidth := lipgloss.Width(styles.Description.Render(i.description)) i.textinput.Width = width - frameSize - promptWidth - 1 if i.inline { i.textinput.Width -= titleWidth diff --git a/field_multiselect.go b/field_multiselect.go index 6a752e58..a9fb70a5 100644 --- a/field_multiselect.go +++ b/field_multiselect.go @@ -55,7 +55,6 @@ func NewMultiSelect[T comparable]() *MultiSelect[T] { validate: func([]T) error { return nil }, filtering: false, filter: filter, - theme: ThemeCharm(), } } @@ -306,11 +305,6 @@ func (m *MultiSelect[T]) updateViewportHeight() { return } - // Wait until the theme has appied or things'll panic. - if m.theme == nil { - return - } - const minHeight = 1 m.viewport.Height = max(minHeight, m.height- lipgloss.Height(m.titleView())- @@ -338,10 +332,14 @@ func (m *MultiSelect[T]) finalize() { } func (m *MultiSelect[T]) activeStyles() *FieldStyles { + theme := m.theme + if theme == nil { + theme = ThemeCharm() + } if m.focused { - return &m.theme.Focused + return &theme.Focused } - return &m.theme.Blurred + return &theme.Blurred } func (m *MultiSelect[T]) titleView() string { @@ -419,21 +417,22 @@ func (m *MultiSelect[T]) View() string { } func (m *MultiSelect[T]) printOptions() { + styles := m.activeStyles() var sb strings.Builder - sb.WriteString(m.theme.Focused.Title.Render(m.title)) + sb.WriteString(styles.Title.Render(m.title)) sb.WriteString("\n") for i, option := range m.options { if option.selected { - sb.WriteString(m.theme.Focused.SelectedOption.Render(fmt.Sprintf("%d. %s %s", i+1, "✓", option.Key))) + sb.WriteString(styles.SelectedOption.Render(fmt.Sprintf("%d. %s %s", i+1, "✓", option.Key))) } else { sb.WriteString(fmt.Sprintf("%d. %s %s", i+1, " ", option.Key)) } sb.WriteString("\n") } - fmt.Println(m.theme.Blurred.Base.Render(sb.String())) + fmt.Println(sb.String()) } // setFilter sets the filter of the select field. @@ -464,6 +463,7 @@ func (m *MultiSelect[T]) Run() error { // runAccessible() runs the multi-select field in accessible mode. func (m *MultiSelect[T]) runAccessible() error { m.printOptions() + styles := m.activeStyles() var choice int for { @@ -503,12 +503,15 @@ func (m *MultiSelect[T]) runAccessible() error { } } - fmt.Println(m.theme.Focused.SelectedOption.Render("Selected:", strings.Join(values, ", ")+"\n")) + fmt.Println(styles.SelectedOption.Render("Selected:", strings.Join(values, ", ")+"\n")) return nil } // WithTheme sets the theme of the multi-select field. func (m *MultiSelect[T]) WithTheme(theme *Theme) Field { + if m.theme != nil { + return m + } m.theme = theme m.filter.Cursor.Style = m.theme.Focused.TextInput.Cursor m.filter.PromptStyle = m.theme.Focused.TextInput.Prompt diff --git a/field_note.go b/field_note.go index b5b1c039..43bdd36c 100644 --- a/field_note.go +++ b/field_note.go @@ -31,7 +31,6 @@ type Note struct { func NewNote() *Note { return &Note{ showNextButton: false, - theme: ThemeCharm(), skip: true, } } @@ -106,15 +105,22 @@ func (n *Note) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return n, nil } -// View renders the note field. -func (n *Note) View() string { - styles := n.theme.Blurred +func (n *Note) activeStyles() *FieldStyles { + theme := n.theme + if theme == nil { + theme = ThemeCharm() + } if n.focused { - styles = n.theme.Focused + return &theme.Focused } + return &theme.Focused +} +// View renders the note field. +func (n *Note) View() string { var ( - sb strings.Builder + styles = n.activeStyles() + sb strings.Builder ) if n.title != "" { @@ -148,13 +154,16 @@ func (n *Note) runAccessible() error { body += n.description - fmt.Println(n.theme.Blurred.Base.Render(body)) + fmt.Println(body) fmt.Println() return nil } // WithTheme sets the theme on a note field. func (n *Note) WithTheme(theme *Theme) Field { + if n.theme != nil { + return n + } n.theme = theme return n } diff --git a/field_select.go b/field_select.go index f60433a2..8e503b05 100644 --- a/field_select.go +++ b/field_select.go @@ -54,7 +54,6 @@ func NewSelect[T comparable]() *Select[T] { validate: func(T) error { return nil }, filtering: false, filter: filter, - theme: ThemeCharm(), } } @@ -323,11 +322,6 @@ func (s *Select[T]) updateViewportHeight() { return } - // Wait until the theme has appied. - if s.theme == nil { - return - } - const minHeight = 1 s.viewport.Height = max(minHeight, s.height- lipgloss.Height(s.titleView())- @@ -335,13 +329,14 @@ func (s *Select[T]) updateViewportHeight() { } func (s *Select[T]) activeStyles() *FieldStyles { - if s.theme == nil { - return nil + theme := s.theme + if theme == nil { + theme = ThemeCharm() } if s.focused { - return &s.theme.Focused + return &theme.Focused } - return &s.theme.Blurred + return &theme.Blurred } func (s *Select[T]) titleView() string { @@ -462,15 +457,16 @@ func (s *Select[T]) Run() error { // runAccessible runs an accessible select field. func (s *Select[T]) runAccessible() error { var sb strings.Builder + styles := s.activeStyles() - sb.WriteString(s.theme.Focused.Title.Render(s.title) + "\n") + sb.WriteString(styles.Title.Render(s.title) + "\n") for i, option := range s.options { sb.WriteString(fmt.Sprintf("%d. %s", i+1, option.Key)) sb.WriteString("\n") } - fmt.Println(s.theme.Blurred.Base.Render(sb.String())) + fmt.Println(sb.String()) for { choice := accessibility.PromptInt("Choose: ", 1, len(s.options)) @@ -479,7 +475,7 @@ func (s *Select[T]) runAccessible() error { fmt.Println(err.Error()) continue } - fmt.Println(s.theme.Focused.SelectedOption.Render("Chose: " + option.Key + "\n")) + fmt.Println(styles.SelectedOption.Render("Chose: " + option.Key + "\n")) *s.value = option.Value break } @@ -489,6 +485,9 @@ func (s *Select[T]) runAccessible() error { // WithTheme sets the theme of the select field. func (s *Select[T]) WithTheme(theme *Theme) Field { + if s.theme != nil { + return s + } s.theme = theme s.filter.Cursor.Style = s.theme.Focused.TextInput.Cursor s.filter.PromptStyle = s.theme.Focused.TextInput.Prompt diff --git a/field_text.go b/field_text.go index cb857fc9..a9da3ffa 100644 --- a/field_text.go +++ b/field_text.go @@ -58,7 +58,6 @@ func NewText() *Text { editorCmd: editorCmd, editorArgs: editorArgs, editorExtension: "md", - theme: ThemeCharm(), } return t @@ -243,19 +242,31 @@ func (t *Text) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return t, tea.Batch(cmds...) } -// View renders the text field. -func (t *Text) View() string { - var ( - styles FieldStyles - textareaStyles *textarea.Style - ) +func (t *Text) activeStyles() *FieldStyles { + theme := t.theme + if theme == nil { + theme = ThemeCharm() + } if t.focused { - styles = t.theme.Focused - textareaStyles = &t.textarea.FocusedStyle - } else { - styles = t.theme.Blurred - textareaStyles = &t.textarea.BlurredStyle + return &theme.Focused } + return &theme.Blurred +} + +func (t *Text) activeTextAreaStyles() *textarea.Style { + if t.theme == nil { + return &t.textarea.BlurredStyle + } + if t.focused { + return &t.textarea.FocusedStyle + } + return &t.textarea.BlurredStyle +} + +// View renders the text field. +func (t *Text) View() string { + var styles = t.activeStyles() + var textareaStyles = t.activeTextAreaStyles() // NB: since the method is on a pointer receiver these are being mutated. // Because this runs on every render this shouldn't matter in practice, @@ -293,7 +304,8 @@ func (t *Text) Run() error { // runAccessible runs an accessible text field. func (t *Text) runAccessible() error { - fmt.Println(t.theme.Blurred.Base.Render(t.theme.Focused.Title.Render(t.title))) + styles := t.activeStyles() + fmt.Println(styles.Title.Render(t.title)) fmt.Println() *t.value = accessibility.PromptString("Input: ", func(input string) error { if err := t.validate(input); err != nil { @@ -312,6 +324,9 @@ func (t *Text) runAccessible() error { // WithTheme sets the theme on a text field. func (t *Text) WithTheme(theme *Theme) Field { + if t.theme != nil { + return t + } t.theme = theme return t } @@ -332,7 +347,7 @@ func (t *Text) WithAccessible(accessible bool) Field { // WithWidth sets the width of the text field. func (t *Text) WithWidth(width int) Field { t.width = width - t.textarea.SetWidth(width - t.theme.Blurred.Base.GetHorizontalFrameSize()) + t.textarea.SetWidth(width - t.activeStyles().Base.GetHorizontalFrameSize()) return t } @@ -345,7 +360,7 @@ func (t *Text) WithHeight(height int) Field { if t.description != "" { adjust++ } - t.textarea.SetHeight(height - t.theme.Blurred.Base.GetVerticalFrameSize() - adjust) + t.textarea.SetHeight(height - t.activeStyles().Base.GetVerticalFrameSize() - adjust) return t } diff --git a/form.go b/form.go index 083880d5..e122710b 100644 --- a/form.go +++ b/form.go @@ -60,7 +60,6 @@ type Form struct { // options width int height int - theme *Theme keymap *KeyMap teaOptions []tea.ProgramOption output io.Writer @@ -78,7 +77,6 @@ func NewForm(groups ...*Group) *Form { f := &Form{ groups: groups, paginator: p, - theme: ThemeCharm(), keymap: NewDefaultKeyMap(), results: make(map[string]any), teaOptions: []tea.ProgramOption{ @@ -88,7 +86,6 @@ func NewForm(groups ...*Group) *Form { // NB: If dynamic forms come into play this will need to be applied when // groups and fields are added. - f.WithTheme(f.theme) f.WithKeyMap(f.keymap) f.WithWidth(f.width) f.WithHeight(f.height) @@ -238,7 +235,6 @@ func (f *Form) WithTheme(theme *Theme) *Form { if theme == nil { return f } - f.theme = theme for _, group := range f.groups { group.WithTheme(theme) } diff --git a/group.go b/group.go index 1bbb6767..ee97fa36 100644 --- a/group.go +++ b/group.go @@ -38,7 +38,6 @@ type Group struct { // group options width int height int - theme *Theme keymap *KeyMap hide func() bool } @@ -91,7 +90,6 @@ func (g *Group) WithShowErrors(show bool) *Group { // WithTheme sets the theme on a group. func (g *Group) WithTheme(t *Theme) *Group { - g.theme = t g.help.Styles = t.Help for _, field := range g.fields { field.WithTheme(t) @@ -256,17 +254,7 @@ func (g *Group) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // height returns the full height of the group. func (g *Group) fullHeight() int { - var height int - - if g.theme == nil { - return g.height // unknown - } - - gap := g.theme.FieldSeparator.String() - if gap != "" { - height += len(g.fields) - } - + height := len(g.fields) for _, f := range g.fields { height += lipgloss.Height(f.View()) } @@ -276,10 +264,7 @@ func (g *Group) fullHeight() int { func (g *Group) buildView() { var fields strings.Builder offset := 0 - gap := g.theme.FieldSeparator.String() - if gap == "" { - gap = "\n" - } + gap := "\n\n" // if the focused field is requesting it be zoomed, only show that field. if g.fields[g.paginator.Page].Zoom() { @@ -312,7 +297,7 @@ func (g *Group) View() string { } if g.showErrors { for _, err := range errors { - view.WriteString(g.theme.Focused.ErrorMessage.Render(err.Error())) + view.WriteString(ThemeCharm().Focused.ErrorMessage.Render(err.Error())) } } return view.String() diff --git a/huh_test.go b/huh_test.go index d86440f5..cdae761d 100644 --- a/huh_test.go +++ b/huh_test.go @@ -692,7 +692,6 @@ func TestPrevGroup(t *testing.T) { func TestNote(t *testing.T) { field := NewNote().Title("Taco").Description("How may we take your order?").Next(true) f := NewForm(NewGroup(field)) - f.theme.Focused.Base.Border(lipgloss.HiddenBorder()) f.Update(f.Init()) view := ansi.Strip(f.View())