Skip to content

Commit f060af0

Browse files
authored
Merge pull request #3 from nhatthm/support-multiselect
Support multiselect
2 parents d385dcc + b1db99a commit f060af0

9 files changed

+529
-27
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ Type | Supported | Supported Actions
2929
`Editor` | ✘ |
3030
`Input` | ✓ | <ul><li>Answer</li><li>No answer</li><li>Suggestions with navigation (Arrow Up ``, Arrow Down ``, Tab ``, Esc ``, Enter ``)</li><li>Interrupt (`^C`)</li><li>Ask for help</li></ul>
3131
`Multiline` | ✘ |
32-
`Multiselect` | |
32+
`Multiselect` | | <ul><li>Type to filter, delete</li><li>Navigation (Move Up ``, Move Down ``, Select None ``, Select All ``, Tab ``, Enter ``)</li><li>Interrupt (`^C`)</li><li>Ask for help</li></ul>
3333
`Password` | ✓ | <ul><li>Answer (+ check for `*`)</li><li>No answer</li><li>Interrupt (`^C`)</li><li>Ask for help</li></ul>
34-
`Select` | ✓ | <ul><li>Type to filter</li><li>Navigation (Arrow Up ``, Arrow Down ``, Tab ``, Esc ``, Enter ``)</li><li>Interrupt (`^C`)</li><li>Ask for help</li></ul>
34+
`Select` | ✓ | <ul><li>Type to filter, delete</li><li>Navigation (Move Up ``, Move Down ``, Tab ``, Enter ``)</li><li>Interrupt (`^C`)</li><li>Ask for help</li></ul>
3535

3636
### Expect
3737

answer.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import (
1010
// ReactionTime is to create a small delay to simulate human reaction.
1111
var ReactionTime = 10 * time.Millisecond
1212

13+
// WaitForReaction creates a small delay to simulate human reaction.
14+
func WaitForReaction() <-chan time.Time {
15+
return time.After(ReactionTime)
16+
}
17+
1318
// Answer is an expectation for answering a question.
1419
type Answer interface {
1520
Step
@@ -135,10 +140,22 @@ func pressArrowDown() *Action {
135140
return action(terminal.KeyArrowDown, "ARROW DOWN")
136141
}
137142

143+
func pressArrowLeft() *Action {
144+
return action(terminal.KeyArrowLeft, "ARROW LEFT")
145+
}
146+
147+
func pressArrowRight() *Action {
148+
return action(terminal.KeyArrowRight, "ARROW RIGHT")
149+
}
150+
138151
func pressInterrupt() *Action {
139152
return action(terminal.KeyInterrupt, "INTERRUPT")
140153
}
141154

155+
func pressSpace() *Action {
156+
return action(terminal.KeySpace, "SPACE")
157+
}
158+
142159
func pressDelete() *Action {
143160
return action(terminal.KeyDelete, "DELETE")
144161
}
@@ -163,7 +180,7 @@ func (a *HelpAction) Do(c Console) error {
163180

164181
// String represents the answer as a string.
165182
func (a *HelpAction) String() string {
166-
return fmt.Sprintf("press %q", a.icon)
183+
return fmt.Sprintf("press %q and see %q", a.icon, a.help)
167184
}
168185

169186
func pressHelp(help string, options ...string) *HelpAction {

cursor_posix.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
package surveyexpect
44

5-
import "time"
6-
75
func waitForCursor(c Console) error {
86
// ANSI escape sequence for DSR - Device Status Report
97
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
@@ -16,7 +14,7 @@ func waitForCursor(c Console) error {
1614
// After rendering the question, the prompt asks for the cursor's size and location (ESC[6n) and expects to receive
1715
// `ESC[n;mR` in return before reading the answer. If the addStep answers too fast (so the answer will be in between
1816
// `ESC[n;mR` and reading answer), the prompt won't see the answer and hangs indefinitely.
19-
<-time.After(ReactionTime)
17+
<-WaitForReaction()
2018

2119
return err
2220
}

cursor_windosw.go cursor_windows.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
package surveyexpect
44

55
func waitForCursor(c Console) error {
6-
<-time.After(ReactionTime)
6+
<-WaitForReaction()
77

88
return nil
99
}

expectation.go

+57-18
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import (
55
"regexp"
66
)
77

8-
var indicatorRegex = regexp.MustCompile(`^([^ ]\s+)(.*)`)
8+
var (
9+
selectIndicatorRegex = regexp.MustCompile(`^([^ ]\s+)(.*)`)
10+
multiselectIndicatorRegex = regexp.MustCompile(`^([^ ]\s+)(\[[x ]].*)`)
11+
)
912

10-
// SelectExpect expects strings from console.
13+
// SelectExpect expects a select list from console.
1114
type SelectExpect []string
1215

1316
// Do runs the step.
@@ -23,17 +26,62 @@ func (e *SelectExpect) Do(c Console) error {
2326

2427
// String represents the answer as a string.
2528
func (e *SelectExpect) String() string {
26-
breakdown := make([]map[string]string, 0)
29+
var sb stringsBuilder
2730

28-
var size int
31+
sb.WriteLinef("Expect a select list:")
32+
writeSelectList(&sb, *e, selectIndicatorRegex)
33+
34+
return sb.String()
35+
}
36+
37+
func expectSelect(options ...string) *SelectExpect {
38+
e := SelectExpect(options)
39+
40+
return &e
41+
}
2942

43+
// MultiSelectExpect expects a multiselect list from console.
44+
type MultiSelectExpect []string
45+
46+
// Do runs the step.
47+
func (e *MultiSelectExpect) Do(c Console) error {
3048
for _, o := range *e {
49+
if _, err := c.ExpectString(o); err != nil {
50+
return err
51+
}
52+
}
53+
54+
return nil
55+
}
56+
57+
// String represents the answer as a string.
58+
func (e *MultiSelectExpect) String() string {
59+
var sb stringsBuilder
60+
61+
sb.WriteLinef("Expect a multiselect list:")
62+
writeSelectList(&sb, *e, multiselectIndicatorRegex)
63+
64+
return sb.String()
65+
}
66+
67+
func expectMultiSelect(options ...string) *MultiSelectExpect {
68+
e := MultiSelectExpect(options)
69+
70+
return &e
71+
}
72+
73+
func breakdownOptions(options []string, indicator *regexp.Regexp) (breakdown []map[string]string, pad string) {
74+
breakdown = make([]map[string]string, 0, len(options))
75+
76+
var size int
77+
78+
for _, o := range options {
3179
e := map[string]string{
3280
"prefix": "",
3381
"option": "",
3482
}
3583

36-
if m := indicatorRegex.FindStringSubmatch(o); m != nil {
84+
if m := indicator.FindStringSubmatch(o); m != nil {
3785
e["prefix"] = m[1]
3886
e["option"] = m[2]
3987
l := len(m[1])
@@ -48,12 +96,11 @@ func (e *SelectExpect) String() string {
4896
breakdown = append(breakdown, e)
4997
}
5098

51-
var (
52-
sb stringsBuilder
53-
pad = fmt.Sprintf("%%-%ds", size)
54-
)
99+
return breakdown, fmt.Sprintf("%%-%ds", size)
100+
}
55101

56-
sb.WriteLinef("Expect a select list:")
102+
func writeSelectList(sb *stringsBuilder, options []string, indicator *regexp.Regexp) {
103+
breakdown, pad := breakdownOptions(options, indicator)
57104

58105
for i, o := range breakdown {
59106
if i > 0 {
@@ -63,12 +110,4 @@ func (e *SelectExpect) String() string {
63110
sb.Writef(pad, o["prefix"]).
64111
Writef(o["option"])
65112
}
66-
67-
return sb.String()
68-
}
69-
70-
func expectSelect(options ...string) *SelectExpect {
71-
e := SelectExpect(options)
72-
73-
return &e
74113
}

multiselect.go

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package surveyexpect
2+
3+
var _ Prompt = (*MultiSelectPrompt)(nil)
4+
5+
// MultiSelectPrompt is an expectation of survey.Select.
6+
type MultiSelectPrompt struct {
7+
*basePrompt
8+
9+
message string
10+
steps *InlineSteps
11+
}
12+
13+
func (p *MultiSelectPrompt) append(steps ...Step) *MultiSelectPrompt {
14+
p.lock()
15+
defer p.unlock()
16+
17+
p.steps.Append(steps...)
18+
19+
return p
20+
}
21+
22+
// ShowHelp asks for help and asserts the help text.
23+
//
24+
// Survey.ExpectMultiSelect("Select a language:").
25+
// ShowHelp("Your preferred language")
26+
func (p *MultiSelectPrompt) ShowHelp(help string, options ...string) *MultiSelectPrompt {
27+
return p.append(pressHelp(help, options...))
28+
}
29+
30+
// Type sends some text to filter the options.
31+
//
32+
// Survey.ExpectMultiSelect("Select a language:").
33+
// Type("Eng")
34+
func (p *MultiSelectPrompt) Type(s string) *MultiSelectPrompt {
35+
return p.append(typeAnswer(s))
36+
}
37+
38+
// Tab sends the TAB key the indicated times. Default is 1 when omitted.
39+
//
40+
// Survey.ExpectMultiSelect("Select a language:").
41+
// Type("Eng").
42+
// Tab()
43+
func (p *MultiSelectPrompt) Tab(times ...int) *MultiSelectPrompt {
44+
return p.append(repeatStep(pressTab(), times...)...)
45+
}
46+
47+
// Interrupt sends ^C and ends the sequence.
48+
//
49+
// Survey.ExpectMultiSelect("Select a language:").
50+
// Interrupt()
51+
func (p *MultiSelectPrompt) Interrupt() {
52+
p.append(pressInterrupt())
53+
p.steps.Close()
54+
}
55+
56+
// Enter sends the ENTER key and ends the sequence.
57+
//
58+
// Survey.ExpectMultiSelect("Select a language:").
59+
// Type("Eng").
60+
// Enter()
61+
func (p *MultiSelectPrompt) Enter() {
62+
p.append(pressEnter())
63+
p.steps.Close()
64+
}
65+
66+
// Delete sends the DELETE key the indicated times. Default is 1 when omitted.
67+
//
68+
// Survey.ExpectMultiSelect("Select a language:").
69+
// Type("Eng").
70+
// Delete(3)
71+
func (p *MultiSelectPrompt) Delete(times ...int) *MultiSelectPrompt {
72+
return p.append(repeatStep(pressDelete(), times...)...)
73+
}
74+
75+
// MoveUp sends the ARROW UP key the indicated times. Default is 1 when omitted.
76+
//
77+
// Survey.ExpectMultiSelect("Select a language:").
78+
// Type("Eng").
79+
// MoveUp()
80+
func (p *MultiSelectPrompt) MoveUp(times ...int) *MultiSelectPrompt {
81+
return p.append(repeatStep(pressArrowUp(), times...)...)
82+
}
83+
84+
// MoveDown sends the ARROW DOWN key the indicated times. Default is 1 when omitted.
85+
//
86+
// Survey.ExpectMultiSelect("Select a language:").
87+
// Type("Eng").
88+
// MoveDown()
89+
func (p *MultiSelectPrompt) MoveDown(times ...int) *MultiSelectPrompt {
90+
return p.append(repeatStep(pressArrowDown(), times...)...)
91+
}
92+
93+
// Select selects an option. If the option is selected, it will be deselected.
94+
//
95+
// Survey.ExpectMultiSelect("Select a language:").
96+
// Type("Eng").
97+
// Select()
98+
func (p *MultiSelectPrompt) Select() *MultiSelectPrompt {
99+
return p.append(pressSpace())
100+
}
101+
102+
// SelectNone deselects all filtered options.
103+
//
104+
// Survey.ExpectMultiSelect("Select a language:").
105+
// Type("Eng").
106+
// SelectNone()
107+
func (p *MultiSelectPrompt) SelectNone() *MultiSelectPrompt {
108+
return p.append(pressArrowLeft())
109+
}
110+
111+
// SelectAll selects all filtered options.
112+
//
113+
// Survey.ExpectMultiSelect("Select a language:").
114+
// Type("Eng").
115+
// SelectAll()
116+
func (p *MultiSelectPrompt) SelectAll() *MultiSelectPrompt {
117+
return p.append(pressArrowRight())
118+
}
119+
120+
// ExpectOptions expects a list of options.
121+
//
122+
// Survey.ExpectMultiSelect("Select a language:").
123+
// Type("Eng").
124+
// ExpectOptions("English")
125+
func (p *MultiSelectPrompt) ExpectOptions(options ...string) *MultiSelectPrompt {
126+
return p.append(expectMultiSelect(options...))
127+
}
128+
129+
// Do runs the step.
130+
func (p *MultiSelectPrompt) Do(c Console) error {
131+
if _, err := c.ExpectString(p.message); err != nil {
132+
return err
133+
}
134+
135+
return p.steps.Do(c)
136+
}
137+
138+
// String represents the expectation as a string.
139+
func (p *MultiSelectPrompt) String() string {
140+
var sb stringsBuilder
141+
142+
return sb.WriteLabelLinef("Expect", "MultiSelect Prompt").
143+
WriteLabelLinef("Message", "%q", p.message).
144+
WriteString(p.steps.String()).
145+
String()
146+
}
147+
148+
func newMultiSelect(parent *Survey, message string) *MultiSelectPrompt {
149+
return &MultiSelectPrompt{
150+
basePrompt: &basePrompt{parent: parent},
151+
message: message,
152+
steps: inlineSteps(),
153+
}
154+
}

0 commit comments

Comments
 (0)