1package survey
2
3import (
4	"errors"
5
6	"github.com/AlecAivazis/survey/v2/core"
7	"github.com/AlecAivazis/survey/v2/terminal"
8)
9
10/*
11Input is a regular text input that prints each character the user types on the screen
12and accepts the input with the enter key. Response type is a string.
13
14	name := ""
15	prompt := &survey.Input{ Message: "What is your name?" }
16	survey.AskOne(prompt, &name)
17*/
18type Input struct {
19	Renderer
20	Message       string
21	Default       string
22	Help          string
23	Suggest       func(toComplete string) []string
24	answer        string
25	typedAnswer   string
26	options       []core.OptionAnswer
27	selectedIndex int
28	showingHelp   bool
29}
30
31// data available to the templates when processing
32type InputTemplateData struct {
33	Input
34	ShowAnswer    bool
35	ShowHelp      bool
36	Answer        string
37	PageEntries   []core.OptionAnswer
38	SelectedIndex int
39	Config        *PromptConfig
40}
41
42// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
43var InputQuestionTemplate = `
44{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
45{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
46{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
47{{- if .ShowAnswer}}
48  {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
49{{- else if .PageEntries -}}
50  {{- .Answer}} [Use arrows to move, enter to select, type to continue]
51  {{- "\n"}}
52  {{- range $ix, $choice := .PageEntries}}
53    {{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}}  {{end}}
54    {{- $choice.Value}}
55    {{- color "reset"}}{{"\n"}}
56  {{- end}}
57{{- else }}
58  {{- if or (and .Help (not .ShowHelp)) .Suggest }}{{color "cyan"}}[
59    {{- if and .Help (not .ShowHelp)}}{{ print .Config.HelpInput }} for help {{- if and .Suggest}}, {{end}}{{end -}}
60    {{- if and .Suggest }}{{color "cyan"}}{{ print .Config.SuggestInput }} for suggestions{{end -}}
61  ]{{color "reset"}} {{end}}
62  {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
63{{- end}}`
64
65func (i *Input) onRune(config *PromptConfig) terminal.OnRuneFn {
66	return terminal.OnRuneFn(func(key rune, line []rune) ([]rune, bool, error) {
67		if i.options != nil && (key == terminal.KeyEnter || key == '\n') {
68			return []rune(i.answer), true, nil
69		} else if i.options != nil && key == terminal.KeyEscape {
70			i.answer = i.typedAnswer
71			i.options = nil
72		} else if key == terminal.KeyArrowUp && len(i.options) > 0 {
73			if i.selectedIndex == 0 {
74				i.selectedIndex = len(i.options) - 1
75			} else {
76				i.selectedIndex--
77			}
78			i.answer = i.options[i.selectedIndex].Value
79		} else if (key == terminal.KeyArrowDown || key == terminal.KeyTab) && len(i.options) > 0 {
80			if i.selectedIndex == len(i.options)-1 {
81				i.selectedIndex = 0
82			} else {
83				i.selectedIndex++
84			}
85			i.answer = i.options[i.selectedIndex].Value
86		} else if key == terminal.KeyTab && i.Suggest != nil {
87			i.answer = string(line)
88			i.typedAnswer = i.answer
89			options := i.Suggest(i.answer)
90			i.selectedIndex = 0
91			if len(options) == 0 {
92				return line, false, nil
93			}
94
95			i.answer = options[0]
96			if len(options) == 1 {
97				i.typedAnswer = i.answer
98				i.options = nil
99			} else {
100				i.options = core.OptionAnswerList(options)
101			}
102		} else {
103			if i.options == nil {
104				return line, false, nil
105			}
106
107			if key >= terminal.KeySpace {
108				i.answer += string(key)
109			}
110			i.typedAnswer = i.answer
111
112			i.options = nil
113		}
114
115		pageSize := config.PageSize
116		opts, idx := paginate(pageSize, i.options, i.selectedIndex)
117		err := i.Render(
118			InputQuestionTemplate,
119			InputTemplateData{
120				Input:         *i,
121				Answer:        i.answer,
122				ShowHelp:      i.showingHelp,
123				SelectedIndex: idx,
124				PageEntries:   opts,
125				Config:        config,
126			},
127		)
128
129		if err == nil {
130			err = readLineAgain
131		}
132
133		return []rune(i.typedAnswer), true, err
134	})
135}
136
137var readLineAgain = errors.New("read line again")
138
139func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
140	// render the template
141	err := i.Render(
142		InputQuestionTemplate,
143		InputTemplateData{
144			Input:    *i,
145			Config:   config,
146			ShowHelp: i.showingHelp,
147		},
148	)
149	if err != nil {
150		return "", err
151	}
152
153	// start reading runes from the standard in
154	rr := i.NewRuneReader()
155	rr.SetTermMode()
156	defer rr.RestoreTermMode()
157
158	cursor := i.NewCursor()
159	if !config.ShowCursor {
160		cursor.Hide()       // hide the cursor
161		defer cursor.Show() // show the cursor when we're done
162	}
163
164	var line []rune
165
166	for {
167		if i.options != nil {
168			line = []rune{}
169		}
170
171		line, err = rr.ReadLineWithDefault(0, line, i.onRune(config))
172		if err == readLineAgain {
173			continue
174		}
175
176		if err != nil {
177			return "", err
178		}
179
180		break
181	}
182
183	i.answer = string(line)
184	// readline print an empty line, go up before we render the follow up
185	cursor.Up(1)
186
187	// if we ran into the help string
188	if i.answer == config.HelpInput && i.Help != "" {
189		// show the help and prompt again
190		i.showingHelp = true
191		return i.Prompt(config)
192	}
193
194	// if the line is empty
195	if len(i.answer) == 0 {
196		// use the default value
197		return i.Default, err
198	}
199
200	lineStr := i.answer
201
202	i.AppendRenderedText(lineStr)
203
204	// we're done
205	return lineStr, err
206}
207
208func (i *Input) Cleanup(config *PromptConfig, val interface{}) error {
209	// use the default answer when cleaning up the prompt if necessary
210	ans := i.answer
211	if ans == "" && i.Default != "" {
212		ans = i.Default
213	}
214
215	// render the cleanup
216	return i.Render(
217		InputQuestionTemplate,
218		InputTemplateData{
219			Input:      *i,
220			ShowAnswer: true,
221			Config:     config,
222			Answer:     ans,
223		},
224	)
225}
226