// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package prompt import ( "fmt" "regexp" "strings" "text/tabwriter" "github.com/AlecAivazis/survey/v2" "github.com/aws/copilot-cli/internal/pkg/term/color" ) // Configuration while spacing text with a tabwriter. const ( minCellWidth = 20 // minimum number of characters in a table's cell. tabWidth = 4 // number of characters in between columns. cellPaddingWidth = 2 // number of padding characters added by default to a cell. paddingChar = ' ' // character in between columns. noAdditionalFormatting = 0 ) // SGR(Select Graphic Rendition) is used to colorize outputs in terminals. // Example matches: // '\x1b[2m' (for Faint output) // '\x1b[1;31m' (for bold, red output) const ( sgrStart = "\x1b\\[" // SGR sequences start with "ESC[". sgrEnd = "m" // SGR sequences end with "m". sgrParameter = "[0-9]{1,3}" // SGR parameter values range from 0 to 107. ) var ( sgrParameterGroups = fmt.Sprintf("%s(;%s)*", sgrParameter, sgrParameter) // One or more SGR parameters separated by ";" sgr = fmt.Sprintf("%s(%s)?%s", sgrStart, sgrParameterGroups, sgrEnd) // A complete SGR sequence: \x1b\\[([0-9]{1,2,3}(;[0-9]{1,2,3})*)?m ) var regexpSGR = regexp.MustCompile(sgr) // Option represents a choice with a hint for clarification. type Option struct { Value string // The actual value represented by the option. FriendlyText string // An optional FriendlyText displayed in place of Value. Hint string // An optional Hint displayed alongside the Value or FriendlyText. } // String implements the fmt.Stringer interface. func (o Option) String() string { text := o.Value if o.FriendlyText != "" { text = o.FriendlyText } if o.Hint == "" { return fmt.Sprintf("%s\t", text) } return fmt.Sprintf("%s\t%s", text, color.Faint.Sprintf("(%s)", o.Hint)) } // SelectOption prompts the user to select one option from options and returns the Value of the option. func (p Prompt) SelectOption(message, help string, opts []Option, promptCfgs ...PromptConfig) (value string, err error) { if len(opts) <= 0 { return "", ErrEmptyOptions } prettified, err := prettifyOptions(opts) if err != nil { return "", err } result, err := p.SelectOne(message, help, prettified.choices, promptCfgs...) if err != nil { return "", err } return prettified.choice2Value[result], nil } // MultiSelectOptions prompts the user to select multiple options and returns the value field from the options. func (p Prompt) MultiSelectOptions(message, help string, opts []Option, promptCfgs ...PromptConfig) ([]string, error) { if len(opts) <= 0 { return nil, ErrEmptyOptions } prettified, err := prettifyOptions(opts) if err != nil { return nil, err } choices, err := p.MultiSelect(message, help, prettified.choices, nil, promptCfgs...) if err != nil { return nil, err } values := make([]string, len(choices)) for i, choice := range choices { values[i] = prettified.choice2Value[choice] } return values, nil } // SelectOne prompts the user with a list of options to choose from with the arrow keys. func (p Prompt) SelectOne(message, help string, options []string, promptCfgs ...PromptConfig) (string, error) { if len(options) <= 0 { return "", ErrEmptyOptions } sel := &survey.Select{ Message: message, Options: options, Default: options[0], } if help != "" { sel.Help = color.Help(help) } prompt := &prompt{ prompter: sel, } for _, cfg := range promptCfgs { cfg(prompt) } var result string err := p(prompt, &result, stdio(), icons()) return result, err } // MultiSelect prompts the user with a list of options to choose from with the arrow keys and enter key. func (p Prompt) MultiSelect(message, help string, options []string, validator ValidatorFunc, promptCfgs ...PromptConfig) ([]string, error) { if len(options) <= 0 { // returns nil slice if error return nil, ErrEmptyOptions } multiselect := &survey.MultiSelect{ Message: message, Options: options, Default: options[0], } if help != "" { multiselect.Help = color.Help(help) } prompt := &prompt{ prompter: multiselect, } for _, cfg := range promptCfgs { cfg(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 prettyOptions struct { choices []string choice2Value map[string]string } func prettifyOptions(opts []Option) (prettyOptions, error) { buf := new(strings.Builder) tw := tabwriter.NewWriter(buf, minCellWidth, tabWidth, cellPaddingWidth, paddingChar, noAdditionalFormatting) var lines []string for _, opt := range opts { lines = append(lines, opt.String()) } if _, err := tw.Write([]byte(strings.Join(lines, "\n"))); err != nil { return prettyOptions{}, fmt.Errorf("render options: %v", err) } if err := tw.Flush(); err != nil { return prettyOptions{}, fmt.Errorf("flush tabwriter options: %v", err) } choices := strings.Split(buf.String(), "\n") choice2Value := make(map[string]string) for idx, choice := range choices { choice2Value[choice] = opts[idx].Value } return prettyOptions{ choices: choices, choice2Value: choice2Value, }, nil } func parseValueFromOptionFmt(formatted string) string { if idx := strings.Index(formatted, "("); idx != -1 { s := regexpSGR.ReplaceAllString(formatted[:idx], "") return strings.TrimSpace(s) } return strings.TrimSpace(formatted) } func parseValuesFromOptions(formatted string) string { options := strings.Split(formatted, ", ") out := make([]string, len(options)) for i, option := range options { out[i] = parseValueFromOptionFmt(option) } return strings.Join(out, ", ") }