charmbracelet / huh

Build terminal forms and prompts 🤷🏻‍♀️
MIT License
4.23k stars 115 forks source link

Export theme.copy #119

Closed ryan-timothy-albert closed 8 months ago

ryan-timothy-albert commented 8 months ago

When customizing themes for a form I frequently want to make a copy from a base theme. The theme copy function is currently not exported.

To get around this I have to write my own function that deep copies every field of theme. Basically exactly what theme.copy does.

Are you open to exporting theme.copy? I would be happy to put up a PR.

maaslalani commented 8 months ago

I think this is super reasonable! What do you think @meowgorithm?

meowgorithm commented 8 months ago

Yep, I think that makes sense, but @ryan-timothy-albert is your use case to modify an existing theme or is this for completely custom themes? Asking as you can create copies of themes with their constructor (i.e. huh.ThemeCharm()) and then alter them from there.

t := huh.ThemeCharm()
t.Base = t.Base.Padding(4, 8) // add loads of padding to the form
ryan-timothy-albert commented 8 months ago

Creating completely custom themes off of huh.ThemeBase. For some reason when I didn't copy it I was trouble having getting some of my custom lipgloss styling to appear.

https://github.com/charmbracelet/huh/pull/124 Here is a PR if it's something that makes sense

meowgorithm commented 8 months ago

The PR makes sense, but could you also post some sample code here showing how you're trying to do it currently? We generally try and avoid increasing API surface area unless totally necessary.

ryan-timothy-albert commented 8 months ago

The PR makes sense, but could you also post some sample code here showing how you're trying to do it currently? We generally try and avoid increasing API surface area unless totally necessary.

var formTheme *huh.Theme

func init() {
    t := copyBaseTheme(huh.ThemeBase())

    f := &t.Focused
    f.Base = f.Base.BorderForeground(styles.Focused.GetForeground())
    f.Title.Foreground(styles.Focused.GetForeground()).Bold(true)
    f.Description.Foreground(styles.Dimmed.GetForeground()).Italic(true).Inline(false)
    f.ErrorIndicator.Foreground(styles.Colors.Red)
    f.ErrorMessage.Foreground(styles.Colors.Red)
    f.SelectSelector.Foreground(styles.Focused.GetForeground())
    f.MultiSelectSelector.Foreground(styles.Focused.GetForeground())
    f.SelectedOption.Foreground(styles.Focused.GetForeground())
    f.FocusedButton.Background(styles.Colors.Green)
    f.BlurredButton.Background(styles.Dimmed.GetForeground())
    f.Next = f.FocusedButton.Copy()

    f.TextInput.Cursor.Foreground(styles.Focused.GetForeground())
    f.TextInput.Placeholder.Foreground(styles.Dimmed.GetForeground()).Italic(true)
    f.TextInput.Prompt.Foreground(styles.Focused.GetForeground())
    f.TextInput.Text.Foreground(styles.Focused.GetForeground())

    b := &t.Blurred
    b.Description.Italic(true)
    b.TextInput.Placeholder.Italic(true)
    b.SelectedOption.Foreground(styles.FocusedDimmed.GetForeground())
    b.SelectSelector.Foreground(styles.FocusedDimmed.GetForeground())

    formTheme = &t
}

// What I've implemented is a direct duplicate of huh theme.copy()
func copyBaseTheme(original *huh.Theme) huh.Theme {
    return huh.Theme{
        Form:           original.Form.Copy(),
        Group:          original.Group.Copy(),
        FieldSeparator: original.FieldSeparator.Copy(),
        Blurred: huh.FieldStyles{
            Base:                original.Blurred.Base.Copy(),
            Title:               original.Blurred.Title.Copy(),
            Description:         original.Blurred.Description.Copy(),
            ErrorIndicator:      original.Blurred.ErrorIndicator.Copy(),
            ErrorMessage:        original.Blurred.ErrorMessage.Copy(),
            SelectSelector:      original.Blurred.SelectSelector.Copy(),
            Option:              original.Blurred.Option.Copy(),
            MultiSelectSelector: original.Blurred.MultiSelectSelector.Copy(),
            SelectedOption:      original.Blurred.SelectedOption.Copy(),
            SelectedPrefix:      original.Blurred.SelectedPrefix.Copy(),
            UnselectedOption:    original.Blurred.UnselectedOption.Copy(),
            UnselectedPrefix:    original.Blurred.UnselectedPrefix.Copy(),
            FocusedButton:       original.Blurred.FocusedButton.Copy(),
            BlurredButton:       original.Blurred.BlurredButton.Copy(),
            TextInput: huh.TextInputStyles{
                Cursor:      original.Blurred.TextInput.Cursor.Copy(),
                Placeholder: original.Blurred.TextInput.Placeholder.Copy(),
                Prompt:      original.Blurred.TextInput.Prompt.Copy(),
                Text:        original.Blurred.TextInput.Text.Copy(),
            },
            Card: original.Blurred.Card.Copy(),
            Next: original.Blurred.Next.Copy(),
        },
        Focused: huh.FieldStyles{
            Base:                original.Focused.Base.Copy(),
            Title:               original.Focused.Title.Copy(),
            Description:         original.Focused.Description.Copy(),
            ErrorIndicator:      original.Focused.ErrorIndicator.Copy(),
            ErrorMessage:        original.Focused.ErrorMessage.Copy(),
            SelectSelector:      original.Focused.SelectSelector.Copy(),
            Option:              original.Focused.Option.Copy(),
            MultiSelectSelector: original.Focused.MultiSelectSelector.Copy(),
            SelectedOption:      original.Focused.SelectedOption.Copy(),
            SelectedPrefix:      original.Focused.SelectedPrefix.Copy(),
            UnselectedOption:    original.Focused.UnselectedOption.Copy(),
            UnselectedPrefix:    original.Focused.UnselectedPrefix.Copy(),
            FocusedButton:       original.Focused.FocusedButton.Copy(),
            BlurredButton:       original.Focused.BlurredButton.Copy(),
            TextInput: huh.TextInputStyles{
                Cursor:      original.Focused.TextInput.Cursor.Copy(),
                Placeholder: original.Focused.TextInput.Placeholder.Copy(),
                Prompt:      original.Focused.TextInput.Prompt.Copy(),
                Text:        original.Focused.TextInput.Text.Copy(),
            },
            Card: original.Focused.Card.Copy(),
            Next: original.Focused.Next.Copy(),
        },
        Help: help.Styles{
            Ellipsis:       original.Help.Ellipsis.Copy(),
            ShortKey:       original.Help.ShortKey.Copy(),
            ShortDesc:      original.Help.ShortDesc.Copy(),
            ShortSeparator: original.Help.ShortSeparator.Copy(),
            FullKey:        original.Help.FullKey.Copy(),
            FullDesc:       original.Help.FullDesc.Copy(),
            FullSeparator:  original.Help.FullSeparator.Copy(),
        },
    }
}
meowgorithm commented 8 months ago

This is helpful; thank you. So huh.ThemeBase() does in fact return a copy of the base style despite the fact that it's returning a pointer reference. Themes actually don't exist as global variables (they're created on the fly in their constructor) so can safely mutate the Theme you get after calling huh.ThemeBase().

Unless I'm missing something?

ryan-timothy-albert commented 8 months ago

This is helpful; thank you. So huh.ThemeBase() does in fact return a copy of the base style despite the fact that it's returning a pointer reference. Themes actually don't exist as global variables (they're created on the fly in their constructor) so can safely mutate the Theme you get after calling huh.ThemeBase().

Unless I'm missing something?

I thought that would be the case but when I replace t := huh.ThemeBase() for t := copyBaseTheme(huh.ThemeBase()) some of my color stylings no longer get correctly applied to my form. I was a bit confused about this, not sure if you see anything obviously wrong.

meowgorithm commented 8 months ago

Interesting. This could be a bug internally. Would you mind sharing some of the form code you're using against this so we can have a look on our end?

ryan-timothy-albert commented 8 months ago

Interesting. This could be a bug internally. Would you mind sharing some of the form code you're using against this so we can have a look on our end?

type Model struct {
    title       string
    description string
    form        *huh.Form // huh.Form is just a tea.Model
}

func NewForm(form *huh.Form, args ...string) Model {
    model := Model{
        form: form.WithTheme(formTheme),
    }

    if len(args) > 0 {
        model.title = args[0]
        if len(args) > 1 {
            model.description = args[1]
        }
    }

    return model
}

func (m Model) Init() tea.Cmd {
    return m.form.Init()
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "esc", "ctrl+c", "q":
            return m, tea.Quit
        }
    }

    var cmds []tea.Cmd

    // Process the form
    form, cmd := m.form.Update(msg)
    if f, ok := form.(*huh.Form); ok {
        m.form = f
        cmds = append(cmds, cmd)
    }

    // Quit when the form is done.
    if m.form.State == huh.StateCompleted {
        cmds = append(cmds, tea.Quit)
    }

    return m, tea.Batch(cmds...)
}

func (m Model) View() string {
    if m.form.State == huh.StateCompleted {
        return ""
    }
    titleStyle := lipgloss.NewStyle().Foreground(styles.Focused.GetForeground()).Bold(true)
    descriptionStyle := lipgloss.NewStyle().Foreground(styles.Dimmed.GetForeground()).Italic(true)
    if m.title != "" {
        header := titleStyle.Render(m.title)
        if m.description != "" {
            header += "\n" + descriptionStyle.Render(m.description)
        }
        return header + "\n\n" + m.form.View()
    }

    return m.form.View()
}

This is the wrapper we apply to a form

ryan-timothy-albert commented 8 months ago

Didn't want to create a new issue but had a quick question on customizing the styling of a multi select.

Screenshot 2024-01-31 at 1 53 32 PM

I would love to get rid of the [] for unselected options and [*] for selected options. Ideally I would just have nothing here because highlighting shows what's selected.

@meowgorithm do you know how I can apply this styling change.

ryan-timothy-albert commented 8 months ago
Screenshot 2024-01-31 at 1 55 36 PM

Something similar to the ReadMe demo would work too. I've had a bit of trouble replicating that style when it comes to my multi select.

ryan-timothy-albert commented 8 months ago

I was actually able to solve the problem by reverse engineering the charm theme