diff --git a/examples/dynamic/dynamic-default/main.go b/examples/dynamic/dynamic-default/main.go new file mode 100644 index 00000000..9ae614b9 --- /dev/null +++ b/examples/dynamic/dynamic-default/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/huh" +) + +func main() { + + var name, id, directory string + + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("Project name").Value(&name).Placeholder("").Validate(huh.ValidateNotEmpty()), + huh.NewInput().Title("Project ID").Value(&id).DefaultFunc(func() string { + return strings.ReplaceAll(strings.ToLower(name), "/", "-") + }, &name), + huh.NewInput().Title("Project directory").Value(&directory).DefaultFunc(func() string { + if id == "" { + return "~/projects/" + } + return "~/projects/" + id + }, &id), + ), + ) + + if err := form.Run(); err != nil { + fmt.Println(err) + } + fmt.Println("Project Name: ", name) + fmt.Println("Project ID: ", id) + fmt.Println("Directory: ", directory) +} diff --git a/field_input.go b/field_input.go index 2815c272..cb645395 100644 --- a/field_input.go +++ b/field_input.go @@ -24,10 +24,11 @@ type Input struct { key string id int - title Eval[string] - description Eval[string] - placeholder Eval[string] - suggestions Eval[[]string] + title Eval[string] + description Eval[string] + placeholder Eval[string] + placeholderIsDefault bool + suggestions Eval[[]string] textinput textinput.Model @@ -215,11 +216,27 @@ func (i *Input) Placeholder(str string) *Input { // PlaceholderFunc sets the placeholder func of the text input. func (i *Input) PlaceholderFunc(f func() string, bindings any) *Input { + i.placeholderIsDefault = false i.placeholder.fn = f i.placeholder.bindings = bindings return i } +// Default sets the default value of the text input. This looks the same as a placeholder, +// but it is the field's value unless the user overrides it. +func (i *Input) Default(str string) *Input { + i.Placeholder(str) + i.placeholderIsDefault = true + return i +} + +// DefaultFunc sets the default func of the text input. +func (i *Input) DefaultFunc(f func() string, bindings any) *Input { + i.PlaceholderFunc(f, bindings) + i.placeholderIsDefault = true + return i +} + // Inline sets whether the title and input should be on the same line. func (i *Input) Inline(inline bool) *Input { i.inline = inline @@ -250,7 +267,7 @@ func (i *Input) Focus() tea.Cmd { // Blur blurs the input field. func (i *Input) Blur() tea.Cmd { i.focused = false - i.accessor.Set(i.textinput.Value()) + i.accessor.Set(i.valueOrDefault()) i.textinput.Blur() i.err = i.validate(i.accessor.Get()) return nil @@ -346,18 +363,29 @@ func (i *Input) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, i.keymap.Prev): cmds = append(cmds, PrevField) case key.Matches(msg, i.keymap.Next, i.keymap.Submit): - value := i.textinput.Value() + value := i.valueOrDefault() i.err = i.validate(value) if i.err != nil { return i, nil } cmds = append(cmds, NextField) + case key.Matches(msg, + i.textinput.KeyMap.CharacterForward, + i.textinput.KeyMap.DeleteCharacterForward, + i.textinput.KeyMap.WordForward, + i.textinput.KeyMap.DeleteWordForward, + i.textinput.KeyMap.LineEnd): + + if i.placeholderIsDefault && i.textinput.Value() == "" { + i.textinput.SetValue(i.textinput.Placeholder) + i.textinput.CursorStart() + } } } i.textinput, cmd = i.textinput.Update(msg) cmds = append(cmds, cmd) - i.accessor.Set(i.textinput.Value()) + i.accessor.Set(i.valueOrDefault()) return i, tea.Batch(cmds...) } @@ -381,7 +409,11 @@ func (i *Input) View() string { // 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, // however. - i.textinput.PlaceholderStyle = styles.TextInput.Placeholder + if i.placeholderIsDefault { + i.textinput.PlaceholderStyle = styles.TextInput.DefaultValue + } else { + i.textinput.PlaceholderStyle = styles.TextInput.Placeholder + } i.textinput.PromptStyle = styles.TextInput.Prompt i.textinput.Cursor.Style = styles.TextInput.Cursor i.textinput.Cursor.TextStyle = styles.TextInput.CursorText @@ -506,3 +538,11 @@ func (i *Input) GetKey() string { return i.key } func (i *Input) GetValue() any { return i.accessor.Get() } + +func (i *Input) valueOrDefault() string { + value := i.textinput.Value() + if i.placeholderIsDefault && value == "" { + value = i.textinput.Placeholder + } + return value +} diff --git a/theme.go b/theme.go index 6de20c37..1618ca07 100644 --- a/theme.go +++ b/theme.go @@ -69,11 +69,12 @@ type FieldStyles struct { // TextInputStyles are the styles for text inputs. type TextInputStyles struct { - Cursor lipgloss.Style - CursorText lipgloss.Style - Placeholder lipgloss.Style - Prompt lipgloss.Style - Text lipgloss.Style + Cursor lipgloss.Style + CursorText lipgloss.Style + Placeholder lipgloss.Style + DefaultValue lipgloss.Style + Prompt lipgloss.Style + Text lipgloss.Style } const ( @@ -108,6 +109,7 @@ func ThemeBase() *Theme { t.Focused.FocusedButton = button.Foreground(lipgloss.Color("0")).Background(lipgloss.Color("7")) t.Focused.BlurredButton = button.Foreground(lipgloss.Color("7")).Background(lipgloss.Color("0")) t.Focused.TextInput.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) + t.Focused.TextInput.DefaultValue = lipgloss.NewStyle().Foreground(lipgloss.Color("7")) t.Help = help.New().Styles @@ -158,6 +160,7 @@ func ThemeCharm() *Theme { t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(green) t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"}) + t.Focused.TextInput.DefaultValue = t.Focused.TextInput.DefaultValue.Foreground(lipgloss.AdaptiveColor{Light: "243", Dark: "248"}) t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(fuchsia) t.Blurred = t.Focused @@ -209,6 +212,7 @@ func ThemeDracula() *Theme { t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(yellow) t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(comment) + t.Focused.TextInput.DefaultValue = t.Focused.TextInput.DefaultValue.Foreground(yellow) t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(yellow) t.Blurred = t.Focused @@ -247,6 +251,7 @@ func ThemeBase16() *Theme { t.Focused.TextInput.Cursor.Foreground(lipgloss.Color("5")) t.Focused.TextInput.Placeholder.Foreground(lipgloss.Color("8")) + t.Focused.TextInput.DefaultValue.Foreground(lipgloss.Color("7")) t.Focused.TextInput.Prompt.Foreground(lipgloss.Color("3")) t.Blurred = t.Focused @@ -278,6 +283,7 @@ func ThemeCatppuccin() *Theme { text = lipgloss.AdaptiveColor{Light: light.Text().Hex, Dark: dark.Text().Hex} subtext1 = lipgloss.AdaptiveColor{Light: light.Subtext1().Hex, Dark: dark.Subtext1().Hex} subtext0 = lipgloss.AdaptiveColor{Light: light.Subtext0().Hex, Dark: dark.Subtext0().Hex} + overlay2 = lipgloss.AdaptiveColor{Light: light.Overlay2().Hex, Dark: dark.Overlay2().Hex} overlay1 = lipgloss.AdaptiveColor{Light: light.Overlay1().Hex, Dark: dark.Overlay1().Hex} overlay0 = lipgloss.AdaptiveColor{Light: light.Overlay0().Hex, Dark: dark.Overlay0().Hex} green = lipgloss.AdaptiveColor{Light: light.Green().Hex, Dark: dark.Green().Hex} @@ -309,6 +315,7 @@ func ThemeCatppuccin() *Theme { t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(cursor) t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(overlay0) + t.Focused.TextInput.DefaultValue = t.Focused.TextInput.DefaultValue.Foreground(overlay2) t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(pink) t.Blurred = t.Focused