Skip to content

Commit 1059ec2

Browse files
authored
feat(experiment): add charm prompts to iostreams (#350)
1 parent 62770ba commit 1059ec2

4 files changed

Lines changed: 152 additions & 35 deletions

File tree

cmd/auth/login.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import (
1818
"context"
1919
"fmt"
2020

21-
"github.com/slackapi/slack-cli/internal/config"
22-
"github.com/slackapi/slack-cli/internal/experiment"
2321
"github.com/slackapi/slack-cli/internal/iostreams"
2422
authpkg "github.com/slackapi/slack-cli/internal/pkg/auth"
2523
"github.com/slackapi/slack-cli/internal/shared"
@@ -111,7 +109,7 @@ func RunLoginCommand(clients *shared.ClientFactory, cmd *cobra.Command) (types.S
111109
return types.SlackAuth{}, err
112110
}
113111
if selectedAuth.Token != "" {
114-
printAuthSuccess(cmd, clients.Config, clients.IO, credentialsPath, selectedAuth.Token)
112+
printAuthSuccess(cmd, clients.IO, credentialsPath, selectedAuth.Token)
115113
printAuthNextSteps(ctx, clients)
116114
}
117115
return selectedAuth, err
@@ -121,14 +119,14 @@ func RunLoginCommand(clients *shared.ClientFactory, cmd *cobra.Command) (types.S
121119
if err != nil {
122120
return types.SlackAuth{}, err
123121
} else {
124-
printAuthSuccess(cmd, clients.Config, clients.IO, credentialsPath, selectedAuth.Token)
122+
printAuthSuccess(cmd, clients.IO, credentialsPath, selectedAuth.Token)
125123
printAuthNextSteps(ctx, clients)
126124
}
127125

128126
return selectedAuth, nil
129127
}
130128

131-
func printAuthSuccess(cmd *cobra.Command, config *config.Config, IO iostreams.IOStreamer, credentialsPath string, token string) {
129+
func printAuthSuccess(cmd *cobra.Command, IO iostreams.IOStreamer, credentialsPath string, token string) {
132130
ctx := cmd.Context()
133131

134132
var secondaryLog string
@@ -138,13 +136,7 @@ func printAuthSuccess(cmd *cobra.Command, config *config.Config, IO iostreams.IO
138136
secondaryLog = fmt.Sprintf("Service token:\n\n %s\n\nMake sure to copy the token now and save it safely.", token)
139137
}
140138

141-
// The legacy prompt leaves no blank line before the success message, so
142-
// print one here. The Charm-based prompt already handles spacing.
143-
if !config.WithExperimentOn(experiment.Charm) {
144-
IO.PrintInfo(ctx, false, "")
145-
}
146-
147-
IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
139+
IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
148140
Emoji: "key",
149141
Text: "You've successfully authenticated!",
150142
Secondary: []string{secondaryLog},

internal/iostreams/charm.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2022-2026 Salesforce, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package iostreams
16+
17+
// Charm-based prompt implementations using the huh library
18+
// These are used when the "charm" experiment is enabled
19+
20+
import (
21+
"context"
22+
"slices"
23+
24+
"github.com/charmbracelet/huh"
25+
)
26+
27+
// charmInputPrompt prompts for text input using a charm huh form
28+
func charmInputPrompt(_ *IOStreams, _ context.Context, message string, cfg InputPromptConfig) (string, error) {
29+
var input string
30+
field := huh.NewInput().
31+
Title(message).
32+
Value(&input)
33+
if cfg.Required {
34+
field.Validate(huh.ValidateMinLength(1))
35+
}
36+
err := huh.NewForm(huh.NewGroup(field)).Run()
37+
if err != nil {
38+
return "", err
39+
}
40+
return input, nil
41+
}
42+
43+
// charmConfirmPrompt prompts for a yes/no confirmation using a charm huh form
44+
func charmConfirmPrompt(_ *IOStreams, _ context.Context, message string, defaultValue bool) (bool, error) {
45+
var choice = defaultValue
46+
field := huh.NewConfirm().
47+
Title(message).
48+
Value(&choice)
49+
err := huh.NewForm(huh.NewGroup(field)).Run()
50+
if err != nil {
51+
return false, err
52+
}
53+
return choice, nil
54+
}
55+
56+
// charmSelectPrompt prompts the user to select one option using a charm huh form
57+
func charmSelectPrompt(_ *IOStreams, _ context.Context, msg string, options []string, cfg SelectPromptConfig) (SelectPromptResponse, error) {
58+
var selected string
59+
var opts []huh.Option[string]
60+
for _, opt := range options {
61+
key := opt
62+
if cfg.Description != nil {
63+
if desc := cfg.Description(opt, len(opts)); desc != "" {
64+
key = opt + "\n " + desc
65+
}
66+
}
67+
opts = append(opts, huh.NewOption(key, opt))
68+
}
69+
70+
field := huh.NewSelect[string]().
71+
Title(msg).
72+
Options(opts...).
73+
Value(&selected)
74+
75+
if cfg.PageSize > 0 {
76+
field.Height(cfg.PageSize + 2)
77+
}
78+
79+
err := huh.NewForm(huh.NewGroup(field)).Run()
80+
if err != nil {
81+
return SelectPromptResponse{}, err
82+
}
83+
84+
index := slices.Index(options, selected)
85+
return SelectPromptResponse{Prompt: true, Index: index, Option: selected}, nil
86+
}
87+
88+
// charmPasswordPrompt prompts for a password (hidden input) using a charm huh form
89+
func charmPasswordPrompt(_ *IOStreams, _ context.Context, message string, cfg PasswordPromptConfig) (PasswordPromptResponse, error) {
90+
var input string
91+
field := huh.NewInput().
92+
Title(message).
93+
EchoMode(huh.EchoModePassword).
94+
Value(&input)
95+
if cfg.Required {
96+
field.Validate(huh.ValidateMinLength(1))
97+
}
98+
err := huh.NewForm(huh.NewGroup(field)).Run()
99+
if err != nil {
100+
return PasswordPromptResponse{}, err
101+
}
102+
return PasswordPromptResponse{Prompt: true, Value: input}, nil
103+
}
104+
105+
// charmMultiSelectPrompt prompts the user to select multiple options using a charm huh form
106+
func charmMultiSelectPrompt(_ *IOStreams, _ context.Context, message string, options []string) ([]string, error) {
107+
var selected []string
108+
var opts []huh.Option[string]
109+
for _, opt := range options {
110+
opts = append(opts, huh.NewOption(opt, opt))
111+
}
112+
113+
field := huh.NewMultiSelect[string]().
114+
Title(message).
115+
Options(opts...).
116+
Value(&selected)
117+
118+
err := huh.NewForm(huh.NewGroup(field)).Run()
119+
if err != nil {
120+
return []string{}, err
121+
}
122+
return selected, nil
123+
}

internal/iostreams/survey.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"github.com/AlecAivazis/survey/v2"
2929
"github.com/AlecAivazis/survey/v2/terminal"
30+
"github.com/slackapi/slack-cli/internal/experiment"
3031
"github.com/slackapi/slack-cli/internal/slackerror"
3132
"github.com/slackapi/slack-cli/internal/style"
3233
"github.com/spf13/pflag"
@@ -120,6 +121,9 @@ func (cfg ConfirmPromptConfig) IsRequired() bool {
120121
// ConfirmPrompt prompts the user for a "yes" or "no" (true or false) value for
121122
// the message
122123
func (io *IOStreams) ConfirmPrompt(ctx context.Context, message string, defaultValue bool) (bool, error) {
124+
if io.config.WithExperimentOn(experiment.Charm) {
125+
return charmConfirmPrompt(io, ctx, message, defaultValue)
126+
}
123127

124128
// Temporarily swap default template for custom one
125129
defaultConfirmTemplate := survey.ConfirmQuestionTemplate
@@ -191,6 +195,10 @@ func (cfg InputPromptConfig) IsRequired() bool {
191195
// InputPrompt prompts the user for a string value for the message, which can
192196
// optionally be made required
193197
func (io *IOStreams) InputPrompt(ctx context.Context, message string, cfg InputPromptConfig) (string, error) {
198+
if io.config.WithExperimentOn(experiment.Charm) {
199+
return charmInputPrompt(io, ctx, message, cfg)
200+
}
201+
194202
defaultInputTemplate := survey.InputQuestionTemplate
195203
survey.InputQuestionTemplate = InputQuestionTemplate
196204
defer func() {
@@ -263,6 +271,10 @@ func (cfg MultiSelectPromptConfig) IsRequired() bool {
263271
// MultiSelectPrompt prompts the user to select multiple values in a list and
264272
// returns the selected values
265273
func (io *IOStreams) MultiSelectPrompt(ctx context.Context, message string, options []string) ([]string, error) {
274+
if io.config.WithExperimentOn(experiment.Charm) {
275+
return charmMultiSelectPrompt(io, ctx, message, options)
276+
}
277+
266278
defaultMultiSelectTemplate := survey.MultiSelectQuestionTemplate
267279
survey.MultiSelectQuestionTemplate = MultiSelectQuestionTemplate
268280
defer func() {
@@ -340,6 +352,10 @@ func (io *IOStreams) PasswordPrompt(ctx context.Context, message string, cfg Pas
340352
return PasswordPromptResponse{}, errInteractivityFlags(cfg)
341353
}
342354

355+
if io.config.WithExperimentOn(experiment.Charm) {
356+
return charmPasswordPrompt(io, ctx, message, cfg)
357+
}
358+
343359
defaultPasswordTemplate := survey.PasswordQuestionTemplate
344360
if cfg.Template != "" {
345361
survey.PasswordQuestionTemplate = cfg.Template
@@ -454,6 +470,10 @@ func (io *IOStreams) SelectPrompt(ctx context.Context, msg string, options []str
454470
}
455471
}
456472

473+
if io.config.WithExperimentOn(experiment.Charm) {
474+
return charmSelectPrompt(io, ctx, msg, options, cfg)
475+
}
476+
457477
defaultSelectTemplate := survey.SelectQuestionTemplate
458478
if cfg.Template != "" {
459479
survey.SelectQuestionTemplate = cfg.Template

internal/pkg/auth/login.go

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@ import (
2020
"strings"
2121
"time"
2222

23-
"github.com/charmbracelet/huh"
2423
"github.com/opentracing/opentracing-go"
2524
"github.com/slackapi/slack-cli/internal/api"
2625
"github.com/slackapi/slack-cli/internal/auth"
2726
"github.com/slackapi/slack-cli/internal/config"
28-
"github.com/slackapi/slack-cli/internal/experiment"
2927
"github.com/slackapi/slack-cli/internal/iostreams"
3028
"github.com/slackapi/slack-cli/internal/pkg/version"
3129
"github.com/slackapi/slack-cli/internal/shared"
@@ -142,27 +140,11 @@ func createNewAuth(ctx context.Context, apiClient api.APIInterface, authClient a
142140
return types.SlackAuth{}, "", err
143141
}
144142

145-
challengeCode := ""
146-
if !config.WithExperimentOn(experiment.Charm) {
147-
challengeCode, err = io.InputPrompt(ctx, "Enter challenge code", iostreams.InputPromptConfig{
148-
Required: true,
149-
})
150-
if err != nil {
151-
return types.SlackAuth{}, "", err
152-
}
153-
} else {
154-
form := huh.NewForm(
155-
huh.NewGroup(
156-
huh.NewInput().
157-
Title("Enter challenge code").
158-
Validate(huh.ValidateMinLength(1)).
159-
Value(&challengeCode),
160-
),
161-
)
162-
err := form.Run()
163-
if err != nil {
164-
return types.SlackAuth{}, "", err
165-
}
143+
challengeCode, err := io.InputPrompt(ctx, "Enter challenge code", iostreams.InputPromptConfig{
144+
Required: true,
145+
})
146+
if err != nil {
147+
return types.SlackAuth{}, "", err
166148
}
167149

168150
authExchangeRes, err := apiClient.ExchangeAuthTicket(ctx, authTicket, challengeCode, version.Get())

0 commit comments

Comments
 (0)