forked from manifoldco/promptui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
prompt.go
345 lines (281 loc) · 8.55 KB
/
prompt.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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
package promptui
import (
"fmt"
"io"
"strings"
"text/template"
"github.com/chzyer/readline"
"github.com/trzsz/promptui/screenbuf"
)
// Prompt represents a single line text field input with options for validation and input masks.
type Prompt struct {
// Label is the value displayed on the command line prompt.
//
// The value for Label can be a simple string or a struct that will need to be accessed by dot notation
// inside the templates. For example, `{{ .Name }}` will display the name property of a struct.
Label interface{}
// Default is the initial value for the prompt. This value will be displayed next to the prompt's label
// and the user will be able to view or change it depending on the options.
Default string
// AllowEdit lets the user edit the default value. If false, any key press
// other than <Enter> automatically clears the default value.
AllowEdit bool
// Validate is an optional function that fill be used against the entered value in the prompt to validate it.
Validate ValidateFunc
// Mask is an optional rune that sets which character to display instead of the entered characters. This
// allows hiding private information like passwords.
Mask rune
// HideEntered sets whether to hide the text after the user has pressed enter.
HideEntered bool
// Templates can be used to customize the prompt output. If nil is passed, the
// default templates are used. See the PromptTemplates docs for more info.
Templates *PromptTemplates
// IsConfirm makes the prompt ask for a yes or no ([Y/N]) question rather than request an input. When set,
// most properties related to input will be ignored.
IsConfirm bool
// IsVimMode enables vi-like movements (hjkl) and editing.
IsVimMode bool
// the Pointer defines how to render the cursor.
Pointer Pointer
Stdin io.ReadCloser
Stdout io.WriteCloser
}
// PromptTemplates allow a prompt to be customized following stdlib
// text/template syntax. Custom state, colors and background color are available for use inside
// the templates and are documented inside the Variable section of the docs.
//
// # Examples
//
// text/templates use a special notation to display programmable content. Using the double bracket notation,
// the value can be printed with specific helper functions. For example
//
// This displays the value given to the template as pure, unstylized text.
//
// '{{ . }}'
//
// This displays the value colored in cyan
//
// '{{ . | cyan }}'
//
// This displays the value colored in red with a cyan background-color
//
// '{{ . | red | cyan }}'
//
// See the doc of text/template for more info: https://golang.org/pkg/text/template/
type PromptTemplates struct {
// Prompt is a text/template for the prompt label displayed on the left side of the prompt.
Prompt string
// Prompt is a text/template for the prompt label when IsConfirm is set as true.
Confirm string
// Valid is a text/template for the prompt label when the value entered is valid.
Valid string
// Invalid is a text/template for the prompt label when the value entered is invalid.
Invalid string
// Success is a text/template for the prompt label when the user has pressed entered and the value has been
// deemed valid by the validation function. The label will keep using this template even when the prompt ends
// inside the console.
Success string
// Prompt is a text/template for the prompt label when the value is invalid due to an error triggered by
// the prompt's validation function.
ValidationError string
// FuncMap is a map of helper functions that can be used inside of templates according to the text/template
// documentation.
//
// By default, FuncMap contains the color functions used to color the text in templates. If FuncMap
// is overridden, the colors functions must be added in the override from promptui.FuncMap to work.
FuncMap template.FuncMap
prompt *template.Template
valid *template.Template
invalid *template.Template
validation *template.Template
success *template.Template
}
// Run executes the prompt. Its displays the label and default value if any, asking the user to enter a value.
// Run will keep the prompt alive until it has been canceled from the command prompt or it has received a valid
// value. It will return the value and an error if any occurred during the prompt's execution.
func (p *Prompt) Run() (string, error) {
var err error
err = p.prepareTemplates()
if err != nil {
return "", err
}
c := &readline.Config{
Stdin: p.Stdin,
Stdout: p.Stdout,
EnableMask: p.Mask != 0,
MaskRune: p.Mask,
HistoryLimit: -1,
VimMode: p.IsVimMode,
UniqueEditLine: true,
FuncIsTerminal: func() bool { return false },
}
err = c.Init()
if err != nil {
return "", err
}
rl, err := readline.NewEx(c)
if err != nil {
return "", err
}
// we're taking over the cursor, so stop showing it.
rl.Write([]byte(hideCursor))
sb := screenbuf.New(rl)
validFn := func(x string) error {
return nil
}
if p.Validate != nil {
validFn = p.Validate
}
var inputErr error
input := p.Default
if p.IsConfirm {
input = ""
}
eraseDefault := input != "" && !p.AllowEdit
cur := NewCursor(input, p.Pointer, eraseDefault)
listen := func(input []rune, pos int, key rune) ([]rune, int, bool) {
_, _, keepOn := cur.Listen(input, pos, key)
err := validFn(cur.Get())
var prompt []byte
if err != nil {
prompt = render(p.Templates.invalid, p.Label)
} else {
prompt = render(p.Templates.valid, p.Label)
if p.IsConfirm {
prompt = render(p.Templates.prompt, p.Label)
}
}
echo := cur.Format()
if p.Mask != 0 {
echo = cur.FormatMask(p.Mask)
}
prompt = append(prompt, []byte(echo)...)
sb.Reset()
sb.Write(prompt)
if inputErr != nil {
validation := render(p.Templates.validation, inputErr)
sb.Write(validation)
inputErr = nil
}
sb.Flush()
return nil, 0, keepOn
}
c.SetListener(listen)
for {
_, err = rl.Readline()
inputErr = validFn(cur.Get())
if inputErr == nil {
break
}
if err != nil {
break
}
}
if err != nil {
switch err {
case readline.ErrInterrupt:
err = ErrInterrupt
case io.EOF:
err = ErrEOF
}
if err.Error() == "Interrupt" {
err = ErrInterrupt
}
sb.Reset()
sb.WriteString("")
sb.Flush()
rl.Write([]byte(showCursor))
rl.Close()
return "", err
}
echo := cur.Get()
if p.Mask != 0 {
echo = cur.GetMask(p.Mask)
}
prompt := render(p.Templates.success, p.Label)
prompt = append(prompt, []byte(echo)...)
if p.IsConfirm {
lowerDefault := strings.ToLower(p.Default)
if strings.ToLower(cur.Get()) != "y" && (lowerDefault != "y" || (lowerDefault == "y" && cur.Get() != "")) {
prompt = render(p.Templates.invalid, p.Label)
err = ErrAbort
}
}
if p.HideEntered {
clearScreen(sb)
} else {
sb.Reset()
sb.Write(prompt)
sb.Flush()
}
rl.Write([]byte(showCursor))
rl.Close()
return cur.Get(), err
}
func (p *Prompt) prepareTemplates() error {
tpls := p.Templates
if tpls == nil {
tpls = &PromptTemplates{}
}
if tpls.FuncMap == nil {
tpls.FuncMap = FuncMap
}
bold := Styler(FGBold)
if p.IsConfirm {
if tpls.Confirm == "" {
confirm := "y/N"
if strings.ToLower(p.Default) == "y" {
confirm = "Y/n"
}
tpls.Confirm = fmt.Sprintf(`{{ "%s" | bold }} {{ . | bold }}? {{ "[%s]" | faint }} `, IconInitial, confirm)
}
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Confirm)
if err != nil {
return err
}
tpls.prompt = tpl
} else {
if tpls.Prompt == "" {
tpls.Prompt = fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconInitial), bold(":"))
}
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Prompt)
if err != nil {
return err
}
tpls.prompt = tpl
}
if tpls.Valid == "" {
tpls.Valid = fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconGood), bold(":"))
}
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Valid)
if err != nil {
return err
}
tpls.valid = tpl
if tpls.Invalid == "" {
tpls.Invalid = fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconBad), bold(":"))
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Invalid)
if err != nil {
return err
}
tpls.invalid = tpl
if tpls.ValidationError == "" {
tpls.ValidationError = `{{ ">>" | red }} {{ . | red }}`
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.ValidationError)
if err != nil {
return err
}
tpls.validation = tpl
if tpls.Success == "" {
tpls.Success = fmt.Sprintf("{{ . | faint }}%s ", Styler(FGFaint)(":"))
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Success)
if err != nil {
return err
}
tpls.success = tpl
p.Templates = tpls
return nil
}