diff --git a/README.md b/README.md index ac2c6fc7..ba75b329 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,39 @@ All of the prompts have a `Help` field which can be defined to provide more info } ``` +## Removing the "Select All" and "Select None" options + +By default, users can select all of the multi-select options using the right arrow key. To prevent users from being able to do this (and remove the ` to all` message from the prompt), use the option `WithRemoveSelectAll`: + +```golang +import ( + "github.com/AlecAivazis/survey/v2" +) + +number := "" +prompt := &survey.Input{ + Message: "This question has the select all option removed", +} + +survey.AskOne(prompt, &number, survey.WithRemoveSelectAll()) +``` + +Also by default, users can use the left arrow key to unselect all of the options. To prevent users from being able to do this (and remove the ` to none` message from the prompt), use the option `WithRemoveSelectNone`: + +```golang +import ( + "github.com/AlecAivazis/survey/v2" +) + +number := "" +prompt := &survey.Input{ + Message: "This question has the select all option removed", +} + +survey.AskOne(prompt, &number, survey.WithRemoveSelectNone()) +``` + + ### Changing the input rune In some situations, `?` is a perfectly valid response. To handle this, you can change the rune that survey diff --git a/multiselect.go b/multiselect.go index 3251ea1c..3eb83425 100644 --- a/multiselect.go +++ b/multiselect.go @@ -71,7 +71,7 @@ var MultiSelectQuestionTemplate = ` {{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}} {{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}} {{- else }} - {{- " "}}{{- color "cyan"}}[Use arrows to move, space to select, to all, to none, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}} + {{- " "}}{{- color "cyan"}}[Use arrows to move, space to select,{{- if not .Config.RemoveSelectAll }} to all,{{end}}{{- if not .Config.RemoveSelectNone }} to none,{{end}} type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}} {{- "\n"}} {{- range $ix, $option := .PageEntries}} {{- template "option" $.IterateOption $ix $option}} @@ -134,14 +134,14 @@ func (m *MultiSelect) OnChange(key rune, config *PromptConfig) { } else if key >= terminal.KeySpace { m.filter += string(key) m.VimMode = false - } else if key == terminal.KeyArrowRight { + } else if !config.RemoveSelectAll && key == terminal.KeyArrowRight { for _, v := range options { m.checked[v.Index] = true } if !config.KeepFilter { m.filter = "" } - } else if key == terminal.KeyArrowLeft { + } else if !config.RemoveSelectNone && key == terminal.KeyArrowLeft { for _, v := range options { m.checked[v.Index] = false } diff --git a/multiselect_test.go b/multiselect_test.go index 41e8c2bb..3d6b2ed1 100644 --- a/multiselect_test.go +++ b/multiselect_test.go @@ -579,3 +579,69 @@ func TestMultiSelectPromptKeepFilter(t *testing.T) { }) } } + +func TestMultiSelectPromptRemoveSelectAll(t *testing.T) { + tests := []PromptTest{ + { + "multi select with remove select all option", + &MultiSelect{ + Message: "What color do you prefer:", + Options: []string{"green", "red", "light-green", "blue", "black", "yellow", "purple"}, + }, + func(c expectConsole) { + c.ExpectString("What color do you prefer: [Use arrows to move, space to select, to none, type to filter]") + // Select the first option "green" + c.Send(" ") + + // attempt to select all (this shouldn't do anything) + c.Send(string(terminal.KeyArrowRight)) + + // end the session + c.SendLine("") + c.ExpectEOF() + }, + []core.OptionAnswer{ // we should only have one option selected, not all of them + {Value: "green", Index: 0}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + RunPromptTestRemoveSelectAll(t, test) + }) + } +} + +func TestMultiSelectPromptRemoveSelectNone(t *testing.T) { + tests := []PromptTest{ + { + "multi select with remove select none option", + &MultiSelect{ + Message: "What color do you prefer:", + Options: []string{"green", "red", "light-green", "blue", "black", "yellow", "purple"}, + }, + func(c expectConsole) { + c.ExpectString("What color do you prefer: [Use arrows to move, space to select, to all, type to filter]") + // Select the first option "green" + c.Send(" ") + + // attempt to unselect all (this shouldn't do anything) + c.Send(string(terminal.KeyArrowLeft)) + + // end the session + c.SendLine("") + c.ExpectEOF() + }, + []core.OptionAnswer{ // we should only have one option selected, not all of them + {Value: "green", Index: 0}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + RunPromptTestRemoveSelectNone(t, test) + }) + } +} diff --git a/survey.go b/survey.go index 95136ebe..3e03b697 100644 --- a/survey.go +++ b/survey.go @@ -56,8 +56,10 @@ func defaultAskOptions() *AskOptions { // include this option if it matches return strings.Contains(strings.ToLower(value), filter) }, - KeepFilter: false, - ShowCursor: false, + KeepFilter: false, + ShowCursor: false, + RemoveSelectAll: false, + RemoveSelectNone: false, }, } } @@ -111,13 +113,15 @@ type Question struct { // PromptConfig holds the global configuration for a prompt type PromptConfig struct { - PageSize int - Icons IconSet - HelpInput string - SuggestInput string - Filter func(filter string, option string, index int) bool - KeepFilter bool - ShowCursor bool + PageSize int + Icons IconSet + HelpInput string + SuggestInput string + Filter func(filter string, option string, index int) bool + KeepFilter bool + ShowCursor bool + RemoveSelectAll bool + RemoveSelectNone bool } // Prompt is the primary interface for the objects that can take user input @@ -175,6 +179,22 @@ func WithKeepFilter(KeepFilter bool) AskOpt { } } +// WithRemoveSelectAll remove the select all option in Multiselect +func WithRemoveSelectAll() AskOpt { + return func(options *AskOptions) error { + options.PromptConfig.RemoveSelectAll = true + return nil + } +} + +// WithRemoveSelectNone remove the select none/unselect all in Multiselect +func WithRemoveSelectNone() AskOpt { + return func(options *AskOptions) error { + options.PromptConfig.RemoveSelectNone = true + 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 d0d6230e..ff8a158b 100644 --- a/survey_test.go +++ b/survey_test.go @@ -97,6 +97,38 @@ func RunPromptTestKeepFilter(t *testing.T, test PromptTest) { require.Equal(t, test.expected, answer) } +func RunPromptTestRemoveSelectAll(t *testing.T, test PromptTest) { + t.Helper() + 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.RemoveSelectAll = true + answer, err = test.prompt.Prompt(config) + return err + }) + require.Equal(t, test.expected, answer) +} + +func RunPromptTestRemoveSelectNone(t *testing.T, test PromptTest) { + t.Helper() + 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.RemoveSelectNone = 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"})