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