Skip to content
This repository has been archived by the owner on Apr 19, 2024. It is now read-only.

Commit

Permalink
allow custom filtering behavior for Select and MultiSelect (#171)
Browse files Browse the repository at this point in the history
* Add capability to provide a filter function to filter options.

* Extract DefaultFilterFn to new file.

* Implement custom filter for multiselect as well.

* Add tests.

* Add documentation on custom filtering.

* Improve documentation.
  • Loading branch information
metacosm authored and AlecAivazis committed Jan 13, 2019
1 parent db1da55 commit 38cdfa1
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 26 deletions.
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,7 @@ prompt := &survey.Select{
survey.AskOne(prompt, &color, nil)
```

The user can filter for options by typing while the prompt is active. The user can also press `esc` to toggle
the ability cycle through the options with the j and k keys to do down and up respectively.
The user can also press `esc` to toggle the ability cycle through the options with the j and k keys to do down and up respectively.

By default, the select prompt is limited to showing 7 options at a time
and will paginate lists of options longer than that. To increase, you can either
Expand All @@ -177,8 +176,7 @@ prompt := &survey.MultiSelect{
survey.AskOne(prompt, &days, nil)
```

The user can filter for options by typing while the prompt is active. The user can also press `esc` to toggle
the ability cycle through the options with the j and k keys to do down and up respectively.
The user can also press `esc` to toggle the ability cycle through the options with the j and k keys to do down and up respectively.

By default, the MultiSelect prompt is limited to showing 7 options at a time
and will paginate lists of options longer than that. To increase, you can either
Expand All @@ -194,6 +192,33 @@ Launches the user's preferred editor (defined by the $EDITOR environment variabl
temporary file. Once the user exits their editor, the contents of the temporary file are read in as
the result. If neither of those are present, notepad (on Windows) or vim (Linux or Mac) is used.

## Filtering options in Select and MultiSelect

The user can filter for options by typing while the prompt is active. This will filter out all options that don't contain the
typed string anywhere in their name, ignoring case. This default filtering behavior is provided by the `DefaultFilterFn`
function.

A custom filter function can also be provided to change this default behavior by providing a value for the `FilterFn` field:

```golang
&Select{
Message: "Choose a color:",
Options: []string{"red", "blue", "green"},
FilterFn: func(filter string, options []string) (filtered []string) {
result := DefaultFilterFn(filter, options)
for _, v := range result {
if len(v) >= 5 {
filtered = append(filtered, v)
}
}
return
},
}
```

While the example above is contrived, this allows for use cases where "smarter" filtering might be useful, for example, when
options are backed by more complex types and filtering might need to occur on more metadata than just the displayed name.

## Validation

Validating individual responses for a particular question can be done by defining a
Expand Down
13 changes: 13 additions & 0 deletions filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package survey

import "strings"

var DefaultFilterFn = func(filter string, options []string) (answer []string) {
filter = strings.ToLower(filter)
for _, o := range options {
if strings.Contains(strings.ToLower(o), filter) {
answer = append(answer, o)
}
}
return answer
}
15 changes: 6 additions & 9 deletions multiselect.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type MultiSelect struct {
PageSize int
VimMode bool
FilterMessage string
FilterFn func(string, []string) []string
filter string
selectedIndex int
checked map[string]bool
Expand Down Expand Up @@ -146,17 +147,13 @@ func (m *MultiSelect) OnChange(line []rune, pos int, key rune) (newLine []rune,
}

func (m *MultiSelect) filterOptions() []string {
filter := strings.ToLower(m.filter)
if filter == "" {
if m.filter == "" {
return m.Options
}
answer := []string{}
for _, o := range m.Options {
if strings.Contains(strings.ToLower(o), filter) {
answer = append(answer, o)
}
if m.FilterFn != nil {
return m.FilterFn(m.filter, m.Options)
}
return answer
return DefaultFilterFn(m.filter, m.Options)
}

func (m *MultiSelect) Prompt() (interface{}, error) {
Expand All @@ -166,7 +163,7 @@ func (m *MultiSelect) Prompt() (interface{}, error) {
if len(m.Default) > 0 {
for _, dflt := range m.Default {
for _, opt := range m.Options {
// if the option correponds to the default
// if the option corresponds to the default
if opt == dflt {
// we found our initial value
m.checked[opt] = true
Expand Down
45 changes: 44 additions & 1 deletion multiselect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"
"testing"

expect "github.com/Netflix/go-expect"
"github.com/Netflix/go-expect"
"github.com/stretchr/testify/assert"
"gopkg.in/AlecAivazis/survey.v1/core"
"gopkg.in/AlecAivazis/survey.v1/terminal"
Expand Down Expand Up @@ -245,6 +245,49 @@ func TestMultiSelectPrompt(t *testing.T) {
},
[]string{"Tuesday"},
},
{
"Test MultiSelect prompt interaction with filter is case-insensitive",
&MultiSelect{
Message: "What days do you prefer:",
Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
},
func(c *expect.Console) {
c.ExpectString("What days do you prefer: [Use arrows to move, type to filter]")
// Filter down to Tuesday.
c.Send("tues")
// Select Tuesday.
c.Send(" ")
c.SendLine("")
c.ExpectEOF()
},
[]string{"Tuesday"},
},
{
"Test MultiSelect prompt interaction with custom filter",
&MultiSelect{
Message: "What days do you prefer:",
Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
FilterFn: func(filter string, options []string) (filtered []string) {
result := DefaultFilterFn(filter, options)
for _, v := range result {
if len(v) >= 7 {
filtered = append(filtered, v)
}
}
return
},
},
func(c *expect.Console) {
c.ExpectString("What days do you prefer:")
// Filter down to days which names are longer than 7 runes
c.Send("day")
// Select Wednesday.
c.Send(string(terminal.KeyArrowDown))
c.SendLine(" ")
c.ExpectEOF()
},
[]string{"Wednesday"},
},
{
"Test MultiSelect clears input on select",
&MultiSelect{
Expand Down
17 changes: 6 additions & 11 deletions select.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package survey

import (
"errors"
"strings"

"gopkg.in/AlecAivazis/survey.v1/core"
"gopkg.in/AlecAivazis/survey.v1/terminal"
)
Expand All @@ -28,6 +26,7 @@ type Select struct {
PageSize int
VimMode bool
FilterMessage string
FilterFn func(string, []string) []string
filter string
selectedIndex int
useDefault bool
Expand Down Expand Up @@ -154,17 +153,13 @@ func (s *Select) OnChange(line []rune, pos int, key rune) (newLine []rune, newPo
}

func (s *Select) filterOptions() []string {
filter := strings.ToLower(s.filter)
if filter == "" {
if s.filter == "" {
return s.Options
}
answer := []string{}
for _, o := range s.Options {
if strings.Contains(strings.ToLower(o), filter) {
answer = append(answer, o)
}
if s.FilterFn != nil {
return s.FilterFn(s.filter, s.Options)
}
return answer
return DefaultFilterFn(s.filter, s.Options)
}

func (s *Select) Prompt() (interface{}, error) {
Expand All @@ -180,7 +175,7 @@ func (s *Select) Prompt() (interface{}, error) {
if s.Default != "" {
// find the choice
for i, opt := range s.Options {
// if the option correponds to the default
// if the option corresponds to the default
if opt == s.Default {
// we found our initial value
sel = i
Expand Down
41 changes: 40 additions & 1 deletion select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"
"testing"

expect "github.com/Netflix/go-expect"
"github.com/Netflix/go-expect"
"github.com/stretchr/testify/assert"
"gopkg.in/AlecAivazis/survey.v1/core"
"gopkg.in/AlecAivazis/survey.v1/terminal"
Expand Down Expand Up @@ -219,6 +219,22 @@ func TestSelectPrompt(t *testing.T) {
},
"green",
},
{
"Test Select prompt interaction with filter is case-insensitive",
&Select{
Message: "Choose a color:",
Options: []string{"red", "blue", "green"},
},
func(c *expect.Console) {
c.ExpectString("Choose a color:")
// Filter down to red and green.
c.Send("RE")
// Select green.
c.SendLine(string(terminal.KeyArrowDown))
c.ExpectEOF()
},
"green",
},
{
"Can select the first result in a filtered list if there is a default",
&Select{
Expand All @@ -234,6 +250,29 @@ func TestSelectPrompt(t *testing.T) {
},
"red",
},
{
"Test Select prompt interaction with custom filter",
&Select{
Message: "Choose a color:",
Options: []string{"red", "blue", "green"},
FilterFn: func(filter string, options []string) (filtered []string) {
result := DefaultFilterFn(filter, options)
for _, v := range result {
if len(v) >= 5 {
filtered = append(filtered, v)
}
}
return
},
},
func(c *expect.Console) {
c.ExpectString("Choose a color:")
// Filter down to only green since custom filter only keeps options that are longer than 5 runes
c.SendLine("re")
c.ExpectEOF()
},
"green",
},
}

for _, test := range tests {
Expand Down

0 comments on commit 38cdfa1

Please sign in to comment.