Repository: AlecAivazis/survey
Pull request #439 (2022-08-05 14:41:06)

Add a RemoveSelectAll and RemoveSelectNone config to multi-select
Adds a config to prevent users from selecting all of the survey options and a config to remove the "select none/de-select all" option in multiselect. Using `survey.WithRemoveSelectAll()` will disable the ability for the right arrow key to select all of the options. It will also remove the instructions from the multi-select prompt.
Similarly, `survey.WithRemoveSelectNone()` will disable not allow a user to select none/de-select all options and the instruction won't show up in the prompt.

Aurora Innovation has a patch with this work in their repo, and I figured I would make the change upstream. I filed a ticket quite awhile ago and finally got around to adding the change: https://github.com/AlecAivazis/survey/issues/393

### Test Plan

Ran all of the tests. Added a new test that verifies the correct prompt and checks that using the right arrow key doesn't select all the options and using the left arrow key doesn't de-select all the options.


AlecAivazis: Thanks for putting this together @ces131! Should removing the select all also remove the select none behavior? Maybe we want a second option while we're here?
ces131: > Thanks for putting this together @ces131! Should removing the select all also remove the select none behavior? Maybe we want a second option while we're here?

If you'd like, I can add an additional option `RemoveSelectNone` while I'm here - for our use case I think we'd like to keep the "none" option even with the SelectAll removed. Sometimes our mulit-select question is "preloaded"/has a few options pre-selected so a quick way for the user to remove all of those is a nice to have
AlecAivazis: Yea i think we should just for completeness. thanks!

Patch:
diff --git a/README.md b/README.md
index ac2c6fc..ba75b32 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 `<right> 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 `<left> 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 3251ea1..3eb8342 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, <right> to all, <left> 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 }} <right> to all,{{end}}{{- if not .Config.RemoveSelectNone }} <left> 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/survey.go b/survey.go
index 95136eb..3e03b69 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 {


Test patch:
diff --git a/multiselect_test.go b/multiselect_test.go
index 41e8c2b..3d6b2ed 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, <left> 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, <right> 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_test.go b/survey_test.go
index d0d6230..ff8a158 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"})