Skip to content
Merged
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
15 changes: 8 additions & 7 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
{
"permissions": {
"allow": [
"Bash(go build:*)",
"Bash(go mod tidy:*)",
"Bash(go test:*)",
"Bash(gofmt:*)",
"Bash(gh issue view:*)",
"Bash(gh label list:*)",
"Bash(gh pr checks:*)",
Expand All @@ -15,20 +11,25 @@
"Bash(gh pr view:*)",
"Bash(gh search code:*)",
"Bash(git diff:*)",
"Bash(git fetch:*)",
"Bash(git grep:*)",
"Bash(git log:*)",
"Bash(git status:*)",
"Bash(go build:*)",
"Bash(go mod graph:*)",
"Bash(go mod tidy:*)",
"Bash(go mod tidy:*)",
"Bash(go test:*)",
"Bash(gofmt:*)",
"Bash(grep:*)",
"Bash(ls:*)",
"Bash(make build:*)",
"Bash(make build-ci:*)",
"Bash(make build:*)",
"Bash(make lint:*)",
"Bash(make test:*)",
"Bash(tree:*)",
"WebFetch(domain:github.com)",
"WebFetch(domain:docs.slack.dev)"
"WebFetch(domain:docs.slack.dev)",
"WebFetch(domain:github.com)"
]
},
"enabledPlugins": {
Expand Down
37 changes: 19 additions & 18 deletions internal/iostreams/charm.go β†’ internal/iostreams/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

package iostreams

// Charm-based prompt implementations using the huh library.
// These are used when the "huh" experiment is enabled.
// Interactive form-based prompt implementations with Charm's Huh package.
//
// Reference: https://github.com/charmbracelet/huh?tab=readme-ov-file#huh

import (
"context"
Expand All @@ -28,7 +29,7 @@ import (
"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.
// 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) {
Expand All @@ -37,7 +38,7 @@ func newForm(io *IOStreams, field huh.Field) *huh.Form {
return form
}

// buildInputForm constructs a huh form for text input prompts.
// buildInputForm constructs an interactive form for text input prompts.
func buildInputForm(io *IOStreams, message string, cfg InputPromptConfig, input *string) *huh.Form {
field := huh.NewInput().
Title(message).
Expand All @@ -50,8 +51,8 @@ func buildInputForm(io *IOStreams, message string, cfg InputPromptConfig, input
return newForm(io, field)
}

// charmInputPrompt prompts for text input using a charm huh form
func charmInputPrompt(io *IOStreams, _ context.Context, message string, cfg InputPromptConfig) (string, error) {
// inputForm interactively prompts for text input.
func inputForm(io *IOStreams, _ context.Context, message string, cfg InputPromptConfig) (string, error) {
var input string
err := buildInputForm(io, message, cfg, &input).Run()
if errors.Is(err, huh.ErrUserAborted) {
Expand All @@ -62,16 +63,16 @@ func charmInputPrompt(io *IOStreams, _ context.Context, message string, cfg Inpu
return input, nil
}

// buildConfirmForm constructs a huh form for yes/no confirmation prompts.
// buildConfirmForm constructs an interactive form for yes/no confirmation prompts.
func buildConfirmForm(io *IOStreams, message string, choice *bool) *huh.Form {
field := huh.NewConfirm().
Title(message).
Value(choice)
return newForm(io, field)
}

// charmConfirmPrompt prompts for a yes/no confirmation using a charm huh form
func charmConfirmPrompt(io *IOStreams, _ context.Context, message string, defaultValue bool) (bool, error) {
// confirmForm interactively prompts for a yes/no confirmation.
func confirmForm(io *IOStreams, _ context.Context, message string, defaultValue bool) (bool, error) {
var choice = defaultValue
err := buildConfirmForm(io, message, &choice).Run()
if errors.Is(err, huh.ErrUserAborted) {
Expand All @@ -82,7 +83,7 @@ func charmConfirmPrompt(io *IOStreams, _ context.Context, message string, defaul
return choice, nil
}

// buildSelectForm constructs a huh form for single-selection prompts.
// buildSelectForm constructs an interactive form for single-selection prompts.
func buildSelectForm(io *IOStreams, msg string, options []string, cfg SelectPromptConfig, selected *string) *huh.Form {
var opts []huh.Option[string]
for _, opt := range options {
Expand All @@ -104,8 +105,8 @@ func buildSelectForm(io *IOStreams, msg string, options []string, cfg SelectProm
return newForm(io, field)
}

// charmSelectPrompt prompts the user to select one option using a charm huh form
func charmSelectPrompt(io *IOStreams, _ context.Context, msg string, options []string, cfg SelectPromptConfig) (SelectPromptResponse, error) {
// selectForm interactively prompts the user to select one option.
func selectForm(io *IOStreams, _ context.Context, msg string, options []string, cfg SelectPromptConfig) (SelectPromptResponse, error) {
var selected string
err := buildSelectForm(io, msg, options, cfg, &selected).Run()
if errors.Is(err, huh.ErrUserAborted) {
Expand All @@ -118,7 +119,7 @@ func charmSelectPrompt(io *IOStreams, _ context.Context, msg string, options []s
return SelectPromptResponse{Prompt: true, Index: index, Option: selected}, nil
}

// buildPasswordForm constructs a huh form for password (hidden input) prompts.
// buildPasswordForm constructs an interactive form for password (hidden input) prompts.
func buildPasswordForm(io *IOStreams, message string, cfg PasswordPromptConfig, input *string) *huh.Form {
field := huh.NewInput().
Title(message).
Expand All @@ -131,8 +132,8 @@ func buildPasswordForm(io *IOStreams, message string, cfg PasswordPromptConfig,
return newForm(io, field)
}

// charmPasswordPrompt prompts for a password (hidden input) using a charm huh form
func charmPasswordPrompt(io *IOStreams, _ context.Context, message string, cfg PasswordPromptConfig) (PasswordPromptResponse, error) {
// passwordForm interactively prompts for a password with hidden input.
func passwordForm(io *IOStreams, _ context.Context, message string, cfg PasswordPromptConfig) (PasswordPromptResponse, error) {
var input string
err := buildPasswordForm(io, message, cfg, &input).Run()
if errors.Is(err, huh.ErrUserAborted) {
Expand All @@ -143,7 +144,7 @@ func charmPasswordPrompt(io *IOStreams, _ context.Context, message string, cfg P
return PasswordPromptResponse{Prompt: true, Value: input}, nil
}

// buildMultiSelectForm constructs a huh form for multiple-selection prompts.
// buildMultiSelectForm constructs an interactive form for multiple-selection prompts.
func buildMultiSelectForm(io *IOStreams, message string, options []string, selected *[]string) *huh.Form {
var opts []huh.Option[string]
for _, opt := range options {
Expand All @@ -158,8 +159,8 @@ func buildMultiSelectForm(io *IOStreams, message string, options []string, selec
return newForm(io, field)
}

// charmMultiSelectPrompt prompts the user to select multiple options using a charm huh form
func charmMultiSelectPrompt(io *IOStreams, _ context.Context, message string, options []string) ([]string, error) {
// multiSelectForm interactively prompts the user to select multiple options.
func multiSelectForm(io *IOStreams, _ context.Context, message string, options []string) ([]string, error) {
var selected []string
err := buildMultiSelectForm(io, message, options, &selected).Run()
if errors.Is(err, huh.ErrUserAborted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func key(r rune) tea.KeyPressMsg {
return tea.KeyPressMsg{Code: r, Text: string(r)}
}

func TestCharmInput(t *testing.T) {
func TestInputForm(t *testing.T) {
t.Run("renders the title", func(t *testing.T) {
var input string
f := buildInputForm(nil, "Enter your name", InputPromptConfig{}, &input)
Expand Down Expand Up @@ -93,7 +93,7 @@ func TestCharmInput(t *testing.T) {
})
}

func TestCharmConfirm(t *testing.T) {
func TestConfirmForm(t *testing.T) {
t.Run("renders the title and buttons", func(t *testing.T) {
choice := false
f := buildConfirmForm(nil, "Are you sure?", &choice)
Expand Down Expand Up @@ -128,7 +128,7 @@ func TestCharmConfirm(t *testing.T) {
})
}

func TestCharmSelect(t *testing.T) {
func TestSelectForm(t *testing.T) {
t.Run("renders the title and options", func(t *testing.T) {
var selected string
options := []string{"Foo", "Bar", "Baz"}
Expand Down Expand Up @@ -210,7 +210,7 @@ func TestCharmSelect(t *testing.T) {
})
}

func TestCharmPassword(t *testing.T) {
func TestPasswordForm(t *testing.T) {
t.Run("renders the title", func(t *testing.T) {
var input string
f := buildPasswordForm(nil, "Enter password", PasswordPromptConfig{}, &input)
Expand Down Expand Up @@ -259,7 +259,7 @@ func TestCharmPassword(t *testing.T) {
})
}

func TestCharmMultiSelect(t *testing.T) {
func TestMultiSelectForm(t *testing.T) {
t.Run("renders the title and options", func(t *testing.T) {
var selected []string
options := []string{"Foo", "Bar", "Baz"}
Expand Down Expand Up @@ -305,7 +305,7 @@ func TestCharmMultiSelect(t *testing.T) {
})
}

func TestCharmFormsUseSlackTheme(t *testing.T) {
func TestFormsUseSlackTheme(t *testing.T) {
fsMock := slackdeps.NewFsMock()
osMock := slackdeps.NewOsMock()
osMock.AddDefaultMocks()
Expand Down Expand Up @@ -364,7 +364,7 @@ func TestCharmFormsUseSlackTheme(t *testing.T) {
})
}

func TestCharmFormsWithoutLipgloss(t *testing.T) {
func TestFormsWithoutLipgloss(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)
Expand Down
12 changes: 6 additions & 6 deletions internal/iostreams/prompts.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

package iostreams

// Prompt type definitions shared between survey and charm implementations.
// Prompts handle flag values and interactive forms for gathering user input.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

πŸ”­ note: This has the callout of prompts I'd like to make! I'm hoping we can improve how flags are parsed in prompts with the thinking that prompts have option for a flag value or form interactive input.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This sounds like a healthy improvement opportunity for how we handle forms and flags. The first iteration has treated us well, but I agree that there is room for improvement!


import (
"context"
Expand Down Expand Up @@ -194,7 +194,7 @@ func errInteractivityFlags(cfg PromptConfig) error {
// the message
func (io *IOStreams) ConfirmPrompt(ctx context.Context, message string, defaultValue bool) (bool, error) {
if io.config.WithExperimentOn(experiment.Huh) {
return charmConfirmPrompt(io, ctx, message, defaultValue)
return confirmForm(io, ctx, message, defaultValue)
}
return surveyConfirmPrompt(io, ctx, message, defaultValue)
}
Expand All @@ -203,7 +203,7 @@ func (io *IOStreams) ConfirmPrompt(ctx context.Context, message string, defaultV
// optionally be made required
func (io *IOStreams) InputPrompt(ctx context.Context, message string, cfg InputPromptConfig) (string, error) {
if io.config.WithExperimentOn(experiment.Huh) {
return charmInputPrompt(io, ctx, message, cfg)
return inputForm(io, ctx, message, cfg)
}
return surveyInputPrompt(io, ctx, message, cfg)
}
Expand All @@ -212,7 +212,7 @@ func (io *IOStreams) InputPrompt(ctx context.Context, message string, cfg InputP
// returns the selected values
func (io *IOStreams) MultiSelectPrompt(ctx context.Context, message string, options []string) ([]string, error) {
if io.config.WithExperimentOn(experiment.Huh) {
return charmMultiSelectPrompt(io, ctx, message, options)
return multiSelectForm(io, ctx, message, options)
}
return surveyMultiSelectPrompt(io, ctx, message, options)
}
Expand All @@ -230,7 +230,7 @@ func (io *IOStreams) PasswordPrompt(ctx context.Context, message string, cfg Pas
}

if io.config.WithExperimentOn(experiment.Huh) {
return charmPasswordPrompt(io, ctx, message, cfg)
return passwordForm(io, ctx, message, cfg)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

praise: These read very well!

}
return surveyPasswordPrompt(io, ctx, message, cfg)
}
Expand Down Expand Up @@ -258,7 +258,7 @@ func (io *IOStreams) SelectPrompt(ctx context.Context, msg string, options []str
}

if io.config.WithExperimentOn(experiment.Huh) {
return charmSelectPrompt(io, ctx, msg, options, cfg)
return selectForm(io, ctx, msg, options, cfg)
}
return surveySelectPrompt(io, ctx, msg, options, cfg)
}
Loading