diff --git a/cmd/help/help.go b/cmd/help/help.go index 9e14eba2..53e1965e 100644 --- a/cmd/help/help.go +++ b/cmd/help/help.go @@ -35,7 +35,7 @@ func HelpFunc( if help, _ := clients.Config.Flags.GetBool("help"); help { clients.Config.LoadExperiments(ctx, clients.IO.PrintDebug) } - style.ToggleCharm(clients.Config.WithExperimentOn(experiment.Charm)) + style.ToggleLipgloss(clients.Config.WithExperimentOn(experiment.Lipgloss)) experiments := []string{} for _, exp := range clients.Config.GetExperiments() { if experiment.Includes(exp) { @@ -68,7 +68,7 @@ func PrintHelpTemplate(cmd *cobra.Command, data style.TemplateData) { } cmd.Long = cmdLongF.String() tmpl := legacyHelpTemplate - if style.IsCharmEnabled() { + if style.IsLipglossEnabled() { tmpl = charmHelpTemplate } err = style.PrintTemplate(cmd.OutOrStdout(), tmpl, templateInfo{cmd, data}) @@ -121,7 +121,7 @@ const charmHelpTemplate string = `{{.Long | ToDescription}} // ════════════════════════════════════════════════════════════════════════════════ // DEPRECATED: Legacy help template — aurora styling // -// Delete this entire block when the charm experiment is permanently enabled. +// Delete this entire block when the lipgloss experiment is permanently enabled. // ════════════════════════════════════════════════════════════════════════════════ const legacyHelpTemplate string = `{{.Long}} diff --git a/cmd/project/create_samples.go b/cmd/project/create_samples.go index 3fdf4d3e..5ffc1f2a 100644 --- a/cmd/project/create_samples.go +++ b/cmd/project/create_samples.go @@ -67,7 +67,7 @@ func promptSampleSelection(ctx context.Context, clients *shared.ClientFactory, s sortedRepos := sortRepos(filteredRepos) selectOptions := make([]string, len(sortedRepos)) for i, r := range sortedRepos { - if !clients.Config.WithExperimentOn(experiment.Charm) { + if !clients.Config.WithExperimentOn(experiment.Huh) { selectOptions[i] = fmt.Sprint(i+1, ". ", r.Name) } else { selectOptions[i] = r.Name @@ -78,7 +78,7 @@ func promptSampleSelection(ctx context.Context, clients *shared.ClientFactory, s selection, err = clients.IO.SelectPrompt(ctx, "Select a sample to build upon:", selectOptions, iostreams.SelectPromptConfig{ Description: func(value string, index int) string { desc := sortedRepos[index].Description - if !clients.Config.WithExperimentOn(experiment.Charm) { + if !clients.Config.WithExperimentOn(experiment.Huh) { desc += "\n https://github.com/" + sortedRepos[index].FullName } return desc diff --git a/cmd/root.go b/cmd/root.go index ed22e2cf..80e8e059 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -295,7 +295,7 @@ func InitConfig(ctx context.Context, clients *shared.ClientFactory, rootCmd *cob // Init configurations clients.Config.LoadExperiments(ctx, clients.IO.PrintDebug) - style.ToggleCharm(clients.Config.WithExperimentOn(experiment.Charm)) + style.ToggleLipgloss(clients.Config.WithExperimentOn(experiment.Lipgloss)) // TODO(slackcontext) Consolidate storing CLI version to slackcontext clients.Config.Version = clients.CLIVersion diff --git a/docs/reference/experiments.md b/docs/reference/experiments.md index ca54c51b..75c1d538 100644 --- a/docs/reference/experiments.md +++ b/docs/reference/experiments.md @@ -6,13 +6,15 @@ The Slack CLI has an experiment (`-e`) flag behind which we put features current The following is a list of currently available experiments. We'll remove experiments from this page if we decide they are no longer needed or once they are released, in which case we'll make an announcement about the feature's general availability in the [developer changelog](https://docs.slack.dev/changelog). -- `charm`: shows beautiful prompts ([PR#348](https://github.com/slackapi/slack-cli/pull/348)). +- `huh`: shows beautiful prompts. +- `lipgloss`: shows pretty styles. - `sandboxes`: enables users who have joined the Slack Developer Program to manage their sandboxes ([PR#379](https://github.com/slackapi/slack-cli/pull/379)). ## Experiments changelog Below is a list of updates related to experiments. +- **March 2026**: Split the `charm` experiment into more beautiful `huh` prompts and prettier `lipgloss` styles for ongoing change. - **March 2026**: Concluded the `bolt` and `bolt-install` experiments with full Bolt framework support now enabled by default in the Slack CLI. All Bolt project features including remote manifest management are now standard functionality. See the announcement [here](https://slack.dev/slackcli-supports-bolt-apps/). - **February 2026**: Added the `charm` experiment. - **December 2025**: Concluded the `read-only-collaborators` experiment with full support introduced to the Slack CLI. See the changelog announcement [here](https://docs.slack.dev/changelog/2025/12/04/slack-cli). diff --git a/internal/experiment/experiment.go b/internal/experiment/experiment.go index 5e4e3cc4..e46d8a4b 100644 --- a/internal/experiment/experiment.go +++ b/internal/experiment/experiment.go @@ -30,20 +30,24 @@ type Experiment string // e.g. --experiment=first-toggle,second-toggle const ( - // Charm experiment enables beautiful prompts. - Charm Experiment = "charm" + // Huh experiment shows beautiful prompts. + Huh Experiment = "huh" - // Sandboxes experiment lets users who have joined the Slack Developer Program use the CLI to manage their sandboxes. - Sandboxes Experiment = "sandboxes" + // Lipgloss experiment shows pretty styles. + Lipgloss Experiment = "lipgloss" // Placeholder experiment is a placeholder for testing and does nothing... or does it? Placeholder Experiment = "placeholder" + + // Sandboxes experiment lets users who have joined the Slack Developer Program use the CLI to manage their sandboxes. + Sandboxes Experiment = "sandboxes" ) // AllExperiments is a list of all available experiments that can be enabled // Please also add here 👇 var AllExperiments = []Experiment{ - Charm, + Huh, + Lipgloss, Placeholder, Sandboxes, } diff --git a/internal/experiment/experiment_test.go b/internal/experiment/experiment_test.go index b138a921..aac45d53 100644 --- a/internal/experiment/experiment_test.go +++ b/internal/experiment/experiment_test.go @@ -25,7 +25,8 @@ func Test_Includes(t *testing.T) { require.Equal(t, true, Includes(Experiment(Placeholder))) // Test expected experiments - require.Equal(t, true, Includes(Experiment("charm"))) + require.Equal(t, true, Includes(Experiment("huh"))) + require.Equal(t, true, Includes(Experiment("lipgloss"))) // Test invalid experiment require.Equal(t, false, Includes(Experiment("should-fail"))) diff --git a/internal/iostreams/charm.go b/internal/iostreams/charm.go index fe9c39b2..1a74ecf1 100644 --- a/internal/iostreams/charm.go +++ b/internal/iostreams/charm.go @@ -14,8 +14,8 @@ package iostreams -// Charm-based prompt implementations using the huh library -// These are used when the "charm" experiment is enabled +// Charm-based prompt implementations using the huh library. +// These are used when the "huh" experiment is enabled. import ( "context" @@ -23,12 +23,22 @@ import ( "slices" huh "charm.land/huh/v2" + "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/slackerror" "github.com/slackapi/slack-cli/internal/style" ) +// newForm wraps a field in a huh form, applying the Slack theme when the lipgloss experiment is enabled. +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()) + } + return form +} + // buildInputForm constructs a huh form for text input prompts. -func buildInputForm(message string, cfg InputPromptConfig, input *string) *huh.Form { +func buildInputForm(io *IOStreams, message string, cfg InputPromptConfig, input *string) *huh.Form { field := huh.NewInput(). Title(message). Prompt(style.Chevron() + " "). @@ -37,13 +47,13 @@ func buildInputForm(message string, cfg InputPromptConfig, input *string) *huh.F if cfg.Required { field.Validate(huh.ValidateMinLength(1)) } - return huh.NewForm(huh.NewGroup(field)).WithTheme(style.ThemeSlack()) + return newForm(io, field) } // charmInputPrompt prompts for text input using a charm huh form -func charmInputPrompt(_ *IOStreams, _ context.Context, message string, cfg InputPromptConfig) (string, error) { +func charmInputPrompt(io *IOStreams, _ context.Context, message string, cfg InputPromptConfig) (string, error) { var input string - err := buildInputForm(message, cfg, &input).Run() + err := buildInputForm(io, message, cfg, &input).Run() if errors.Is(err, huh.ErrUserAborted) { return "", slackerror.New(slackerror.ErrProcessInterrupted) } else if err != nil { @@ -53,17 +63,17 @@ func charmInputPrompt(_ *IOStreams, _ context.Context, message string, cfg Input } // buildConfirmForm constructs a huh form for yes/no confirmation prompts. -func buildConfirmForm(message string, choice *bool) *huh.Form { +func buildConfirmForm(io *IOStreams, message string, choice *bool) *huh.Form { field := huh.NewConfirm(). Title(message). Value(choice) - return huh.NewForm(huh.NewGroup(field)).WithTheme(style.ThemeSlack()) + return newForm(io, field) } // charmConfirmPrompt prompts for a yes/no confirmation using a charm huh form -func charmConfirmPrompt(_ *IOStreams, _ context.Context, message string, defaultValue bool) (bool, error) { +func charmConfirmPrompt(io *IOStreams, _ context.Context, message string, defaultValue bool) (bool, error) { var choice = defaultValue - err := buildConfirmForm(message, &choice).Run() + err := buildConfirmForm(io, message, &choice).Run() if errors.Is(err, huh.ErrUserAborted) { return false, slackerror.New(slackerror.ErrProcessInterrupted) } else if err != nil { @@ -73,7 +83,7 @@ func charmConfirmPrompt(_ *IOStreams, _ context.Context, message string, default } // buildSelectForm constructs a huh form for single-selection prompts. -func buildSelectForm(msg string, options []string, cfg SelectPromptConfig, selected *string) *huh.Form { +func buildSelectForm(io *IOStreams, msg string, options []string, cfg SelectPromptConfig, selected *string) *huh.Form { var opts []huh.Option[string] for _, opt := range options { key := opt @@ -91,13 +101,13 @@ func buildSelectForm(msg string, options []string, cfg SelectPromptConfig, selec Options(opts...). Value(selected) - return huh.NewForm(huh.NewGroup(field)).WithTheme(style.ThemeSlack()) + return newForm(io, field) } // charmSelectPrompt prompts the user to select one option using a charm huh form -func charmSelectPrompt(_ *IOStreams, _ context.Context, msg string, options []string, cfg SelectPromptConfig) (SelectPromptResponse, error) { +func charmSelectPrompt(io *IOStreams, _ context.Context, msg string, options []string, cfg SelectPromptConfig) (SelectPromptResponse, error) { var selected string - err := buildSelectForm(msg, options, cfg, &selected).Run() + err := buildSelectForm(io, msg, options, cfg, &selected).Run() if errors.Is(err, huh.ErrUserAborted) { return SelectPromptResponse{}, slackerror.New(slackerror.ErrProcessInterrupted) } else if err != nil { @@ -109,7 +119,7 @@ func charmSelectPrompt(_ *IOStreams, _ context.Context, msg string, options []st } // buildPasswordForm constructs a huh form for password (hidden input) prompts. -func buildPasswordForm(message string, cfg PasswordPromptConfig, input *string) *huh.Form { +func buildPasswordForm(io *IOStreams, message string, cfg PasswordPromptConfig, input *string) *huh.Form { field := huh.NewInput(). Title(message). Prompt(style.Chevron() + " "). @@ -118,13 +128,13 @@ func buildPasswordForm(message string, cfg PasswordPromptConfig, input *string) if cfg.Required { field.Validate(huh.ValidateMinLength(1)) } - return huh.NewForm(huh.NewGroup(field)).WithTheme(style.ThemeSlack()) + return newForm(io, field) } // charmPasswordPrompt prompts for a password (hidden input) using a charm huh form -func charmPasswordPrompt(_ *IOStreams, _ context.Context, message string, cfg PasswordPromptConfig) (PasswordPromptResponse, error) { +func charmPasswordPrompt(io *IOStreams, _ context.Context, message string, cfg PasswordPromptConfig) (PasswordPromptResponse, error) { var input string - err := buildPasswordForm(message, cfg, &input).Run() + err := buildPasswordForm(io, message, cfg, &input).Run() if errors.Is(err, huh.ErrUserAborted) { return PasswordPromptResponse{}, slackerror.New(slackerror.ErrProcessInterrupted) } else if err != nil { @@ -134,7 +144,7 @@ func charmPasswordPrompt(_ *IOStreams, _ context.Context, message string, cfg Pa } // buildMultiSelectForm constructs a huh form for multiple-selection prompts. -func buildMultiSelectForm(message string, options []string, selected *[]string) *huh.Form { +func buildMultiSelectForm(io *IOStreams, message string, options []string, selected *[]string) *huh.Form { var opts []huh.Option[string] for _, opt := range options { opts = append(opts, huh.NewOption(opt, opt)) @@ -145,13 +155,13 @@ func buildMultiSelectForm(message string, options []string, selected *[]string) Options(opts...). Value(selected) - return huh.NewForm(huh.NewGroup(field)).WithTheme(style.ThemeSlack()) + return newForm(io, field) } // charmMultiSelectPrompt prompts the user to select multiple options using a charm huh form -func charmMultiSelectPrompt(_ *IOStreams, _ context.Context, message string, options []string) ([]string, error) { +func charmMultiSelectPrompt(io *IOStreams, _ context.Context, message string, options []string) ([]string, error) { var selected []string - err := buildMultiSelectForm(message, options, &selected).Run() + err := buildMultiSelectForm(io, message, options, &selected).Run() if errors.Is(err, huh.ErrUserAborted) { return []string{}, slackerror.New(slackerror.ErrProcessInterrupted) } else if err != nil { diff --git a/internal/iostreams/charm_test.go b/internal/iostreams/charm_test.go index 867ce2c1..a54fb9f7 100644 --- a/internal/iostreams/charm_test.go +++ b/internal/iostreams/charm_test.go @@ -15,12 +15,15 @@ package iostreams import ( + "context" "strings" "testing" tea "charm.land/bubbletea/v2" huh "charm.land/huh/v2" "github.com/charmbracelet/x/ansi" + "github.com/slackapi/slack-cli/internal/config" + "github.com/slackapi/slack-cli/internal/slackdeps" "github.com/slackapi/slack-cli/internal/style" "github.com/stretchr/testify/assert" ) @@ -33,7 +36,7 @@ func key(r rune) tea.KeyPressMsg { func TestCharmInput(t *testing.T) { t.Run("renders the title", func(t *testing.T) { var input string - f := buildInputForm("Enter your name", InputPromptConfig{}, &input) + f := buildInputForm(nil, "Enter your name", InputPromptConfig{}, &input) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -42,7 +45,7 @@ func TestCharmInput(t *testing.T) { t.Run("renders the chevron prompt", func(t *testing.T) { var input string - f := buildInputForm("Name?", InputPromptConfig{}, &input) + f := buildInputForm(nil, "Name?", InputPromptConfig{}, &input) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -51,7 +54,7 @@ func TestCharmInput(t *testing.T) { t.Run("accepts typed input", func(t *testing.T) { var input string - f := buildInputForm("Name?", InputPromptConfig{}, &input) + f := buildInputForm(nil, "Name?", InputPromptConfig{}, &input) f.Update(f.Init()) f.Update(key('H')) @@ -64,7 +67,7 @@ func TestCharmInput(t *testing.T) { t.Run("renders placeholder text", func(t *testing.T) { var input string - f := buildInputForm("Name?", InputPromptConfig{Placeholder: "my-cool-app"}, &input) + f := buildInputForm(nil, "Name?", InputPromptConfig{Placeholder: "my-cool-app"}, &input) f.Update(f.Init()) // In huh v2, the cursor overlays the first placeholder character, @@ -77,7 +80,7 @@ func TestCharmInput(t *testing.T) { t.Run("stores typed value", func(t *testing.T) { var input string - f := buildInputForm("Name?", InputPromptConfig{}, &input) + f := buildInputForm(nil, "Name?", InputPromptConfig{}, &input) f.Update(f.Init()) f.Update(key('t')) @@ -93,7 +96,7 @@ func TestCharmInput(t *testing.T) { func TestCharmConfirm(t *testing.T) { t.Run("renders the title and buttons", func(t *testing.T) { choice := false - f := buildConfirmForm("Are you sure?", &choice) + f := buildConfirmForm(nil, "Are you sure?", &choice) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -104,7 +107,7 @@ func TestCharmConfirm(t *testing.T) { t.Run("default value is respected", func(t *testing.T) { choice := true - f := buildConfirmForm("Continue?", &choice) + f := buildConfirmForm(nil, "Continue?", &choice) f.Update(f.Init()) assert.True(t, choice) @@ -112,7 +115,7 @@ func TestCharmConfirm(t *testing.T) { t.Run("toggle changes value", func(t *testing.T) { choice := false - f := buildConfirmForm("Continue?", &choice) + f := buildConfirmForm(nil, "Continue?", &choice) f.Update(f.Init()) // Toggle to Yes @@ -129,7 +132,7 @@ func TestCharmSelect(t *testing.T) { t.Run("renders the title and options", func(t *testing.T) { var selected string options := []string{"Foo", "Bar", "Baz"} - f := buildSelectForm("Pick one", options, SelectPromptConfig{}, &selected) + f := buildSelectForm(nil, "Pick one", options, SelectPromptConfig{}, &selected) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -142,7 +145,7 @@ func TestCharmSelect(t *testing.T) { t.Run("cursor starts on first option", func(t *testing.T) { var selected string options := []string{"Foo", "Bar"} - f := buildSelectForm("Pick one", options, SelectPromptConfig{}, &selected) + f := buildSelectForm(nil, "Pick one", options, SelectPromptConfig{}, &selected) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -152,7 +155,7 @@ func TestCharmSelect(t *testing.T) { t.Run("cursor navigation moves selection", func(t *testing.T) { var selected string options := []string{"Foo", "Bar", "Baz"} - f := buildSelectForm("Pick one", options, SelectPromptConfig{}, &selected) + f := buildSelectForm(nil, "Pick one", options, SelectPromptConfig{}, &selected) f.Update(f.Init()) m, _ := f.Update(tea.KeyPressMsg{Code: tea.KeyDown}) @@ -164,7 +167,7 @@ func TestCharmSelect(t *testing.T) { t.Run("submit selects the hovered option", func(t *testing.T) { var selected string options := []string{"Foo", "Bar", "Baz"} - f := buildSelectForm("Pick one", options, SelectPromptConfig{}, &selected) + f := buildSelectForm(nil, "Pick one", options, SelectPromptConfig{}, &selected) f.Update(f.Init()) // Move down to Bar, then submit @@ -185,7 +188,7 @@ func TestCharmSelect(t *testing.T) { return "" }, } - f := buildSelectForm("Choose", options, cfg, &selected) + f := buildSelectForm(nil, "Choose", options, cfg, &selected) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -196,7 +199,7 @@ func TestCharmSelect(t *testing.T) { var selected string options := []string{"A", "B", "C", "D", "E", "F", "G", "H"} cfg := SelectPromptConfig{PageSize: 3} - f := buildSelectForm("Pick", options, cfg, &selected) + f := buildSelectForm(nil, "Pick", options, cfg, &selected) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -210,7 +213,7 @@ func TestCharmSelect(t *testing.T) { func TestCharmPassword(t *testing.T) { t.Run("renders the title", func(t *testing.T) { var input string - f := buildPasswordForm("Enter password", PasswordPromptConfig{}, &input) + f := buildPasswordForm(nil, "Enter password", PasswordPromptConfig{}, &input) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -219,7 +222,7 @@ func TestCharmPassword(t *testing.T) { t.Run("renders the chevron prompt", func(t *testing.T) { var input string - f := buildPasswordForm("Enter password", PasswordPromptConfig{}, &input) + f := buildPasswordForm(nil, "Enter password", PasswordPromptConfig{}, &input) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -228,7 +231,7 @@ func TestCharmPassword(t *testing.T) { t.Run("typed characters are masked in view", func(t *testing.T) { var input string - f := buildPasswordForm("Password", PasswordPromptConfig{}, &input) + f := buildPasswordForm(nil, "Password", PasswordPromptConfig{}, &input) f.Update(f.Init()) f.Update(key('s')) @@ -244,7 +247,7 @@ func TestCharmPassword(t *testing.T) { t.Run("stores typed value despite masking", func(t *testing.T) { var input string - f := buildPasswordForm("Password", PasswordPromptConfig{}, &input) + f := buildPasswordForm(nil, "Password", PasswordPromptConfig{}, &input) f.Update(f.Init()) f.Update(key('a')) @@ -260,7 +263,7 @@ func TestCharmMultiSelect(t *testing.T) { t.Run("renders the title and options", func(t *testing.T) { var selected []string options := []string{"Foo", "Bar", "Baz"} - f := buildMultiSelectForm("Pick many", options, &selected) + f := buildMultiSelectForm(nil, "Pick many", options, &selected) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -273,7 +276,7 @@ func TestCharmMultiSelect(t *testing.T) { t.Run("toggle selection with x key", func(t *testing.T) { var selected []string options := []string{"Foo", "Bar"} - f := buildMultiSelectForm("Pick", options, &selected) + f := buildMultiSelectForm(nil, "Pick", options, &selected) f.Update(f.Init()) // Toggle first item @@ -287,7 +290,7 @@ func TestCharmMultiSelect(t *testing.T) { t.Run("submit returns toggled items", func(t *testing.T) { var selected []string options := []string{"Foo", "Bar", "Baz"} - f := buildMultiSelectForm("Pick", options, &selected) + f := buildMultiSelectForm(nil, "Pick", options, &selected) f.Update(f.Init()) // Toggle Foo (first item) @@ -303,9 +306,17 @@ func TestCharmMultiSelect(t *testing.T) { } func TestCharmFormsUseSlackTheme(t *testing.T) { + 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) + t.Run("input form uses Slack theme", func(t *testing.T) { var input string - f := buildInputForm("Test", InputPromptConfig{}, &input) + f := buildInputForm(io, "Test", InputPromptConfig{}, &input) f.Update(f.Init()) // The Slack theme applies a thick left border with bright aubergine color. @@ -317,7 +328,7 @@ func TestCharmFormsUseSlackTheme(t *testing.T) { t.Run("select form renders themed cursor", func(t *testing.T) { var selected string - f := buildSelectForm("Pick", []string{"A", "B"}, SelectPromptConfig{}, &selected) + f := buildSelectForm(io, "Pick", []string{"A", "B"}, SelectPromptConfig{}, &selected) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -326,7 +337,7 @@ func TestCharmFormsUseSlackTheme(t *testing.T) { t.Run("multi-select form renders themed prefixes", func(t *testing.T) { var selected []string - f := buildMultiSelectForm("Pick", []string{"A", "B"}, &selected) + f := buildMultiSelectForm(io, "Pick", []string{"A", "B"}, &selected) f.Update(f.Init()) view := ansi.Strip(f.View()) @@ -340,11 +351,11 @@ func TestCharmFormsUseSlackTheme(t *testing.T) { var b bool var ss []string forms := []*huh.Form{ - buildInputForm("msg", InputPromptConfig{}, &s), - buildConfirmForm("msg", &b), - buildSelectForm("msg", []string{"a"}, SelectPromptConfig{}, &s), - buildPasswordForm("msg", PasswordPromptConfig{}, &s), - buildMultiSelectForm("msg", []string{"a"}, &ss), + buildInputForm(io, "msg", InputPromptConfig{}, &s), + buildConfirmForm(io, "msg", &b), + buildSelectForm(io, "msg", []string{"a"}, SelectPromptConfig{}, &s), + buildPasswordForm(io, "msg", PasswordPromptConfig{}, &s), + buildMultiSelectForm(io, "msg", []string{"a"}, &ss), } for _, f := range forms { f.Update(f.Init()) @@ -352,3 +363,15 @@ func TestCharmFormsUseSlackTheme(t *testing.T) { } }) } + +func TestCharmFormsWithoutLipgloss(t *testing.T) { + t.Run("multi-select uses default 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, "[ ]") + }) +} diff --git a/internal/iostreams/printer.go b/internal/iostreams/printer.go index 48446b58..dc48395f 100644 --- a/internal/iostreams/printer.go +++ b/internal/iostreams/printer.go @@ -89,7 +89,7 @@ func (io *IOStreams) PrintInfo(ctx context.Context, shouldTrace bool, format str span, _ := opentracing.StartSpanFromContext(ctx, "printInfo", opentracing.Tag{Key: "printInfo", Value: message}) defer span.Finish() } - if style.IsCharmEnabled() { + if style.IsLipglossEnabled() { io.Stdout.Println(message) } else { io.Stdout.Println(style.Styler().Reset(message)) diff --git a/internal/iostreams/prompts.go b/internal/iostreams/prompts.go index 2547947d..9c5a2f0a 100644 --- a/internal/iostreams/prompts.go +++ b/internal/iostreams/prompts.go @@ -193,7 +193,7 @@ func errInteractivityFlags(cfg PromptConfig) error { // ConfirmPrompt prompts the user for a "yes" or "no" (true or false) value for // the message func (io *IOStreams) ConfirmPrompt(ctx context.Context, message string, defaultValue bool) (bool, error) { - if io.config.WithExperimentOn(experiment.Charm) { + if io.config.WithExperimentOn(experiment.Huh) { return charmConfirmPrompt(io, ctx, message, defaultValue) } return surveyConfirmPrompt(io, ctx, message, defaultValue) @@ -202,7 +202,7 @@ func (io *IOStreams) ConfirmPrompt(ctx context.Context, message string, defaultV // InputPrompt prompts the user for a string value for the message, which can // optionally be made required func (io *IOStreams) InputPrompt(ctx context.Context, message string, cfg InputPromptConfig) (string, error) { - if io.config.WithExperimentOn(experiment.Charm) { + if io.config.WithExperimentOn(experiment.Huh) { return charmInputPrompt(io, ctx, message, cfg) } return surveyInputPrompt(io, ctx, message, cfg) @@ -211,7 +211,7 @@ func (io *IOStreams) InputPrompt(ctx context.Context, message string, cfg InputP // MultiSelectPrompt prompts the user to select multiple values in a list and // returns the selected values func (io *IOStreams) MultiSelectPrompt(ctx context.Context, message string, options []string) ([]string, error) { - if io.config.WithExperimentOn(experiment.Charm) { + if io.config.WithExperimentOn(experiment.Huh) { return charmMultiSelectPrompt(io, ctx, message, options) } return surveyMultiSelectPrompt(io, ctx, message, options) @@ -229,7 +229,7 @@ func (io *IOStreams) PasswordPrompt(ctx context.Context, message string, cfg Pas return PasswordPromptResponse{}, errInteractivityFlags(cfg) } - if io.config.WithExperimentOn(experiment.Charm) { + if io.config.WithExperimentOn(experiment.Huh) { return charmPasswordPrompt(io, ctx, message, cfg) } return surveyPasswordPrompt(io, ctx, message, cfg) @@ -257,7 +257,7 @@ func (io *IOStreams) SelectPrompt(ctx context.Context, msg string, options []str } } - if io.config.WithExperimentOn(experiment.Charm) { + if io.config.WithExperimentOn(experiment.Huh) { return charmSelectPrompt(io, ctx, msg, options, cfg) } return surveySelectPrompt(io, ctx, msg, options, cfg) diff --git a/internal/style/format.go b/internal/style/format.go index 6c941d1e..5c246a96 100644 --- a/internal/style/format.go +++ b/internal/style/format.go @@ -231,7 +231,7 @@ func ExampleTemplatef(template string) string { for _, cmd := range lines { example := "" if cmd != "" { - if isCharmEnabled { + if isLipglossEnabled { example = fmt.Sprintf(" %s", styleExampleLine(cmd)) } else { styled := reComment.ReplaceAllStringFunc(cmd, Secondary) diff --git a/internal/style/format_test.go b/internal/style/format_test.go index 794ba521..f45891a4 100644 --- a/internal/style/format_test.go +++ b/internal/style/format_test.go @@ -244,10 +244,10 @@ func TestStyleFlags(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { ToggleStyles(tc.charmEnabled) - ToggleCharm(tc.charmEnabled) + ToggleLipgloss(tc.charmEnabled) defer func() { ToggleStyles(false) - ToggleCharm(false) + ToggleLipgloss(false) }() actual := StyleFlags(tc.input) assert.Equal(t, tc.expectedFunc(), actual) @@ -358,10 +358,10 @@ func Test_ExampleTemplatef(t *testing.T) { func Test_ExampleTemplatef_Charm(t *testing.T) { defer func() { ToggleStyles(false) - ToggleCharm(false) + ToggleLipgloss(false) }() ToggleStyles(true) - ToggleCharm(true) + ToggleLipgloss(true) template := []string{ "# Create a new project from a selected template", diff --git a/internal/style/style.go b/internal/style/style.go index d8366b8a..1eab0ba1 100644 --- a/internal/style/style.go +++ b/internal/style/style.go @@ -33,8 +33,8 @@ var isColorShown = isStyleEnabled // isLinkShown specifies if hyperlinks should be formatted var isLinkShown = isStyleEnabled -// isCharmEnabled specifies if lipgloss/charm styling should be used instead of aurora -var isCharmEnabled = false +// isLipglossEnabled specifies if lipgloss styling should be used instead of aurora +var isLipglossEnabled = false // RemoveANSI uses regex to strip ANSI colour codes // @@ -66,14 +66,14 @@ func ToggleStyles(active bool) { isLinkShown = active } -// ToggleCharm enables lipgloss-based styling when set to true -func ToggleCharm(active bool) { - isCharmEnabled = active +// ToggleLipgloss enables lipgloss-based styling when set to true +func ToggleLipgloss(active bool) { + isLipglossEnabled = active } -// IsCharmEnabled reports whether lipgloss/charm styling is active -func IsCharmEnabled() bool { - return isCharmEnabled +// IsLipglossEnabled reports whether lipgloss styling is active +func IsLipglossEnabled() bool { + return isLipglossEnabled } // render applies a lipgloss style to text, returning plain text when colors are disabled. @@ -127,7 +127,7 @@ Color styles // Secondary dims the displayed text func Secondary(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacySecondary(text) } return render(lipgloss.NewStyle().Foreground(slackDescriptionText), text) @@ -135,7 +135,7 @@ func Secondary(text string) string { // CommandText emphasizes command text func CommandText(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyCommandText(text) } return render(lipgloss.NewStyle().Foreground(slackBlue).Bold(true), text) @@ -143,42 +143,42 @@ func CommandText(text string) string { // LinkText underlines and formats the provided path func LinkText(path string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyLinkText(path) } return render(lipgloss.NewStyle().Foreground(slackPool).Underline(true), path) } func Selector(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacySelector(text) } return render(lipgloss.NewStyle().Foreground(slackGreen).Bold(true), text) } func Error(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyError(text) } return render(lipgloss.NewStyle().Foreground(slackRed).Bold(true), text) } func Warning(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyWarning(text) } return render(lipgloss.NewStyle().Foreground(slackYellow).Bold(true), text) } func Header(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyHeader(text) } return render(lipgloss.NewStyle().Foreground(slackAubergine).Bold(true), strings.ToUpper(text)) } func Input(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyInput(text) } return render(lipgloss.NewStyle().Foreground(slackBlue), text) @@ -186,7 +186,7 @@ func Input(text string) string { // Green applies green color to text without bold func Green(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyGreen(text) } return render(lipgloss.NewStyle().Foreground(slackGreen), text) @@ -194,7 +194,7 @@ func Green(text string) string { // Red applies red color to text without bold func Red(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyRed(text) } return render(lipgloss.NewStyle().Foreground(slackRedDark), text) @@ -202,7 +202,7 @@ func Red(text string) string { // Yellow applies yellow color to text without bold func Yellow(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyYellow(text) } return render(lipgloss.NewStyle().Foreground(slackYellow), text) @@ -210,7 +210,7 @@ func Yellow(text string) string { // Gray applies a subdued gray color to text func Gray(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyGray(text) } return render(lipgloss.NewStyle().Foreground(slackLegalGray), text) @@ -222,7 +222,7 @@ Text styles // Bright is a strong bold version of the text func Bright(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyBright(text) } return render(lipgloss.NewStyle().Bold(true), text) @@ -230,7 +230,7 @@ func Bright(text string) string { // Bold brightly emboldens the provided text func Bold(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyBold(text) } return render(lipgloss.NewStyle().Foreground(slackOptionText).Bold(true), text) @@ -238,7 +238,7 @@ func Bold(text string) string { // Darken adds a bold gray shade to text func Darken(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyDarken(text) } return render(lipgloss.NewStyle().Foreground(slackPlaceholderText).Bold(true), text) @@ -249,7 +249,7 @@ func Faint(text string) string { if !isColorShown { return text } - if !isCharmEnabled { + if !isLipglossEnabled { return legacyFaint(text) } return lipgloss.NewStyle().Faint(true).Render(text) @@ -257,7 +257,7 @@ func Faint(text string) string { // Highlight adds emphasis to text func Highlight(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyHighlight(text) } return render(lipgloss.NewStyle().Bold(true), text) @@ -265,7 +265,7 @@ func Highlight(text string) string { // Underline underscores the given text func Underline(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return legacyUnderline(text) } return render(lipgloss.NewStyle().Underline(true), text) @@ -285,7 +285,7 @@ func Pluralize(singular string, plural string, count int) string { // DEPRECATED: Legacy aurora styling // // Delete this entire section, the aurora import, and the ANSI color constants -// when the charm experiment is permanently enabled. +// when the lipgloss experiment is permanently enabled. // ════════════════════════════════════════════════════════════════════════════════ const ( diff --git a/internal/style/style_test.go b/internal/style/style_test.go index 695f791e..73f3db71 100644 --- a/internal/style/style_test.go +++ b/internal/style/style_test.go @@ -148,18 +148,18 @@ func TestPluralize(t *testing.T) { } } -func TestToggleCharm(t *testing.T) { +func TestToggleLipgloss(t *testing.T) { tests := map[string]struct { initial bool toggle bool expected bool }{ - "enables charm styling": { + "enables lipgloss styling": { initial: false, toggle: true, expected: true, }, - "disables charm styling": { + "disables lipgloss styling": { initial: true, toggle: false, expected: false, @@ -167,10 +167,10 @@ func TestToggleCharm(t *testing.T) { } for name, tc := range tests { t.Run(name, func(t *testing.T) { - isCharmEnabled = tc.initial - defer func() { isCharmEnabled = false }() - ToggleCharm(tc.toggle) - assert.Equal(t, tc.expected, isCharmEnabled) + isLipglossEnabled = tc.initial + defer func() { isLipglossEnabled = false }() + ToggleLipgloss(tc.toggle) + assert.Equal(t, tc.expected, isLipglossEnabled) }) } } @@ -181,28 +181,28 @@ func testStyleFunc(t *testing.T, name string, fn func(string) string) { t.Helper() defer func() { ToggleStyles(false) - ToggleCharm(false) + ToggleLipgloss(false) }() input := "hello" t.Run(name+" returns plain text when colors are off", func(t *testing.T) { ToggleStyles(false) - ToggleCharm(false) + ToggleLipgloss(false) result := fn(input) assert.Equal(t, input, RemoveANSI(result)) }) t.Run(name+" returns styled text with legacy aurora", func(t *testing.T) { ToggleStyles(true) - ToggleCharm(false) + ToggleLipgloss(false) result := fn(input) assert.Contains(t, RemoveANSI(result), input) }) t.Run(name+" returns styled text with charm lipgloss", func(t *testing.T) { ToggleStyles(true) - ToggleCharm(true) + ToggleLipgloss(true) result := fn(input) assert.Contains(t, RemoveANSI(result), input) }) @@ -233,19 +233,19 @@ func TestTextStyleFunctions(t *testing.T) { func TestHeader(t *testing.T) { defer func() { ToggleStyles(false) - ToggleCharm(false) + ToggleLipgloss(false) }() t.Run("uppercases text", func(t *testing.T) { ToggleStyles(true) - ToggleCharm(true) + ToggleLipgloss(true) result := Header("commands") assert.Contains(t, RemoveANSI(result), "COMMANDS") }) t.Run("uppercases text with legacy", func(t *testing.T) { ToggleStyles(true) - ToggleCharm(false) + ToggleLipgloss(false) result := Header("commands") assert.Contains(t, RemoveANSI(result), "COMMANDS") }) @@ -254,7 +254,7 @@ func TestHeader(t *testing.T) { func TestFaint(t *testing.T) { defer func() { ToggleStyles(false) - ToggleCharm(false) + ToggleLipgloss(false) }() t.Run("returns plain text when colors are off", func(t *testing.T) { @@ -265,7 +265,7 @@ func TestFaint(t *testing.T) { t.Run("returns styled text with legacy", func(t *testing.T) { ToggleStyles(true) - ToggleCharm(false) + ToggleLipgloss(false) result := Faint("hello") assert.Contains(t, result, "hello") assert.NotEqual(t, "hello", result) @@ -273,7 +273,7 @@ func TestFaint(t *testing.T) { t.Run("returns styled text with charm", func(t *testing.T) { ToggleStyles(true) - ToggleCharm(true) + ToggleLipgloss(true) result := Faint("hello") assert.Contains(t, RemoveANSI(result), "hello") }) diff --git a/internal/style/template.go b/internal/style/template.go index 80714180..8e57b56c 100644 --- a/internal/style/template.go +++ b/internal/style/template.go @@ -57,7 +57,7 @@ func getTemplateFuncs() template.FuncMap { if len(experiments) == 0 { return ExampleTemplatef("None") } - if isCharmEnabled { + if isLipglossEnabled { styled := make([]string, len(experiments)) for i, exp := range experiments { styled[i] = " " + Red(exp) @@ -108,45 +108,45 @@ func getTemplateFuncs() template.FuncMap { "trimTrailingWhitespaces": func(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) }, - // Charm-only template functions — return plain text when charm is off + // Lipgloss-only template functions — return plain text when lipgloss is off "ToDescription": func(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return text } return Secondary(text) }, "ToSecondary": func(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return text } return Secondary(text) }, "ToPrompt": func(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return text } return Yellow(text) }, "ToGroupName": func(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return text } return Warning(text) }, "ToAliasParent": func(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return text } return Red(text) }, "ToDarken": func(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return text } return Darken(text) }, "ToFlags": func(text string) string { - if !isCharmEnabled { + if !isLipglossEnabled { return text } return StyleFlags(text)