1package survey 2 3import ( 4 "errors" 5 "strings" 6 7 "gopkg.in/AlecAivazis/survey.v1/core" 8 "gopkg.in/AlecAivazis/survey.v1/terminal" 9) 10 11/* 12MultiSelect is a prompt that presents a list of various options to the user 13for them to select using the arrow keys and enter. Response type is a slice of strings. 14 15 days := []string{} 16 prompt := &survey.MultiSelect{ 17 Message: "What days do you prefer:", 18 Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, 19 } 20 survey.AskOne(prompt, &days, nil) 21*/ 22type MultiSelect struct { 23 core.Renderer 24 Message string 25 Options []string 26 Default []string 27 Help string 28 PageSize int 29 VimMode bool 30 FilterMessage string 31 FilterFn func(string, []string) []string 32 filter string 33 selectedIndex int 34 checked map[string]bool 35 showingHelp bool 36} 37 38// data available to the templates when processing 39type MultiSelectTemplateData struct { 40 MultiSelect 41 Answer string 42 ShowAnswer bool 43 Checked map[string]bool 44 SelectedIndex int 45 ShowHelp bool 46 PageEntries []string 47} 48 49var MultiSelectQuestionTemplate = ` 50{{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} 51{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}} 52{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}} 53{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}} 54{{- else }} 55 {{- " "}}{{- color "cyan"}}[Use arrows to move, enter to select, type to filter{{- if and .Help (not .ShowHelp)}}, {{ HelpInputRune }} for more help{{end}}]{{color "reset"}} 56 {{- "\n"}} 57 {{- range $ix, $option := .PageEntries}} 58 {{- if eq $ix $.SelectedIndex}}{{color "cyan"}}{{ SelectFocusIcon }}{{color "reset"}}{{else}} {{end}} 59 {{- if index $.Checked $option}}{{color "green"}} {{ MarkedOptionIcon }} {{else}}{{color "default+hb"}} {{ UnmarkedOptionIcon }} {{end}} 60 {{- color "reset"}} 61 {{- " "}}{{$option}}{{"\n"}} 62 {{- end}} 63{{- end}}` 64 65// OnChange is called on every keypress. 66func (m *MultiSelect) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { 67 options := m.filterOptions() 68 oldFilter := m.filter 69 70 if key == terminal.KeyArrowUp || (m.VimMode && key == 'k') { 71 // if we are at the top of the list 72 if m.selectedIndex == 0 { 73 // go to the bottom 74 m.selectedIndex = len(options) - 1 75 } else { 76 // decrement the selected index 77 m.selectedIndex-- 78 } 79 } else if key == terminal.KeyArrowDown || (m.VimMode && key == 'j') { 80 // if we are at the bottom of the list 81 if m.selectedIndex == len(options)-1 { 82 // start at the top 83 m.selectedIndex = 0 84 } else { 85 // increment the selected index 86 m.selectedIndex++ 87 } 88 // if the user pressed down and there is room to move 89 } else if key == terminal.KeySpace { 90 if m.selectedIndex < len(options) { 91 if old, ok := m.checked[options[m.selectedIndex]]; !ok { 92 // otherwise just invert the current value 93 m.checked[options[m.selectedIndex]] = true 94 } else { 95 // otherwise just invert the current value 96 m.checked[options[m.selectedIndex]] = !old 97 } 98 m.filter = "" 99 } 100 // only show the help message if we have one to show 101 } else if key == core.HelpInputRune && m.Help != "" { 102 m.showingHelp = true 103 } else if key == terminal.KeyEscape { 104 m.VimMode = !m.VimMode 105 } else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine { 106 m.filter = "" 107 } else if key == terminal.KeyDelete || key == terminal.KeyBackspace { 108 if m.filter != "" { 109 m.filter = m.filter[0 : len(m.filter)-1] 110 } 111 } else if key >= terminal.KeySpace { 112 m.filter += string(key) 113 m.VimMode = false 114 } 115 116 m.FilterMessage = "" 117 if m.filter != "" { 118 m.FilterMessage = " " + m.filter 119 } 120 if oldFilter != m.filter { 121 // filter changed 122 options = m.filterOptions() 123 if len(options) > 0 && len(options) <= m.selectedIndex { 124 m.selectedIndex = len(options) - 1 125 } 126 } 127 // paginate the options 128 129 // TODO if we have started filtering and were looking at the end of a list 130 // and we have modified the filter then we should move the page back! 131 opts, idx := paginate(m.PageSize, options, m.selectedIndex) 132 133 // render the options 134 m.Render( 135 MultiSelectQuestionTemplate, 136 MultiSelectTemplateData{ 137 MultiSelect: *m, 138 SelectedIndex: idx, 139 Checked: m.checked, 140 ShowHelp: m.showingHelp, 141 PageEntries: opts, 142 }, 143 ) 144 145 // if we are not pressing ent 146 return line, 0, true 147} 148 149func (m *MultiSelect) filterOptions() []string { 150 if m.filter == "" { 151 return m.Options 152 } 153 if m.FilterFn != nil { 154 return m.FilterFn(m.filter, m.Options) 155 } 156 return DefaultFilterFn(m.filter, m.Options) 157} 158 159func (m *MultiSelect) Prompt() (interface{}, error) { 160 // compute the default state 161 m.checked = make(map[string]bool) 162 // if there is a default 163 if len(m.Default) > 0 { 164 for _, dflt := range m.Default { 165 for _, opt := range m.Options { 166 // if the option corresponds to the default 167 if opt == dflt { 168 // we found our initial value 169 m.checked[opt] = true 170 // stop looking 171 break 172 } 173 } 174 } 175 } 176 177 // if there are no options to render 178 if len(m.Options) == 0 { 179 // we failed 180 return "", errors.New("please provide options to select from") 181 } 182 183 // paginate the options 184 opts, idx := paginate(m.PageSize, m.Options, m.selectedIndex) 185 186 cursor := m.NewCursor() 187 cursor.Hide() // hide the cursor 188 defer cursor.Show() // show the cursor when we're done 189 190 // ask the question 191 err := m.Render( 192 MultiSelectQuestionTemplate, 193 MultiSelectTemplateData{ 194 MultiSelect: *m, 195 SelectedIndex: idx, 196 Checked: m.checked, 197 PageEntries: opts, 198 }, 199 ) 200 if err != nil { 201 return "", err 202 } 203 204 rr := m.NewRuneReader() 205 rr.SetTermMode() 206 defer rr.RestoreTermMode() 207 208 // start waiting for input 209 for { 210 r, _, _ := rr.ReadRune() 211 if r == '\r' || r == '\n' { 212 break 213 } 214 if r == terminal.KeyInterrupt { 215 return "", terminal.InterruptErr 216 } 217 if r == terminal.KeyEndTransmission { 218 break 219 } 220 m.OnChange(nil, 0, r) 221 } 222 m.filter = "" 223 m.FilterMessage = "" 224 225 answers := []string{} 226 for _, option := range m.Options { 227 if val, ok := m.checked[option]; ok && val { 228 answers = append(answers, option) 229 } 230 } 231 232 return answers, nil 233} 234 235// Cleanup removes the options section, and renders the ask like a normal question. 236func (m *MultiSelect) Cleanup(val interface{}) error { 237 // execute the output summary template with the answer 238 return m.Render( 239 MultiSelectQuestionTemplate, 240 MultiSelectTemplateData{ 241 MultiSelect: *m, 242 SelectedIndex: m.selectedIndex, 243 Checked: m.checked, 244 Answer: strings.Join(val.([]string), ", "), 245 ShowAnswer: true, 246 }, 247 ) 248} 249