diff --git a/cmd/project/create_samples.go b/cmd/project/create_samples.go index adea91e2..3fdf4d3e 100644 --- a/cmd/project/create_samples.go +++ b/cmd/project/create_samples.go @@ -21,6 +21,7 @@ import ( "sort" "strings" + "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/iostreams" "github.com/slackapi/slack-cli/internal/pkg/create" "github.com/slackapi/slack-cli/internal/shared" @@ -64,14 +65,26 @@ func promptSampleSelection(ctx context.Context, clients *shared.ClientFactory, s } sortedRepos := sortRepos(filteredRepos) - selectOptions := createSelectOptions(sortedRepos) + selectOptions := make([]string, len(sortedRepos)) + for i, r := range sortedRepos { + if !clients.Config.WithExperimentOn(experiment.Charm) { + selectOptions[i] = fmt.Sprint(i+1, ". ", r.Name) + } else { + selectOptions[i] = r.Name + } + } var selectedTemplate string selection, err = clients.IO.SelectPrompt(ctx, "Select a sample to build upon:", selectOptions, iostreams.SelectPromptConfig{ Description: func(value string, index int) string { - return sortedRepos[index].Description + "\n https://github.com/" + sortedRepos[index].FullName + desc := sortedRepos[index].Description + if !clients.Config.WithExperimentOn(experiment.Charm) { + desc += "\n https://github.com/" + sortedRepos[index].FullName + } + return desc }, Flag: clients.Config.Flags.Lookup("template"), + Help: fmt.Sprintf("Guided tutorials can be found at %s", style.LinkText("https://docs.slack.dev/samples")), PageSize: 4, // Supports standard terminal height (24 rows) Required: true, Template: embedPromptSamplesTmpl, @@ -127,18 +140,3 @@ func sortRepos(sampleRepos []create.GithubRepo) []create.GithubRepo { }) return sortedRepos } - -// createSelectOptions takes in a list of repositories -// and returns an array of strings, each value being -// equal to the repository name (ie, deno-starter-template) -// and prepended with a number for a prompt visual aid -func createSelectOptions(filteredRepos []create.GithubRepo) []string { - // Create a slice of repository names to use as - // the primary item selection in the prompt - selectOptions := make([]string, 0) - for i, f := range filteredRepos { - selectOption := fmt.Sprint(i+1, ". ", f.Name) - selectOptions = append(selectOptions, selectOption) - } - return selectOptions -} diff --git a/cmd/project/create_samples_test.go b/cmd/project/create_samples_test.go index f6eb7cde..10f503c5 100644 --- a/cmd/project/create_samples_test.go +++ b/cmd/project/create_samples_test.go @@ -174,9 +174,3 @@ func TestSamples_SortRepos(t *testing.T) { assert.Equal(t, sortedRepos[3].StargazersCount, 0, "Expected sortedRepos[3].StargazersCount to equal 0") assert.Equal(t, sortedRepos[3].Description, "This is a new sample") } - -func TestSamples_CreateSelectOptions(t *testing.T) { - selectOptions := createSelectOptions(mockGitHubRepos) - assert.Equal(t, len(selectOptions), 4, "Expected selectOptions length to be 4") - assert.Contains(t, selectOptions[0], mockGitHubRepos[0].Name, "Expected selectOptions[0] to contain mockGitHubRepos[0].Name") -} diff --git a/cmd/project/create_template.go b/cmd/project/create_template.go index 0fc58dc0..14998ca5 100644 --- a/cmd/project/create_template.go +++ b/cmd/project/create_template.go @@ -132,9 +132,6 @@ func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory, // Prompt to choose a category selection, err := clients.IO.SelectPrompt(ctx, promptForCategory, titlesForCategory, iostreams.SelectPromptConfig{ - Description: func(value string, index int) string { - return optionsForCategory[index].Description - }, Flag: clients.Config.Flags.Lookup("template"), Required: true, Template: templateForCategory, @@ -168,9 +165,6 @@ func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory, // Prompt to choose a template selection, err := clients.IO.SelectPrompt(ctx, prompt, titles, iostreams.SelectPromptConfig{ - Description: func(value string, index int) string { - return options[index].Description - }, Flag: clients.Config.Flags.Lookup("template"), Required: true, Template: template, diff --git a/internal/iostreams/charm.go b/internal/iostreams/charm.go index a45b9a51..deb0eaec 100644 --- a/internal/iostreams/charm.go +++ b/internal/iostreams/charm.go @@ -72,8 +72,8 @@ func buildSelectForm(msg string, options []string, cfg SelectPromptConfig, selec for _, opt := range options { key := opt if cfg.Description != nil { - if desc := cfg.Description(opt, len(opts)); desc != "" { - key = opt + "\n " + desc + if desc := style.RemoveEmoji(cfg.Description(opt, len(opts))); desc != "" { + key = opt + " - " + desc } } opts = append(opts, huh.NewOption(key, opt)) @@ -81,6 +81,7 @@ func buildSelectForm(msg string, options []string, cfg SelectPromptConfig, selec field := huh.NewSelect[string](). Title(msg). + Description(cfg.Help). Options(opts...). Value(selected) diff --git a/internal/iostreams/survey.go b/internal/iostreams/survey.go index aebd9774..a9fdaa5c 100644 --- a/internal/iostreams/survey.go +++ b/internal/iostreams/survey.go @@ -409,9 +409,10 @@ var selectQuestionTemplate = fmt.Sprintf(` // SelectPromptConfig holds additional config for a survey.Select prompt type SelectPromptConfig struct { - Description func(value string, index int) string // Optional text displayed below each prompt option + Description func(value string, index int) string // Optional text displayed with each prompt option Flag *pflag.Flag // The single flag substitute for this prompt Flags []*pflag.Flag // Otherwise multiple flag substitutes for this prompt + Help string // Optional help text displayed below the select title PageSize int // The number of options displayed before the user needs to scroll Required bool // If a response is required Template string // Custom formatting of the selection prompt diff --git a/internal/style/style.go b/internal/style/style.go index 44fb6e2b..d8366b8a 100644 --- a/internal/style/style.go +++ b/internal/style/style.go @@ -45,6 +45,20 @@ func RemoveANSI(str string) string { return ansiRegex.ReplaceAllString(str, "") } +// RemoveEmoji strips non-ASCII characters (such as emoji) from a string +// and collapses any resulting extra whitespace. +// +// https://en.wikipedia.org/wiki/ASCII#Printable_character_table +func RemoveEmoji(str string) string { + var b strings.Builder + for _, r := range str { + if r <= 127 { + b.WriteRune(r) + } + } + return strings.Join(strings.Fields(b.String()), " ") +} + // ToggleStyles sets styles and formatting values to the active state func ToggleStyles(active bool) { isStyleEnabled = active diff --git a/internal/style/style_test.go b/internal/style/style_test.go index 564c2a7c..695f791e 100644 --- a/internal/style/style_test.go +++ b/internal/style/style_test.go @@ -58,6 +58,36 @@ func TestRemoveANSI(t *testing.T) { } } +func TestRemoveEmoji(t *testing.T) { + tests := map[string]struct { + input string + expected string + }{ + "plain text is unchanged": { + input: "A simple description", + expected: "A simple description", + }, + "emoji flags are removed": { + input: "A translation bot 🇨🇳 🇮🇹 🇹🇭 🇫🇷", + expected: "A translation bot", + }, + "mixed emoji and text": { + input: "Hello 🌍 world 🚀 test", + expected: "Hello world test", + }, + "empty string": { + input: "", + expected: "", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actual := RemoveEmoji(tc.input) + assert.Equal(t, tc.expected, actual) + }) + } +} + func TestToggleStyles(t *testing.T) { defer func() { ToggleStyles(false)