From e19d4cc0fcfc3dce16bf5be3b67dba7fcdd6f9b3 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 31 Mar 2026 15:24:43 -0400 Subject: [PATCH] feat: --no-color overrides huh themes --- internal/iostreams/forms.go | 4 +++- internal/iostreams/forms_test.go | 41 ++++++++++++++++++++++++++++++++ internal/style/theme.go | 30 +++++++++++++++++++++++ internal/style/theme_test.go | 34 ++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/internal/iostreams/forms.go b/internal/iostreams/forms.go index 0ee0b78c..e09b8bef 100644 --- a/internal/iostreams/forms.go +++ b/internal/iostreams/forms.go @@ -32,7 +32,9 @@ import ( // newForm wraps a field in an interactive form with optional Slack theming. func newForm(io *IOStreams, field huh.Field) *huh.Form { form := huh.NewForm(huh.NewGroup(field)) - if io != nil && io.config.WithExperimentOn(experiment.Lipgloss) { + if io != nil && io.config.NoColor { + form = form.WithTheme(style.ThemePlain()) + } else if io != nil && io.config.WithExperimentOn(experiment.Lipgloss) { form = form.WithTheme(style.ThemeSlack()) } else { form = form.WithTheme(style.ThemeSurvey()) diff --git a/internal/iostreams/forms_test.go b/internal/iostreams/forms_test.go index 31df93e2..dd6bb963 100644 --- a/internal/iostreams/forms_test.go +++ b/internal/iostreams/forms_test.go @@ -415,6 +415,47 @@ func TestFormsUseSlackTheme(t *testing.T) { }) } +func TestFormsNoColor(t *testing.T) { + t.Run("forms use plain theme with no-color", func(t *testing.T) { + fsMock := slackdeps.NewFsMock() + osMock := slackdeps.NewOsMock() + osMock.AddDefaultMocks() + cfg := config.NewConfig(fsMock, osMock) + cfg.NoColor = true + io := NewIOStreams(cfg, fsMock, osMock) + + var selected string + f := buildSelectForm(io, "Pick", []string{"A", "B"}, SelectPromptConfig{}, &selected) + f.Update(f.Init()) + + view := f.View() + // Title and option lines should have no ANSI codes + for _, line := range strings.Split(view, "\n")[:3] { + assert.Equal(t, ansi.Strip(line), line, "content line should have no ANSI codes") + } + }) + + t.Run("no-color takes priority over lipgloss experiment", func(t *testing.T) { + fsMock := slackdeps.NewFsMock() + osMock := slackdeps.NewOsMock() + osMock.AddDefaultMocks() + cfg := config.NewConfig(fsMock, osMock) + cfg.NoColor = true + cfg.ExperimentsFlag = []string{"lipgloss"} + cfg.LoadExperiments(context.Background(), func(_ context.Context, _ string, _ ...any) {}) + io := NewIOStreams(cfg, fsMock, osMock) + + var selected string + f := buildSelectForm(io, "Pick", []string{"A", "B"}, SelectPromptConfig{}, &selected) + f.Update(f.Init()) + + view := f.View() + for _, line := range strings.Split(view, "\n")[:3] { + assert.Equal(t, ansi.Strip(line), line, "content line should have no ANSI codes even with lipgloss experiment on") + } + }) +} + func TestFormsUseSurveyTheme(t *testing.T) { t.Run("multi-select uses survey prefix without lipgloss", func(t *testing.T) { var selected []string diff --git a/internal/style/theme.go b/internal/style/theme.go index 02b5430a..f2988df5 100644 --- a/internal/style/theme.go +++ b/internal/style/theme.go @@ -126,6 +126,36 @@ func Chevron() string { return "❱" } +// ThemePlain returns a huh Theme with no colors or formatting. +func ThemePlain() huh.Theme { + return huh.ThemeFunc(func(_ bool) *huh.Styles { + t := huh.ThemeBase(false) + t.Focused.Title = lipgloss.NewStyle() + t.Focused.Description = lipgloss.NewStyle() + t.Focused.ErrorIndicator = lipgloss.NewStyle().SetString(" *") + t.Focused.ErrorMessage = lipgloss.NewStyle() + t.Focused.SelectSelector = lipgloss.NewStyle().SetString("> ") + t.Focused.Option = lipgloss.NewStyle() + t.Focused.MultiSelectSelector = lipgloss.NewStyle().SetString("> ") + t.Focused.SelectedOption = lipgloss.NewStyle() + t.Focused.SelectedPrefix = lipgloss.NewStyle().SetString("[x] ") + t.Focused.UnselectedOption = lipgloss.NewStyle() + t.Focused.UnselectedPrefix = lipgloss.NewStyle().SetString("[ ] ") + t.Focused.FocusedButton = lipgloss.NewStyle().Padding(0, 2).MarginRight(1) + t.Focused.BlurredButton = lipgloss.NewStyle().Padding(0, 2).MarginRight(1) + t.Focused.TextInput.Cursor = lipgloss.NewStyle() + t.Focused.TextInput.Prompt = lipgloss.NewStyle() + t.Focused.TextInput.Placeholder = lipgloss.NewStyle() + t.Focused.TextInput.Text = lipgloss.NewStyle() + t.Focused.Base = lipgloss.NewStyle().PaddingLeft(1).BorderStyle(lipgloss.ThickBorder()).BorderLeft(true) + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.SelectSelector = lipgloss.NewStyle().SetString(" ") + t.Blurred.MultiSelectSelector = lipgloss.NewStyle().SetString(" ") + return t + }) +} + // 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 { diff --git a/internal/style/theme_test.go b/internal/style/theme_test.go index 6650a2f1..1ecf9845 100644 --- a/internal/style/theme_test.go +++ b/internal/style/theme_test.go @@ -116,6 +116,40 @@ func TestThemeSurvey(t *testing.T) { } } +func TestThemePlain(t *testing.T) { + theme := ThemePlain().Theme(false) + tests := map[string]struct { + rendered string + expected string + }{ + "title renders plain text": { + rendered: theme.Focused.Title.Render("x"), + expected: "x", + }, + "error message renders plain text": { + rendered: theme.Focused.ErrorMessage.Render("err"), + expected: "err", + }, + "select selector renders plain >": { + rendered: theme.Focused.SelectSelector.Render(), + expected: "> ", + }, + "selected prefix renders [x]": { + rendered: theme.Focused.SelectedPrefix.Render(), + expected: "[x] ", + }, + "unselected prefix renders [ ]": { + rendered: theme.Focused.UnselectedPrefix.Render(), + expected: "[ ] ", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.rendered) + }) + } +} + func TestChevron(t *testing.T) { tests := map[string]struct { styleEnabled bool