-
Notifications
You must be signed in to change notification settings - Fork 417
/
select.go
201 lines (178 loc) · 5.78 KB
/
select.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// 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, ", ")
}