diff --git a/README.md b/README.md index 1adec5a3..41001675 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,24 @@ func myFilter(filterValue string, optValue string, optIndex int) bool { survey.AskOne(prompt, &color, survey.WithFilter(myFilter)) ``` +## Keeping the filter active + +By default the filter will disappear if the user selects one of the filtered elements. Once the user selects one element the filter setting is gone. + +However the user can prevent this from happening and keep the filter active for multiple selections in a e.g. MultiSelect: + +```golang +// configure it for a specific prompt +&Select{ + Message: "Choose a color:", + Options: []string{"light-green", "green", "dark-green", "red"}, + KeepFilter: true, +} + +// or define a default for all of the questions +survey.AskOne(prompt, &color, survey.WithKeepFilter(true)) +``` + ## Validation Validating individual responses for a particular question can be done by defining a diff --git a/examples/longmultikeepfilter.go b/examples/longmultikeepfilter.go new file mode 100644 index 00000000..888e692b --- /dev/null +++ b/examples/longmultikeepfilter.go @@ -0,0 +1,277 @@ +package main + +import ( + "fmt" + "strings" + + survey "github.com/AlecAivazis/survey/v2" +) + +// the questions to ask +var multiQs = []*survey.Question{ + { + Name: "letter", + Prompt: &survey.MultiSelect{ + Message: "Choose one or more words :", + Options: []string{ + "Afghanistan", + "Ă…land Islands", + "Albania", + "Algeria", + "American Samoa", + "AndorrA", + "Angola", + "Anguilla", + "Antarctica", + "Antigua and Barbuda", + "Argentina", + "Armenia", + "Aruba", + "Australia", + "Austria", + "Azerbaijan", + "Bahamas", + "Bahrain", + "Bangladesh", + "Barbados", + "Belarus", + "Belgium", + "Belize", + "Benin", + "Bermuda", + "Bhutan", + "Bolivia", + "Bosnia and Herzegovina", + "Botswana", + "Bouvet Island", + "Brazil", + "British Indian Ocean Territory", + "Brunei Darussalam", + "Bulgaria", + "Burkina Faso", + "Burundi", + "Cambodia", + "Cameroon", + "Canada", + "Cape Verde", + "Cayman Islands", + "Central African Republic", + "Chad", + "Chile", + "China", + "Christmas Island", + "Cocos (Keeling) Islands", + "Colombia", + "Comoros", + "Congo", + "Congo, The Democratic Republic of the", + "Cook Islands", + "Costa Rica", + "Cote D'Ivoire", + "Croatia", + "Cuba", + "Cyprus", + "Czech Republic", + "Denmark", + "Djibouti", + "Dominica", + "Dominican Republic", + "Ecuador", + "Egypt", + "El Salvador", + "Equatorial Guinea", + "Eritrea", + "Estonia", + "Ethiopia", + "Falkland Islands (Malvinas)", + "Faroe Islands", + "Fiji", + "Finland", + "France", + "French Guiana", + "French Polynesia", + "French Southern Territories", + "Gabon", + "Gambia", + "Georgia", + "Germany", + "Ghana", + "Gibraltar", + "Greece", + "Greenland", + "Grenada", + "Guadeloupe", + "Guam", + "Guatemala", + "Guernsey", + "Guinea", + "Guinea-Bissau", + "Guyana", + "Haiti", + "Heard Island and Mcdonald Islands", + "Holy See (Vatican City State)", + "Honduras", + "Hong Kong", + "Hungary", + "Iceland", + "India", + "Indonesia", + "Iran, Islamic Republic Of", + "Iraq", + "Ireland", + "Isle of Man", + "Israel", + "Italy", + "Jamaica", + "Japan", + "Jersey", + "Jordan", + "Kazakhstan", + "Kenya", + "Kiribati", + "Korea, Democratic People'S Republic of", + "Korea, Republic of", + "Kuwait", + "Kyrgyzstan", + "Lao People'S Democratic Republic", + "Latvia", + "Lebanon", + "Lesotho", + "Liberia", + "Libyan Arab Jamahiriya", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Macao", + "Macedonia, The Former Yugoslav Republic of", + "Madagascar", + "Malawi", + "Malaysia", + "Maldives", + "Mali", + "Malta", + "Marshall Islands", + "Martinique", + "Mauritania", + "Mauritius", + "Mayotte", + "Mexico", + "Micronesia, Federated States of", + "Moldova, Republic of", + "Monaco", + "Mongolia", + "Montserrat", + "Morocco", + "Mozambique", + "Myanmar", + "Namibia", + "Nauru", + "Nepal", + "Netherlands", + "Netherlands Antilles", + "New Caledonia", + "New Zealand", + "Nicaragua", + "Niger", + "Nigeria", + "Niue", + "Norfolk Island", + "Northern Mariana Islands", + "Norway", + "Oman", + "Pakistan", + "Palau", + "Palestinian Territory, Occupied", + "Panama", + "Papua New Guinea", + "Paraguay", + "Peru", + "Philippines", + "Pitcairn", + "Poland", + "Portugal", + "Puerto Rico", + "Qatar", + "Reunion", + "Romania", + "Russian Federation", + "RWANDA", + "Saint Helena", + "Saint Kitts and Nevis", + "Saint Lucia", + "Saint Pierre and Miquelon", + "Saint Vincent and the Grenadines", + "Samoa", + "San Marino", + "Sao Tome and Principe", + "Saudi Arabia", + "Senegal", + "Serbia and Montenegro", + "Seychelles", + "Sierra Leone", + "Singapore", + "Slovakia", + "Slovenia", + "Solomon Islands", + "Somalia", + "South Africa", + "South Georgia and the South Sandwich Islands", + "Spain", + "Sri Lanka", + "Sudan", + "Suriname", + "Svalbard and Jan Mayen", + "Swaziland", + "Sweden", + "Switzerland", + "Syrian Arab Republic", + "Taiwan, Province of China", + "Tajikistan", + "Tanzania, United Republic of", + "Thailand", + "Timor-Leste", + "Togo", + "Tokelau", + "Tonga", + "Trinidad and Tobago", + "Tunisia", + "Turkey", + "Turkmenistan", + "Turks and Caicos Islands", + "Tuvalu", + "Uganda", + "Ukraine", + "United Arab Emirates", + "United Kingdom", + "United States", + "United States Minor Outlying Islands", + "Uruguay", + "Uzbekistan", + "Vanuatu", + "Venezuela", + "Viet Nam", + "Virgin Islands, British", + "Virgin Islands, U.S.", + "Wallis and Futuna", + "Western Sahara", + "Yemen", + "Zambia", + "Zimbabwe", + }, + }, + }, +} + +func main() { + answers := []string{} + + // ask the question + err := survey.Ask(multiQs, &answers, survey.WithKeepFilter(true)) + + if err != nil { + fmt.Println(err.Error()) + return + } + // print the answers + fmt.Printf("you chose: %s\n", strings.Join(answers, ", ")) +} diff --git a/multiselect.go b/multiselect.go index 314b1bd0..d5e0625b 100644 --- a/multiselect.go +++ b/multiselect.go @@ -100,7 +100,9 @@ func (m *MultiSelect) OnChange(key rune, config *PromptConfig) { // otherwise just invert the current value m.checked[selectedOpt.Index] = !old } - m.filter = "" + if !config.KeepFilter { + m.filter = "" + } } // only show the help message if we have one to show } else if string(key) == config.HelpInput && m.Help != "" { diff --git a/multiselect_test.go b/multiselect_test.go index ffd18523..1c090686 100644 --- a/multiselect_test.go +++ b/multiselect_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/Netflix/go-expect" + expect "github.com/Netflix/go-expect" "github.com/stretchr/testify/assert" "github.com/AlecAivazis/survey/v2/core" @@ -363,3 +363,37 @@ func TestMultiSelectPrompt(t *testing.T) { }) } } + +func TestMultiSelectPromptKeepFilter(t *testing.T) { + tests := []PromptTest{ + { + "multi select with filter keep", + &MultiSelect{ + Message: "What color do you prefer:", + Options: []string{"green", "red", "light-green", "blue", "black", "yellow", "purple"}, + }, + func(c *expect.Console) { + c.ExpectString("What color do you prefer: [Use arrows to move, enter to select, type to filter]") + // Filter down to green + c.Send("green") + // Select green. + c.Send(" ") + // Select light-green. + c.Send(string(terminal.KeyArrowDown)) + c.Send(" ") + c.SendLine("") + c.ExpectEOF() + }, + []core.OptionAnswer{ + core.OptionAnswer{Value: "green", Index: 0}, + core.OptionAnswer{Value: "light-green", Index: 2}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + RunPromptTestKeepFilter(t, test) + }) + } +} diff --git a/survey.go b/survey.go index eaa84393..ede27272 100644 --- a/survey.go +++ b/survey.go @@ -53,6 +53,7 @@ func defaultAskOptions() *AskOptions { // include this option if it matches return strings.Contains(strings.ToLower(value), filter) }, + KeepFilter: false, }, } } @@ -106,10 +107,11 @@ type Question struct { // PromptConfig holds the global configuration for a prompt type PromptConfig struct { - PageSize int - Icons IconSet - HelpInput string - Filter func(filter string, option string, index int) bool + PageSize int + Icons IconSet + HelpInput string + Filter func(filter string, option string, index int) bool + KeepFilter bool } // Prompt is the primary interface for the objects that can take user input @@ -156,6 +158,17 @@ func WithFilter(filter func(filter string, value string, index int) (include boo } } +// WithKeepFilter sets the if the filter is kept after selections +func WithKeepFilter(KeepFilter bool) AskOpt { + return func(options *AskOptions) error { + // set the page size + options.PromptConfig.KeepFilter = KeepFilter + + // nothing went wrong + return nil + } +} + // WithValidator specifies a validator to use while prompting the user func WithValidator(v Validator) AskOpt { return func(options *AskOptions) error { diff --git a/survey_test.go b/survey_test.go index 658ddbb5..58f8420e 100644 --- a/survey_test.go +++ b/survey_test.go @@ -43,6 +43,21 @@ func RunPromptTest(t *testing.T, test PromptTest) { require.Equal(t, test.expected, answer) } +func RunPromptTestKeepFilter(t *testing.T, test PromptTest) { + var answer interface{} + RunTest(t, test.procedure, func(stdio terminal.Stdio) error { + var err error + if p, ok := test.prompt.(wantsStdio); ok { + p.WithStdio(stdio) + } + config := defaultPromptConfig() + config.KeepFilter = true + answer, err = test.prompt.Prompt(config) + return err + }) + require.Equal(t, test.expected, answer) +} + func TestPagination_tooFew(t *testing.T) { // a small list of options choices := core.OptionAnswerList([]string{"choice1", "choice2", "choice3"}) @@ -235,10 +250,10 @@ func TestAsk(t *testing.T) { "pizza": true, "commit-message": "Add editor prompt tests\n", "commit-message-validated": "Add editor prompt tests\n", - "name": "Johnny Appleseed", - "day": []string{"Monday", "Wednesday"}, - "password": "secret", - "color": "yellow", + "name": "Johnny Appleseed", + "day": []string{"Monday", "Wednesday"}, + "password": "secret", + "color": "yellow", }, }, {