github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/prompt/prompt.go (about) 1 package prompt 2 3 import ( 4 "runtime" 5 "strings" 6 7 "github.com/ActiveState/cli/internal/analytics/dimensions" 8 "gopkg.in/AlecAivazis/survey.v1" 9 "gopkg.in/AlecAivazis/survey.v1/terminal" 10 11 "github.com/ActiveState/cli/internal/analytics/constants" 12 "github.com/ActiveState/cli/internal/locale" 13 "github.com/ActiveState/cli/internal/logging" 14 "github.com/ActiveState/cli/internal/output" 15 ) 16 17 type EventDispatcher interface { 18 EventWithLabel(category, action string, label string, dim ...*dimensions.Values) 19 } 20 21 // Prompter is the interface used to run our prompt from, useful for mocking in tests 22 type Prompter interface { 23 Input(title, message string, defaultResponse *string, flags ...ValidatorFlag) (string, error) 24 InputAndValidate(title, message string, defaultResponse *string, validator ValidatorFunc, flags ...ValidatorFlag) (string, error) 25 Select(title, message string, choices []string, defaultResponse *string) (string, error) 26 Confirm(title, message string, defaultChoice *bool) (bool, error) 27 InputSecret(title, message string, flags ...ValidatorFlag) (string, error) 28 IsInteractive() bool 29 } 30 31 // ValidatorFunc is a function pass to the Prompter to perform validation 32 // on the users input 33 type ValidatorFunc = survey.Validator 34 35 var _ Prompter = &Prompt{} 36 37 // Prompt is our main prompting struct 38 type Prompt struct { 39 out output.Outputer 40 analytics EventDispatcher 41 isInteractive bool 42 } 43 44 // New creates a new prompter 45 func New(isInteractive bool, an EventDispatcher) Prompter { 46 return &Prompt{output.Get(), an, isInteractive} 47 } 48 49 // IsInteractive checks if the prompts can be interactive or should just return default values 50 func (p *Prompt) IsInteractive() bool { 51 return p.isInteractive 52 } 53 54 // ValidatorFlag represents flags for prompt functions to change their behavior on. 55 type ValidatorFlag int 56 57 const ( 58 // InputRequired requires that the user provide input 59 InputRequired ValidatorFlag = iota 60 // IsAlpha 61 // IsNumber 62 // etc. 63 ) 64 65 // Input prompts the user for input. The user can specify available validation flags to trigger validation of responses 66 func (p *Prompt) Input(title, message string, defaultResponse *string, flags ...ValidatorFlag) (string, error) { 67 return p.InputAndValidate(title, message, defaultResponse, func(val interface{}) error { 68 return nil 69 }, flags...) 70 } 71 72 // interactiveInputError returns the proper input error for a non-interactive prompt. 73 // If the terminal cannot show prompts (e.g. Git Bash on Windows), the error mentions this. 74 // Otherwise, the error simply states the prompt cannot be resolved in non-interactive mode. 75 // The "message" argument is the prompt's user-facing message. 76 func interactiveInputError(message string) error { 77 if runtime.GOOS == "windows" { 78 return locale.NewExternalError("err_non_interactive_mode") 79 } 80 return locale.NewExternalError("err_non_interactive_prompt", message) 81 } 82 83 // InputAndValidate prompts an input field and allows you to specfiy a custom validation function as well as the built in flags 84 func (p *Prompt) InputAndValidate(title, message string, defaultResponse *string, validator ValidatorFunc, flags ...ValidatorFlag) (string, error) { 85 if !p.isInteractive { 86 if defaultResponse != nil { 87 logging.Debug("Selecting default choice %s for Input prompt %s in non-interactive mode", *defaultResponse, title) 88 return *defaultResponse, nil 89 } 90 return "", interactiveInputError(message) 91 } 92 93 var response string 94 flagValidators, err := processValidators(flags) 95 if err != nil { 96 return "", err 97 } 98 if len(flagValidators) != 0 { 99 validator = wrapValidators(append(flagValidators, validator)) 100 } 101 102 if title != "" { 103 p.out.Notice(output.Emphasize(title)) 104 } 105 106 // We handle defaults more clearly than the survey package can 107 if defaultResponse != nil && *defaultResponse != "" { 108 v, err := p.Select("", formatMessage(message, !p.out.Config().Colored), []string{*defaultResponse, locale.Tl("prompt_custom", "Other ..")}, defaultResponse) 109 if err != nil { 110 return "", err 111 } 112 if v == *defaultResponse { 113 return v, nil 114 } 115 message = "" 116 } 117 118 err = survey.AskOne(&Input{&survey.Input{ 119 Message: formatMessage(message, !p.out.Config().Colored), 120 }}, &response, validator) 121 if err != nil { 122 return "", locale.NewInputError(err.Error()) 123 } 124 125 return response, nil 126 } 127 128 // Select prompts the user to select one entry from multiple choices 129 func (p *Prompt) Select(title, message string, choices []string, defaultChoice *string) (string, error) { 130 if !p.isInteractive { 131 if defaultChoice != nil { 132 logging.Debug("Selecting default choice %s for Select prompt %s in non-interactive mode", *defaultChoice, title) 133 return *defaultChoice, nil 134 } 135 return "", interactiveInputError(message) 136 } 137 138 if title != "" { 139 p.out.Notice(output.Emphasize(title)) 140 } 141 142 var defChoice string 143 if defaultChoice != nil { 144 defChoice = *defaultChoice 145 } 146 147 var response string 148 err := survey.AskOne(&Select{&survey.Select{ 149 Message: formatMessage(message, !p.out.Config().Colored), 150 Options: choices, 151 Default: defChoice, 152 FilterFn: func(input string, choices []string) []string { return choices }, // no filter 153 }}, &response, nil) 154 if err != nil { 155 return "", locale.NewInputError(err.Error()) 156 } 157 return response, nil 158 } 159 160 // Confirm prompts user for yes or no response. 161 func (p *Prompt) Confirm(title, message string, defaultChoice *bool) (bool, error) { 162 if !p.isInteractive { 163 if defaultChoice != nil { 164 logging.Debug("Prompt %s confirmed with default choice %v in non-interactive mode", title, defaultChoice) 165 return *defaultChoice, nil 166 } 167 return false, interactiveInputError(message) 168 } 169 if title != "" { 170 p.out.Notice(output.Emphasize(title)) 171 } 172 173 p.analytics.EventWithLabel(constants.CatPrompt, title, "present") 174 175 var defChoice bool 176 if defaultChoice != nil { 177 defChoice = *defaultChoice 178 } 179 180 var resp bool 181 err := survey.AskOne(&Confirm{&survey.Confirm{ 182 Message: formatMessage(strings.TrimSuffix(message, "\n"), !p.out.Config().Colored), 183 Default: defChoice, 184 }}, &resp, nil) 185 if err != nil { 186 if err == terminal.InterruptErr { 187 p.analytics.EventWithLabel(constants.CatPrompt, title, "interrupt") 188 } 189 return false, locale.NewInputError(err.Error()) 190 } 191 p.analytics.EventWithLabel(constants.CatPrompt, title, translateConfirm(resp)) 192 193 return resp, nil 194 } 195 196 func translateConfirm(confirm bool) string { 197 if confirm { 198 return "positive" 199 } 200 return "negative" 201 } 202 203 // InputSecret prompts the user for input and obfuscates the text in stdout. 204 // Will fail if empty. 205 func (p *Prompt) InputSecret(title, message string, flags ...ValidatorFlag) (string, error) { 206 if !p.isInteractive { 207 return "", interactiveInputError(message) 208 } 209 var response string 210 validators, err := processValidators(flags) 211 if err != nil { 212 return "", err 213 } 214 215 if title != "" { 216 p.out.Notice(output.Emphasize(title)) 217 } 218 219 err = survey.AskOne(&Password{&survey.Password{ 220 Message: formatMessage(message, !p.out.Config().Colored), 221 }}, &response, wrapValidators(validators)) 222 if err != nil { 223 return "", locale.NewInputError(err.Error()) 224 } 225 return response, nil 226 } 227 228 // wrapValidators wraps a list of validators in a wrapper function that can be run by the survey package functions 229 func wrapValidators(validators []ValidatorFunc) ValidatorFunc { 230 validator := func(val interface{}) error { 231 for _, v := range validators { 232 if error := v(val); error != nil { 233 return error 234 } 235 } 236 return nil 237 } 238 return validator 239 } 240 241 // This function seems like overkill right now but the assumption is we'll have more than one built in validator 242 func processValidators(flags []ValidatorFlag) ([]ValidatorFunc, error) { 243 var validators []ValidatorFunc 244 var err error 245 for flag := range flags { 246 switch ValidatorFlag(flag) { 247 case InputRequired: 248 validators = append(validators, inputRequired) 249 default: 250 err = locale.NewError("err_prompt_bad_flag") 251 } 252 } 253 return validators, err 254 }