Skip to content

Commit

Permalink
Merge pull request #3 from nhatthm/support-multiselect
Browse files Browse the repository at this point in the history
Support multiselect
  • Loading branch information
nhatthm committed Apr 23, 2021
2 parents d385dcc + b1db99a commit f060af0
Show file tree
Hide file tree
Showing 9 changed files with 529 additions and 27 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ Type | Supported | Supported Actions
`Editor` | ✘ |
`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>
`Multiline` | ✘ |
`Multiselect` | |
`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>
`Password` | ✓ | <ul><li>Answer (+ check for `*`)</li><li>No answer</li><li>Interrupt (`^C`)</li><li>Ask for help</li></ul>
`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>
`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>

### Expect

Expand Down
19 changes: 18 additions & 1 deletion answer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import (
// ReactionTime is to create a small delay to simulate human reaction.
var ReactionTime = 10 * time.Millisecond

// WaitForReaction creates a small delay to simulate human reaction.
func WaitForReaction() <-chan time.Time {
return time.After(ReactionTime)
}

// Answer is an expectation for answering a question.
type Answer interface {
Step
Expand Down Expand Up @@ -135,10 +140,22 @@ func pressArrowDown() *Action {
return action(terminal.KeyArrowDown, "ARROW DOWN")
}

func pressArrowLeft() *Action {
return action(terminal.KeyArrowLeft, "ARROW LEFT")
}

func pressArrowRight() *Action {
return action(terminal.KeyArrowRight, "ARROW RIGHT")
}

func pressInterrupt() *Action {
return action(terminal.KeyInterrupt, "INTERRUPT")
}

func pressSpace() *Action {
return action(terminal.KeySpace, "SPACE")
}

func pressDelete() *Action {
return action(terminal.KeyDelete, "DELETE")
}
Expand All @@ -163,7 +180,7 @@ func (a *HelpAction) Do(c Console) error {

// String represents the answer as a string.
func (a *HelpAction) String() string {
return fmt.Sprintf("press %q", a.icon)
return fmt.Sprintf("press %q and see %q", a.icon, a.help)
}

func pressHelp(help string, options ...string) *HelpAction {
Expand Down
4 changes: 1 addition & 3 deletions cursor_posix.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

package surveyexpect

import "time"

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

return err
}
2 changes: 1 addition & 1 deletion cursor_windosw.go → cursor_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
package surveyexpect

func waitForCursor(c Console) error {
<-time.After(ReactionTime)
<-WaitForReaction()

return nil
}
75 changes: 57 additions & 18 deletions expectation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"regexp"
)

var indicatorRegex = regexp.MustCompile(`^([^ ]\s+)(.*)`)
var (
selectIndicatorRegex = regexp.MustCompile(`^([^ ]\s+)(.*)`)
multiselectIndicatorRegex = regexp.MustCompile(`^([^ ]\s+)(\[[x ]].*)`)
)

// SelectExpect expects strings from console.
// SelectExpect expects a select list from console.
type SelectExpect []string

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

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

var size int
sb.WriteLinef("Expect a select list:")
writeSelectList(&sb, *e, selectIndicatorRegex)

return sb.String()
}

func expectSelect(options ...string) *SelectExpect {
e := SelectExpect(options)

return &e
}

// MultiSelectExpect expects a multiselect list from console.
type MultiSelectExpect []string

// Do runs the step.
func (e *MultiSelectExpect) Do(c Console) error {
for _, o := range *e {
if _, err := c.ExpectString(o); err != nil {
return err
}
}

return nil
}

// String represents the answer as a string.
func (e *MultiSelectExpect) String() string {
var sb stringsBuilder

sb.WriteLinef("Expect a multiselect list:")
writeSelectList(&sb, *e, multiselectIndicatorRegex)

return sb.String()
}

func expectMultiSelect(options ...string) *MultiSelectExpect {
e := MultiSelectExpect(options)

return &e
}

func breakdownOptions(options []string, indicator *regexp.Regexp) (breakdown []map[string]string, pad string) {
breakdown = make([]map[string]string, 0, len(options))

var size int

for _, o := range options {
e := map[string]string{
"prefix": "",
"option": "",
}

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

var (
sb stringsBuilder
pad = fmt.Sprintf("%%-%ds", size)
)
return breakdown, fmt.Sprintf("%%-%ds", size)
}

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

for i, o := range breakdown {
if i > 0 {
Expand All @@ -63,12 +110,4 @@ func (e *SelectExpect) String() string {
sb.Writef(pad, o["prefix"]).
Writef(o["option"])
}

return sb.String()
}

func expectSelect(options ...string) *SelectExpect {
e := SelectExpect(options)

return &e
}
154 changes: 154 additions & 0 deletions multiselect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package surveyexpect

var _ Prompt = (*MultiSelectPrompt)(nil)

// MultiSelectPrompt is an expectation of survey.Select.
type MultiSelectPrompt struct {
*basePrompt

message string
steps *InlineSteps
}

func (p *MultiSelectPrompt) append(steps ...Step) *MultiSelectPrompt {
p.lock()
defer p.unlock()

p.steps.Append(steps...)

return p
}

// ShowHelp asks for help and asserts the help text.
//
// Survey.ExpectMultiSelect("Select a language:").
// ShowHelp("Your preferred language")
func (p *MultiSelectPrompt) ShowHelp(help string, options ...string) *MultiSelectPrompt {
return p.append(pressHelp(help, options...))
}

// Type sends some text to filter the options.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng")
func (p *MultiSelectPrompt) Type(s string) *MultiSelectPrompt {
return p.append(typeAnswer(s))
}

// Tab sends the TAB key the indicated times. Default is 1 when omitted.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng").
// Tab()
func (p *MultiSelectPrompt) Tab(times ...int) *MultiSelectPrompt {
return p.append(repeatStep(pressTab(), times...)...)
}

// Interrupt sends ^C and ends the sequence.
//
// Survey.ExpectMultiSelect("Select a language:").
// Interrupt()
func (p *MultiSelectPrompt) Interrupt() {
p.append(pressInterrupt())
p.steps.Close()
}

// Enter sends the ENTER key and ends the sequence.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng").
// Enter()
func (p *MultiSelectPrompt) Enter() {
p.append(pressEnter())
p.steps.Close()
}

// Delete sends the DELETE key the indicated times. Default is 1 when omitted.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng").
// Delete(3)
func (p *MultiSelectPrompt) Delete(times ...int) *MultiSelectPrompt {
return p.append(repeatStep(pressDelete(), times...)...)
}

// MoveUp sends the ARROW UP key the indicated times. Default is 1 when omitted.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng").
// MoveUp()
func (p *MultiSelectPrompt) MoveUp(times ...int) *MultiSelectPrompt {
return p.append(repeatStep(pressArrowUp(), times...)...)
}

// MoveDown sends the ARROW DOWN key the indicated times. Default is 1 when omitted.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng").
// MoveDown()
func (p *MultiSelectPrompt) MoveDown(times ...int) *MultiSelectPrompt {
return p.append(repeatStep(pressArrowDown(), times...)...)
}

// Select selects an option. If the option is selected, it will be deselected.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng").
// Select()
func (p *MultiSelectPrompt) Select() *MultiSelectPrompt {
return p.append(pressSpace())
}

// SelectNone deselects all filtered options.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng").
// SelectNone()
func (p *MultiSelectPrompt) SelectNone() *MultiSelectPrompt {
return p.append(pressArrowLeft())
}

// SelectAll selects all filtered options.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng").
// SelectAll()
func (p *MultiSelectPrompt) SelectAll() *MultiSelectPrompt {
return p.append(pressArrowRight())
}

// ExpectOptions expects a list of options.
//
// Survey.ExpectMultiSelect("Select a language:").
// Type("Eng").
// ExpectOptions("English")
func (p *MultiSelectPrompt) ExpectOptions(options ...string) *MultiSelectPrompt {
return p.append(expectMultiSelect(options...))
}

// Do runs the step.
func (p *MultiSelectPrompt) Do(c Console) error {
if _, err := c.ExpectString(p.message); err != nil {
return err
}

return p.steps.Do(c)
}

// String represents the expectation as a string.
func (p *MultiSelectPrompt) String() string {
var sb stringsBuilder

return sb.WriteLabelLinef("Expect", "MultiSelect Prompt").
WriteLabelLinef("Message", "%q", p.message).
WriteString(p.steps.String()).
String()
}

func newMultiSelect(parent *Survey, message string) *MultiSelectPrompt {
return &MultiSelectPrompt{
basePrompt: &basePrompt{parent: parent},
message: message,
steps: inlineSteps(),
}
}

0 comments on commit f060af0

Please sign in to comment.