internal/pkg/term/prompt/prompt.go (254 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package prompt provides functionality to retrieve free-form text, selection,
// and confirmation input from the user via a terminal.
package prompt
import (
"errors"
"os"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/aws/copilot-cli/internal/pkg/term/color"
)
func init() {
survey.ConfirmQuestionTemplate = `{{if not .Answer}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }}{{$lines := split .Help "\n"}}{{range $i, $line := $lines}}
{{- if eq $i 0}} {{ $line }}
{{ else }} {{ $line }}
{{ end }}{{- end }}{{color "reset"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{if not .Answer}} {{ .Config.Icons.Question.Text }}{{else}}{{ .Config.Icons.Question.Text }}{{end}}{{color "reset"}}
{{- color "default"}}{{ .Message }} {{color "reset"}}
{{- if .Answer}}
{{- color "default"}}{{.Answer}}{{color "reset"}}{{"\n"}}
{{- else }}
{{- if and .Help (not .ShowHelp)}}{{color "white"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}
{{- color "default"}}{{if .Default}}(Y/n) {{else}}(y/N) {{end}}{{color "reset"}}
{{- end}}`
survey.SelectQuestionTemplate = `{{if not .Answer}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }}{{$lines := split .Help "\n"}}{{range $i, $line := $lines}}
{{- if eq $i 0}} {{ $line }}
{{ else }} {{ $line }}
{{ end }}{{- end }}{{color "reset"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{if not .ShowAnswer}} {{ .Config.Icons.Question.Text }}{{else}}{{ .Config.Icons.Question.Text }}{{end}}{{color "reset"}}
{{- color "default"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
{{- if .ShowAnswer}}{{color "default"}} {{parseAnswer .Answer}}{{color "reset"}}{{"\n"}}
{{- else}}
{{- " "}}{{- color "white"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
{{- "\n"}}
{{- range $ix, $choice := .PageEntries}}
{{- if eq $ix $.SelectedIndex }}{{color "default+b" }} {{ $.Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
{{- $choice.Value}}
{{- color "reset"}}{{"\n"}}
{{- end}}
{{- end}}`
survey.InputQuestionTemplate = `{{if not .ShowAnswer}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }}{{$lines := split .Help "\n"}}{{range $i, $line := $lines}}
{{- if eq $i 0}} {{ $line }}
{{ else }} {{ $line }}
{{ end }}{{- end }}{{color "reset"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{if not .ShowAnswer}} {{ .Config.Icons.Question.Text }}{{else}}{{ .Config.Icons.Question.Text }}{{end}}{{color "reset"}}
{{- color "default"}}{{ .Message }} {{color "reset"}}
{{- if .ShowAnswer}}
{{- color "default"}}{{.Answer}}{{color "reset"}}{{"\n"}}
{{- else }}
{{- if and .Help (not .ShowHelp)}}{{color "white"}}[{{ print .Config.HelpInput }} for help]{{color "reset"}} {{end}}
{{- if .Default}}{{color "default"}}({{.Default}}) {{color "reset"}}{{end}}
{{- .Answer -}}
{{- end}}`
survey.PasswordQuestionTemplate = `
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }}{{$lines := split .Help "\n"}}{{range $i, $line := $lines}}
{{- if eq $i 0}} {{ $line }}
{{ else }} {{ $line }}
{{ end }}{{- end }}{{color "reset"}}{{end}}
{{- color .Config.Icons.Question.Format }} {{ .Config.Icons.Question.Text }}{{color "reset"}}
{{- color "default"}}{{ .Message }} {{color "reset"}}
{{- if and .Help (not .ShowHelp)}}{{color "white"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}`
survey.MultiSelectQuestionTemplate = `{{if not .Answer}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }}{{$lines := split .Help "\n"}}{{range $i, $line := $lines}}
{{- if eq $i 0}} {{ $line }}
{{ else }} {{ $line }}
{{ end }}{{- end }}{{color "reset"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{if not .ShowAnswer}} {{ .Config.Icons.Question.Text }}{{else}}{{ .Config.Icons.Question.Text }}{{end}}{{color "reset"}}
{{- color "default"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
{{- if .ShowAnswer}}{{color "default"}} {{parseAnswers .Answer}}{{color "reset"}}{{"\n"}}
{{- else }}
{{- " "}}{{- color "white"}}[Use arrows to move, space to select, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
{{- "\n"}}
{{- range $ix, $option := .PageEntries}}
{{- if eq $ix $.SelectedIndex }}{{color "default+b" }} {{ $.Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
{{- if index $.Checked $option.Index }}{{color "default+b" }} {{ $.Config.Icons.MarkedOption.Text }} {{else}}{{color "default" }} {{ $.Config.Icons.UnmarkedOption.Text }} {{end}}
{{- color "reset"}}
{{- " "}}{{$option.Value}}{{"\n"}}
{{- end}}
{{- end}}`
split := func(s string, sep string) []string {
return strings.Split(s, sep)
}
core.TemplateFuncsWithColor["split"] = split
core.TemplateFuncsWithColor["parseAnswer"] = parseValueFromOptionFmt
core.TemplateFuncsWithColor["parseAnswers"] = parseValuesFromOptions
core.TemplateFuncsNoColor["split"] = split
core.TemplateFuncsNoColor["parseAnswer"] = parseValueFromOptionFmt
core.TemplateFuncsNoColor["parseAnswers"] = parseValuesFromOptions
}
// ErrEmptyOptions indicates the input options list was empty.
var ErrEmptyOptions = errors.New("list of provided options is empty")
// Prompt abstracts the survey.Askone function.
type Prompt func(survey.Prompt, interface{}, ...survey.AskOpt) error
// ValidatorFunc defines the function signature for validating inputs.
type ValidatorFunc func(interface{}) error
// New returns a Prompt with default configuration.
func New() Prompt {
return survey.AskOne
}
type prompter interface {
Prompt(config *survey.PromptConfig) (interface{}, error)
Cleanup(*survey.PromptConfig, interface{}) error
Error(*survey.PromptConfig, error) error
WithStdio(terminal.Stdio)
}
type prompt struct {
prompter
FinalMessage string // Text to display after the user selects an answer.
}
// Cleanup does a final render with the user's chosen value.
// This method overrides survey.Select's Cleanup method by assigning the prompt's message to be the final message.
func (p *prompt) Cleanup(config *survey.PromptConfig, val interface{}) error {
if p.FinalMessage == "" {
return p.prompter.Cleanup(config, val) // Delegate to the parent Cleanup.
}
// Update the message of the underlying struct.
switch typedPrompt := p.prompter.(type) {
case *survey.Select:
typedPrompt.Message = p.FinalMessage
case *survey.Input:
typedPrompt.Message = p.FinalMessage
case *passwordPrompt:
typedPrompt.Message = p.FinalMessage
case *survey.Confirm:
typedPrompt.Message = p.FinalMessage
case *survey.MultiSelect:
typedPrompt.Message = p.FinalMessage
}
return p.prompter.Cleanup(config, val)
}
// Get prompts the user for free-form text input.
func (p Prompt) Get(message, help string, validator ValidatorFunc, promptOpts ...PromptConfig) (string, error) {
input := &survey.Input{
Message: message,
}
if help != "" {
input.Help = color.Help(help)
}
prompt := &prompt{
prompter: input,
}
for _, opt := range promptOpts {
opt(prompt)
}
var result string
var err error
if validator == nil {
err = p(prompt, &result, stdio(), icons())
} else {
err = p(prompt, &result, stdio(), validators(validator), icons())
}
return result, err
}
type passwordPrompt struct {
*survey.Password
}
// Cleanup renders a new template that's left-shifted when the user answers the prompt.
func (pp *passwordPrompt) Cleanup(config *survey.PromptConfig, val interface{}) error {
// The user already entered their password, move the cursor one level up to override the prompt.
pp.Password.NewCursor().PreviousLine(1)
// survey.Password unlike other survey structs doesn't have an "Answer" field. Therefore, we can't use a single
// template like other prompts. Instead, when Cleanup is called, we render a new template
// that behaves as if the question is answered.
return pp.Password.Render(`
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }}{{color "reset"}}
{{- color "default"}}{{ .Message }} {{color "reset"}}
`,
survey.PasswordTemplateData{
Password: *pp.Password,
Config: config,
ShowHelp: false,
})
}
// GetSecret prompts the user for sensitive input. Wraps survey.Password
func (p Prompt) GetSecret(message, help string, promptOpts ...PromptConfig) (string, error) {
passwd := &passwordPrompt{
Password: &survey.Password{
Message: message,
},
}
if help != "" {
passwd.Help = color.Help(help)
}
prompt := &prompt{
prompter: passwd,
}
for _, opt := range promptOpts {
opt(prompt)
}
var result string
err := p(prompt, &result, stdio(), icons())
return result, err
}
// Confirm prompts the user with a yes/no option.
func (p Prompt) Confirm(message, help string, promptCfgs ...PromptConfig) (bool, error) {
confirm := &survey.Confirm{
Message: message,
}
if help != "" {
confirm.Help = color.Help(help)
}
prompt := &prompt{
prompter: confirm,
}
for _, cfg := range promptCfgs {
cfg(prompt)
}
var result bool
err := p(prompt, &result, stdio(), icons())
return result, err
}
// PromptConfig is a functional option to configure the prompt.
type PromptConfig func(*prompt)
// WithDefaultInput sets a default message for an input prompt.
func WithDefaultInput(s string) PromptConfig {
return func(p *prompt) {
if get, ok := p.prompter.(*survey.Input); ok {
get.Default = s
}
}
}
// WithFinalMessage sets a final message that replaces the question prompt once the user enters an answer.
func WithFinalMessage(msg string) PromptConfig {
return func(p *prompt) {
p.FinalMessage = color.Emphasize(msg)
}
}
// WithConfirmFinalMessage sets a short final message for to confirm the user's input.
func WithConfirmFinalMessage() PromptConfig {
return func(p *prompt) {
p.FinalMessage = color.Emphasize("Sure?")
}
}
// WithDefaultSelections selects the options to be checked by default for a multiselect prompt.
func WithDefaultSelections(options []string) PromptConfig {
return func(p *prompt) {
if confirm, ok := p.prompter.(*survey.MultiSelect); ok {
confirm.Default = options
}
}
}
// WithTrueDefault sets the default for a confirm prompt to true.
func WithTrueDefault() PromptConfig {
return func(p *prompt) {
if confirm, ok := p.prompter.(*survey.Confirm); ok {
confirm.Default = true
}
}
}
func stdio() survey.AskOpt {
return survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
}
func icons() survey.AskOpt {
return survey.WithIcons(func(icons *survey.IconSet) {
// The question mark "?" icon to denote a prompt will be colored in bold.
icons.Question.Text = ""
icons.Question.Format = "default+b"
// Survey uses https://github.com/mgutz/ansi to set colors which unfortunately doesn't support the "Faint" style.
// We are setting the help text to be fainted in the individual prompt methods instead.
icons.Help.Text = ""
icons.Help.Format = "default"
})
}
// RequireNonEmpty returns an error if v is a zero-value.
func RequireNonEmpty(v interface{}) error {
return survey.Required(v)
}
// RequireMinItems enforces at least min elements to be selected from MultiSelect.
func RequireMinItems(min int) ValidatorFunc {
return (ValidatorFunc)(survey.MinItems(min))
}
func validators(validatorFunc ValidatorFunc) survey.AskOpt {
return survey.WithValidator(survey.ComposeValidators(survey.Required, survey.Validator(validatorFunc)))
}