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