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