1package survey 2 3import ( 4 "errors" 5 "io" 6 "os" 7 "strings" 8 9 "github.com/AlecAivazis/survey/v2/core" 10 "github.com/AlecAivazis/survey/v2/terminal" 11) 12 13// DefaultAskOptions is the default options on ask, using the OS stdio. 14func defaultAskOptions() *AskOptions { 15 return &AskOptions{ 16 Stdio: terminal.Stdio{ 17 In: os.Stdin, 18 Out: os.Stdout, 19 Err: os.Stderr, 20 }, 21 PromptConfig: PromptConfig{ 22 PageSize: 7, 23 HelpInput: "?", 24 SuggestInput: "tab", 25 Icons: IconSet{ 26 Error: Icon{ 27 Text: "X", 28 Format: "red", 29 }, 30 Help: Icon{ 31 Text: "?", 32 Format: "cyan", 33 }, 34 Question: Icon{ 35 Text: "?", 36 Format: "green+hb", 37 }, 38 MarkedOption: Icon{ 39 Text: "[x]", 40 Format: "green", 41 }, 42 UnmarkedOption: Icon{ 43 Text: "[ ]", 44 Format: "default+hb", 45 }, 46 SelectFocus: Icon{ 47 Text: ">", 48 Format: "cyan+b", 49 }, 50 }, 51 Filter: func(filter string, value string, index int) (include bool) { 52 filter = strings.ToLower(filter) 53 54 // include this option if it matches 55 return strings.Contains(strings.ToLower(value), filter) 56 }, 57 KeepFilter: false, 58 }, 59 } 60} 61func defaultPromptConfig() *PromptConfig { 62 return &defaultAskOptions().PromptConfig 63} 64 65func defaultIcons() *IconSet { 66 return &defaultPromptConfig().Icons 67} 68 69// OptionAnswer is an ergonomic alias for core.OptionAnswer 70type OptionAnswer = core.OptionAnswer 71 72// Icon holds the text and format to show for a particular icon 73type Icon struct { 74 Text string 75 Format string 76} 77 78// IconSet holds the icons to use for various prompts 79type IconSet struct { 80 HelpInput Icon 81 Error Icon 82 Help Icon 83 Question Icon 84 MarkedOption Icon 85 UnmarkedOption Icon 86 SelectFocus Icon 87} 88 89// Validator is a function passed to a Question after a user has provided a response. 90// If the function returns an error, then the user will be prompted again for another 91// response. 92type Validator func(ans interface{}) error 93 94// Transformer is a function passed to a Question after a user has provided a response. 95// The function can be used to implement a custom logic that will result to return 96// a different representation of the given answer. 97// 98// Look `TransformString`, `ToLower` `Title` and `ComposeTransformers` for more. 99type Transformer func(ans interface{}) (newAns interface{}) 100 101// Question is the core data structure for a survey questionnaire. 102type Question struct { 103 Name string 104 Prompt Prompt 105 Validate Validator 106 Transform Transformer 107} 108 109// PromptConfig holds the global configuration for a prompt 110type PromptConfig struct { 111 PageSize int 112 Icons IconSet 113 HelpInput string 114 SuggestInput string 115 Filter func(filter string, option string, index int) bool 116 KeepFilter bool 117} 118 119// Prompt is the primary interface for the objects that can take user input 120// and return a response. 121type Prompt interface { 122 Prompt(config *PromptConfig) (interface{}, error) 123 Cleanup(*PromptConfig, interface{}) error 124 Error(*PromptConfig, error) error 125} 126 127// PromptAgainer Interface for Prompts that support prompting again after invalid input 128type PromptAgainer interface { 129 PromptAgain(config *PromptConfig, invalid interface{}, err error) (interface{}, error) 130} 131 132// AskOpt allows setting optional ask options. 133type AskOpt func(options *AskOptions) error 134 135// AskOptions provides additional options on ask. 136type AskOptions struct { 137 Stdio terminal.Stdio 138 Validators []Validator 139 PromptConfig PromptConfig 140} 141 142// WithStdio specifies the standard input, output and error files survey 143// interacts with. By default, these are os.Stdin, os.Stdout, and os.Stderr. 144func WithStdio(in terminal.FileReader, out terminal.FileWriter, err io.Writer) AskOpt { 145 return func(options *AskOptions) error { 146 options.Stdio.In = in 147 options.Stdio.Out = out 148 options.Stdio.Err = err 149 return nil 150 } 151} 152 153// WithFilter specifies the default filter to use when asking questions. 154func WithFilter(filter func(filter string, value string, index int) (include bool)) AskOpt { 155 return func(options *AskOptions) error { 156 // save the filter internally 157 options.PromptConfig.Filter = filter 158 159 return nil 160 } 161} 162 163// WithKeepFilter sets the if the filter is kept after selections 164func WithKeepFilter(KeepFilter bool) AskOpt { 165 return func(options *AskOptions) error { 166 // set the page size 167 options.PromptConfig.KeepFilter = KeepFilter 168 169 // nothing went wrong 170 return nil 171 } 172} 173 174// WithValidator specifies a validator to use while prompting the user 175func WithValidator(v Validator) AskOpt { 176 return func(options *AskOptions) error { 177 // add the provided validator to the list 178 options.Validators = append(options.Validators, v) 179 180 // nothing went wrong 181 return nil 182 } 183} 184 185type wantsStdio interface { 186 WithStdio(terminal.Stdio) 187} 188 189// WithPageSize sets the default page size used by prompts 190func WithPageSize(pageSize int) AskOpt { 191 return func(options *AskOptions) error { 192 // set the page size 193 options.PromptConfig.PageSize = pageSize 194 195 // nothing went wrong 196 return nil 197 } 198} 199 200// WithHelpInput changes the character that prompts look for to give the user helpful information. 201func WithHelpInput(r rune) AskOpt { 202 return func(options *AskOptions) error { 203 // set the input character 204 options.PromptConfig.HelpInput = string(r) 205 206 // nothing went wrong 207 return nil 208 } 209} 210 211// WithIcons sets the icons that will be used when prompting the user 212func WithIcons(setIcons func(*IconSet)) AskOpt { 213 return func(options *AskOptions) error { 214 // update the default icons with whatever the user says 215 setIcons(&options.PromptConfig.Icons) 216 217 // nothing went wrong 218 return nil 219 } 220} 221 222/* 223AskOne performs the prompt for a single prompt and asks for validation if required. 224Response types should be something that can be casted from the response type designated 225in the documentation. For example: 226 227 name := "" 228 prompt := &survey.Input{ 229 Message: "name", 230 } 231 232 survey.AskOne(prompt, &name) 233 234*/ 235func AskOne(p Prompt, response interface{}, opts ...AskOpt) error { 236 err := Ask([]*Question{{Prompt: p}}, response, opts...) 237 if err != nil { 238 return err 239 } 240 241 return nil 242} 243 244/* 245Ask performs the prompt loop, asking for validation when appropriate. The response 246type can be one of two options. If a struct is passed, the answer will be written to 247the field whose name matches the Name field on the corresponding question. Field types 248should be something that can be casted from the response type designated in the 249documentation. Note, a survey tag can also be used to identify a Otherwise, a 250map[string]interface{} can be passed, responses will be written to the key with the 251matching name. For example: 252 253 qs := []*survey.Question{ 254 { 255 Name: "name", 256 Prompt: &survey.Input{Message: "What is your name?"}, 257 Validate: survey.Required, 258 Transform: survey.Title, 259 }, 260 } 261 262 answers := struct{ Name string }{} 263 264 265 err := survey.Ask(qs, &answers) 266*/ 267func Ask(qs []*Question, response interface{}, opts ...AskOpt) error { 268 // build up the configuration options 269 options := defaultAskOptions() 270 for _, opt := range opts { 271 if opt == nil { 272 continue 273 } 274 if err := opt(options); err != nil { 275 return err 276 } 277 } 278 279 // if we weren't passed a place to record the answers 280 if response == nil { 281 // we can't go any further 282 return errors.New("cannot call Ask() with a nil reference to record the answers") 283 } 284 285 // go over every question 286 for _, q := range qs { 287 // If Prompt implements controllable stdio, pass in specified stdio. 288 if p, ok := q.Prompt.(wantsStdio); ok { 289 p.WithStdio(options.Stdio) 290 } 291 292 // grab the user input and save it 293 ans, err := q.Prompt.Prompt(&options.PromptConfig) 294 // if there was a problem 295 if err != nil { 296 return err 297 } 298 299 // build up a list of validators that we have to apply to this question 300 validators := []Validator{} 301 302 // make sure to include the question specific one 303 if q.Validate != nil { 304 validators = append(validators, q.Validate) 305 } 306 // add any "global" validators 307 for _, validator := range options.Validators { 308 validators = append(validators, validator) 309 } 310 311 // apply every validator to thte response 312 for _, validator := range validators { 313 // wait for a valid response 314 for invalid := validator(ans); invalid != nil; invalid = validator(ans) { 315 err := q.Prompt.Error(&options.PromptConfig, invalid) 316 // if there was a problem 317 if err != nil { 318 return err 319 } 320 321 // ask for more input 322 if promptAgainer, ok := q.Prompt.(PromptAgainer); ok { 323 ans, err = promptAgainer.PromptAgain(&options.PromptConfig, ans, invalid) 324 } else { 325 ans, err = q.Prompt.Prompt(&options.PromptConfig) 326 } 327 // if there was a problem 328 if err != nil { 329 return err 330 } 331 } 332 } 333 334 if q.Transform != nil { 335 // check if we have a transformer available, if so 336 // then try to acquire the new representation of the 337 // answer, if the resulting answer is not nil. 338 if newAns := q.Transform(ans); newAns != nil { 339 ans = newAns 340 } 341 } 342 343 // tell the prompt to cleanup with the validated value 344 q.Prompt.Cleanup(&options.PromptConfig, ans) 345 346 // if something went wrong 347 if err != nil { 348 // stop listening 349 return err 350 } 351 352 // add it to the map 353 err = core.WriteAnswer(response, q.Name, ans) 354 // if something went wrong 355 if err != nil { 356 return err 357 } 358 359 } 360 361 // return the response 362 return nil 363} 364 365// paginate returns a single page of choices given the page size, the total list of 366// possible choices, and the current selected index in the total list. 367func paginate(pageSize int, choices []core.OptionAnswer, sel int) ([]core.OptionAnswer, int) { 368 var start, end, cursor int 369 370 if len(choices) < pageSize { 371 // if we dont have enough options to fill a page 372 start = 0 373 end = len(choices) 374 cursor = sel 375 376 } else if sel < pageSize/2 { 377 // if we are in the first half page 378 start = 0 379 end = pageSize 380 cursor = sel 381 382 } else if len(choices)-sel-1 < pageSize/2 { 383 // if we are in the last half page 384 start = len(choices) - pageSize 385 end = len(choices) 386 cursor = sel - start 387 388 } else { 389 // somewhere in the middle 390 above := pageSize / 2 391 below := pageSize - above 392 393 cursor = pageSize / 2 394 start = sel - above 395 end = sel + below 396 } 397 398 // return the subset we care about and the index 399 return choices[start:end], cursor 400} 401