Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion internal/iostreams/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ func newForm(io *IOStreams, field huh.Field) *huh.Form {
form := huh.NewForm(huh.NewGroup(field))
if io != nil && io.config.WithExperimentOn(experiment.Lipgloss) {
form = form.WithTheme(style.ThemeSlack())
} else {
form = form.WithTheme(style.ThemeSurvey())
}
return form
}
Expand Down Expand Up @@ -90,7 +92,7 @@ func buildSelectForm(io *IOStreams, msg string, options []string, cfg SelectProm
key := opt
if cfg.Description != nil {
if desc := style.RemoveEmoji(cfg.Description(opt, len(opts))); desc != "" {
key = opt + " - " + desc
key = style.Bright(opt) + " " + style.Secondary(desc)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thought: If we're changing the "—" separator to a character outside of the ASCII character sets we can perhaps use a new function to fallback to the common dash without styles?

style.Separator

👾 quibble: This might be a change to discuss in a separate PR. IMHO #400 isn't an experimental change but it was related to prompts at the time of these updates and gets confusing...

}
}
opts = append(opts, huh.NewOption(key, opt))
Expand Down
100 changes: 94 additions & 6 deletions internal/iostreams/forms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,57 @@ func TestSelectForm(t *testing.T) {
assert.Contains(t, view, "First letter")
})

t.Run("descriptions use em-dash separator with lipgloss enabled", func(t *testing.T) {
style.ToggleLipgloss(true)
style.ToggleStyles(true)
t.Cleanup(func() {
style.ToggleLipgloss(false)
style.ToggleStyles(false)
})

fsMock := slackdeps.NewFsMock()
osMock := slackdeps.NewOsMock()
osMock.AddDefaultMocks()
cfg := config.NewConfig(fsMock, osMock)
cfg.ExperimentsFlag = []string{"lipgloss"}
cfg.LoadExperiments(context.Background(), func(_ context.Context, _ string, _ ...any) {})
io := NewIOStreams(cfg, fsMock, osMock)

var selected string
options := []string{"Alpha", "Beta"}
selectCfg := SelectPromptConfig{
Description: func(opt string, _ int) string {
if opt == "Alpha" {
return "First letter"
}
return ""
},
}
f := buildSelectForm(io, "Choose", options, selectCfg, &selected)
f.Update(f.Init())

view := ansi.Strip(f.View())
assert.Contains(t, view, " — First letter")
})

t.Run("descriptions use em-dash separator without lipgloss", func(t *testing.T) {
var selected string
options := []string{"Alpha", "Beta"}
selectCfg := SelectPromptConfig{
Description: func(opt string, _ int) string {
if opt == "Alpha" {
return "First letter"
}
return ""
},
}
f := buildSelectForm(nil, "Choose", options, selectCfg, &selected)
f.Update(f.Init())

view := ansi.Strip(f.View())
assert.Contains(t, view, "Alpha — First letter")
})

t.Run("page size sets field height", func(t *testing.T) {
var selected string
options := []string{"A", "B", "C", "D", "E", "F", "G", "H"}
Expand Down Expand Up @@ -283,8 +334,8 @@ func TestMultiSelectForm(t *testing.T) {
m, _ := f.Update(key('x'))
view := ansi.Strip(m.View())

// After toggle, the first item should show as selected (checkmark)
assert.Contains(t, view, "")
// After toggle, the first item should show as selected
assert.Contains(t, view, "[x]")
})

t.Run("submit returns toggled items", func(t *testing.T) {
Expand Down Expand Up @@ -364,14 +415,51 @@ func TestFormsUseSlackTheme(t *testing.T) {
})
}

func TestFormsWithoutLipgloss(t *testing.T) {
t.Run("multi-select uses default prefix without lipgloss", func(t *testing.T) {
func TestFormsUseSurveyTheme(t *testing.T) {
t.Run("multi-select uses survey prefix without lipgloss", func(t *testing.T) {
var selected []string
f := buildMultiSelectForm(nil, "Pick", []string{"A", "B"}, &selected)
f.Update(f.Init())

view := ansi.Strip(f.View())
// Without lipgloss the Slack theme is not applied, so "[ ]" should not appear
assert.NotContains(t, view, "[ ]")
// ThemeSurvey uses "[ ] " as unselected prefix
assert.Contains(t, view, "[ ]")
})

t.Run("multi-select uses [x] for selected prefix", func(t *testing.T) {
var selected []string
f := buildMultiSelectForm(nil, "Pick", []string{"A", "B"}, &selected)
f.Update(f.Init())

// Toggle first item
m, _ := f.Update(key('x'))
view := ansi.Strip(m.View())
assert.Contains(t, view, "[x]")
})

t.Run("select form renders chevron cursor", func(t *testing.T) {
var selected string
f := buildSelectForm(nil, "Pick", []string{"A", "B"}, SelectPromptConfig{}, &selected)
f.Update(f.Init())

view := ansi.Strip(f.View())
assert.Contains(t, view, style.Chevron()+" A")
})

t.Run("all form builders apply ThemeSurvey without lipgloss", func(t *testing.T) {
var s string
var b bool
var ss []string
forms := []*huh.Form{
buildInputForm(nil, "msg", InputPromptConfig{}, &s),
buildConfirmForm(nil, "msg", &b),
buildSelectForm(nil, "msg", []string{"a"}, SelectPromptConfig{}, &s),
buildPasswordForm(nil, "msg", PasswordPromptConfig{}, &s),
buildMultiSelectForm(nil, "msg", []string{"a"}, &ss),
}
for _, f := range forms {
f.Update(f.Init())
assert.NotEmpty(t, f.View())
}
})
}
48 changes: 48 additions & 0 deletions internal/style/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,54 @@ func Chevron() string {
return "❱"
}

// ThemeSurvey returns a huh Theme that matches the legacy survey prompt styling.
// Applied when experiment.Huh is on but experiment.Lipgloss is off.
func ThemeSurvey() huh.Theme {
return huh.ThemeFunc(themeSurvey)
}

// themeSurvey builds huh styles matching the survey library's appearance.
func themeSurvey(isDark bool) *huh.Styles {
t := huh.ThemeBase(isDark)

ansiBlue := lipgloss.ANSIColor(blue)
ansiGray := lipgloss.ANSIColor(gray)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👽 praise: This does solid in foreground text where used!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪐 Before

Image

💫 After

Image

ansiGreen := lipgloss.ANSIColor(green)
ansiRed := lipgloss.ANSIColor(red)

t.Focused.Title = lipgloss.NewStyle().
Foreground(ansiGray).
Bold(true)
Comment on lines +144 to +146
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌲 praise: Nice use of bold formatting once more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌈 Before

Image

🍯 After

Image

t.Focused.ErrorIndicator = lipgloss.NewStyle().
Foreground(ansiRed).
SetString(" *")
t.Focused.ErrorMessage = lipgloss.NewStyle().
Foreground(ansiRed)

// Select styles
t.Focused.SelectSelector = lipgloss.NewStyle().
Foreground(ansiBlue).
Bold(true).
SetString(Chevron() + " ")
t.Focused.SelectedOption = lipgloss.NewStyle().
Foreground(ansiBlue).
Bold(true)
Comment on lines +154 to +160
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌟 praise: These selections have a impressive similarities! The change to help text placement builds confidence in this next update I think!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📸 Before

Image

📸 After

Image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👾 issue(non-blocking): I notice the guided tutorials reference isn't included in the updated help description.

🪬 ramble: This might be a quick change to the create selection but we might want to check for similar custom templates and other deprecated fields being used in other prompts. I'd lean toward a separate PR since this doesn't seem related to the theme itself, if this is something you'd want to check out related?


// Multi-select styles
t.Focused.MultiSelectSelector = lipgloss.NewStyle().
Foreground(ansiBlue).
Bold(true).
SetString(Chevron() + " ")
t.Focused.SelectedPrefix = lipgloss.NewStyle().
Foreground(ansiGreen).
SetString("[x] ")
t.Focused.UnselectedPrefix = lipgloss.NewStyle().
Bold(true).
SetString("[ ] ")

return t
}

// SurveyIcons returns customizations to the appearance of survey prompts.
func SurveyIcons() survey.AskOpt {
if !isStyleEnabled {
Expand Down
40 changes: 40 additions & 0 deletions internal/style/theme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,46 @@ func TestThemeSlack(t *testing.T) {
}
}

func TestThemeSurvey(t *testing.T) {
theme := ThemeSurvey().Theme(false)
tests := map[string]struct {
rendered string
expected []string
unexpected []string
}{
"focused title renders text": {
rendered: theme.Focused.Title.Render("x"),
expected: []string{"x"},
},
"focused error message renders text": {
rendered: theme.Focused.ErrorMessage.Render("err"),
expected: []string{"err"},
},
"focused select selector renders chevron": {
rendered: theme.Focused.SelectSelector.Render(),
expected: []string{Chevron()},
},
"focused multi-select selected prefix has [x]": {
rendered: theme.Focused.SelectedPrefix.Render(),
expected: []string{"[x]"},
},
"focused multi-select unselected prefix has brackets": {
rendered: theme.Focused.UnselectedPrefix.Render(),
expected: []string{"[ ]"},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
for _, exp := range tc.expected {
assert.Contains(t, tc.rendered, exp)
}
for _, unexp := range tc.unexpected {
assert.NotContains(t, tc.rendered, unexp)
}
})
}
}

func TestChevron(t *testing.T) {
tests := map[string]struct {
styleEnabled bool
Expand Down
Loading