1package survey 2 3import ( 4 "errors" 5 "io" 6 "os" 7 8 "gopkg.in/AlecAivazis/survey.v1/core" 9 "gopkg.in/AlecAivazis/survey.v1/terminal" 10) 11 12// PageSize is the default maximum number of items to show in select/multiselect prompts 13var PageSize = 7 14 15// DefaultAskOptions is the default options on ask, using the OS stdio. 16var DefaultAskOptions = AskOptions{ 17 Stdio: terminal.Stdio{ 18 In: os.Stdin, 19 Out: os.Stdout, 20 Err: os.Stderr, 21 }, 22} 23 24// Validator is a function passed to a Question after a user has provided a response. 25// If the function returns an error, then the user will be prompted again for another 26// response. 27type Validator func(ans interface{}) error 28 29// Transformer is a function passed to a Question after a user has provided a response. 30// The function can be used to implement a custom logic that will result to return 31// a different representation of the given answer. 32// 33// Look `TransformString`, `ToLower` `Title` and `ComposeTransformers` for more. 34type Transformer func(ans interface{}) (newAns interface{}) 35 36// Question is the core data structure for a survey questionnaire. 37type Question struct { 38 Name string 39 Prompt Prompt 40 Validate Validator 41 Transform Transformer 42} 43 44// Prompt is the primary interface for the objects that can take user input 45// and return a response. 46type Prompt interface { 47 Prompt() (interface{}, error) 48 Cleanup(interface{}) error 49 Error(error) error 50} 51 52// PromptAgainer Interface for Prompts that support prompting again after invalid input 53type PromptAgainer interface { 54 PromptAgain(invalid interface{}, err error) (interface{}, error) 55} 56 57// AskOpt allows setting optional ask options. 58type AskOpt func(options *AskOptions) error 59 60// AskOptions provides additional options on ask. 61type AskOptions struct { 62 Stdio terminal.Stdio 63} 64 65// WithStdio specifies the standard input, output and error files survey 66// interacts with. By default, these are os.Stdin, os.Stdout, and os.Stderr. 67func WithStdio(in terminal.FileReader, out terminal.FileWriter, err io.Writer) AskOpt { 68 return func(options *AskOptions) error { 69 options.Stdio.In = in 70 options.Stdio.Out = out 71 options.Stdio.Err = err 72 return nil 73 } 74} 75 76type wantsStdio interface { 77 WithStdio(terminal.Stdio) 78} 79 80/* 81AskOne performs the prompt for a single prompt and asks for validation if required. 82Response types should be something that can be casted from the response type designated 83in the documentation. For example: 84 85 name := "" 86 prompt := &survey.Input{ 87 Message: "name", 88 } 89 90 survey.AskOne(prompt, &name, nil) 91 92*/ 93func AskOne(p Prompt, response interface{}, v Validator, opts ...AskOpt) error { 94 err := Ask([]*Question{{Prompt: p, Validate: v}}, response, opts...) 95 if err != nil { 96 return err 97 } 98 99 return nil 100} 101 102/* 103Ask performs the prompt loop, asking for validation when appropriate. The response 104type can be one of two options. If a struct is passed, the answer will be written to 105the field whose name matches the Name field on the corresponding question. Field types 106should be something that can be casted from the response type designated in the 107documentation. Note, a survey tag can also be used to identify a Otherwise, a 108map[string]interface{} can be passed, responses will be written to the key with the 109matching name. For example: 110 111 qs := []*survey.Question{ 112 { 113 Name: "name", 114 Prompt: &survey.Input{Message: "What is your name?"}, 115 Validate: survey.Required, 116 Transform: survey.Title, 117 }, 118 } 119 120 answers := struct{ Name string }{} 121 122 123 err := survey.Ask(qs, &answers) 124*/ 125func Ask(qs []*Question, response interface{}, opts ...AskOpt) error { 126 127 options := DefaultAskOptions 128 for _, opt := range opts { 129 if err := opt(&options); err != nil { 130 return err 131 } 132 } 133 134 // if we weren't passed a place to record the answers 135 if response == nil { 136 // we can't go any further 137 return errors.New("cannot call Ask() with a nil reference to record the answers") 138 } 139 140 // go over every question 141 for _, q := range qs { 142 // If Prompt implements controllable stdio, pass in specified stdio. 143 if p, ok := q.Prompt.(wantsStdio); ok { 144 p.WithStdio(options.Stdio) 145 } 146 147 // grab the user input and save it 148 ans, err := q.Prompt.Prompt() 149 // if there was a problem 150 if err != nil { 151 return err 152 } 153 154 // if there is a validate handler for this question 155 if q.Validate != nil { 156 // wait for a valid response 157 for invalid := q.Validate(ans); invalid != nil; invalid = q.Validate(ans) { 158 err := q.Prompt.Error(invalid) 159 // if there was a problem 160 if err != nil { 161 return err 162 } 163 164 // ask for more input 165 if promptAgainer, ok := q.Prompt.(PromptAgainer); ok { 166 ans, err = promptAgainer.PromptAgain(ans, invalid) 167 } else { 168 ans, err = q.Prompt.Prompt() 169 } 170 // if there was a problem 171 if err != nil { 172 return err 173 } 174 } 175 } 176 177 if q.Transform != nil { 178 // check if we have a transformer available, if so 179 // then try to acquire the new representation of the 180 // answer, if the resulting answer is not nil. 181 if newAns := q.Transform(ans); newAns != nil { 182 ans = newAns 183 } 184 } 185 186 // tell the prompt to cleanup with the validated value 187 q.Prompt.Cleanup(ans) 188 189 // if something went wrong 190 if err != nil { 191 // stop listening 192 return err 193 } 194 195 // add it to the map 196 err = core.WriteAnswer(response, q.Name, ans) 197 // if something went wrong 198 if err != nil { 199 return err 200 } 201 202 } 203 204 // return the response 205 return nil 206} 207 208// paginate returns a single page of choices given the page size, the total list of 209// possible choices, and the current selected index in the total list. 210func paginate(page int, choices []string, sel int) ([]string, int) { 211 // the number of elements to show in a single page 212 var pageSize int 213 // if the select has a specific page size 214 if page != 0 { 215 // use the specified one 216 pageSize = page 217 // otherwise the select does not have a page size 218 } else { 219 // use the package default 220 pageSize = PageSize 221 } 222 223 var start, end, cursor int 224 225 if len(choices) < pageSize { 226 // if we dont have enough options to fill a page 227 start = 0 228 end = len(choices) 229 cursor = sel 230 231 } else if sel < pageSize/2 { 232 // if we are in the first half page 233 start = 0 234 end = pageSize 235 cursor = sel 236 237 } else if len(choices)-sel-1 < pageSize/2 { 238 // if we are in the last half page 239 start = len(choices) - pageSize 240 end = len(choices) 241 cursor = sel - start 242 243 } else { 244 // somewhere in the middle 245 above := pageSize / 2 246 below := pageSize - above 247 248 cursor = pageSize / 2 249 start = sel - above 250 end = sel + below 251 } 252 253 // return the subset we care about and the index 254 return choices[start:end], cursor 255} 256