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
6 changes: 3 additions & 3 deletions cmd/help/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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}}
Expand Down
4 changes: 2 additions & 2 deletions cmd/project/create_samples.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,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

Expand Down
4 changes: 3 additions & 1 deletion docs/reference/experiments.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

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

👌🏻

- **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).
Expand Down
14 changes: 9 additions & 5 deletions internal/experiment/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
3 changes: 2 additions & 1 deletion internal/experiment/experiment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")))
Expand Down
54 changes: 32 additions & 22 deletions internal/iostreams/charm.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,29 @@

package iostreams
Copy link
Member

Choose a reason for hiding this comment

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

question: Long-term, is there a more appropriate filename than charm.go? As a new maintainer I may not connect that two packages - huh and lipgloss - are from the same group charm. I see the newForm constructor uses both, but I wonder if we have an opportunity for a huh.go instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

@mwbrooks I share this feeling. The terms "charm" or "huh" or "lipgloss" are meaningful for package imports but we might want to refactor this into a different packages once experiments conclude:

  • internal/iostreams/charm.go -> internal/iostreams/forms.go

I'm not against huh.go either but it takes me a second to realize it's asking for input the same 😉

Copy link
Member

Choose a reason for hiding this comment

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

forms.go is a good choice as well!

Copy link
Member Author

Choose a reason for hiding this comment

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

@mwbrooks Cool cool! I'll hold off on changing that for another PR but it might be a good step toward the conclusions of huh experiment!


// 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"
"slices"

huh "charm.land/huh/v2"
"github.com/slackapi/slack-cli/internal/experiment"
"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() + " ").
Expand All @@ -35,39 +45,39 @@ 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 err != nil {
return "", err
}
return input, nil
}

// 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 err != nil {
return false, err
}
return choice, nil
}

// 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
Expand All @@ -85,13 +95,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 err != nil {
return SelectPromptResponse{}, err
}
Expand All @@ -101,7 +111,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() + " ").
Expand All @@ -110,21 +120,21 @@ 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 err != nil {
return PasswordPromptResponse{}, err
}
return PasswordPromptResponse{Prompt: true, Value: input}, nil
}

// 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))
Expand All @@ -135,13 +145,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 err != nil {
return []string{}, err
}
Expand Down
Loading
Loading