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