diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.md b/.github/ISSUE_TEMPLATE/ask-for-help.md
new file mode 100644
index 00000000..644920ef
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/ask-for-help.md
@@ -0,0 +1,10 @@
+---
+name: Ask for help
+about: Suggest an idea for this project or ask for help
+title: ''
+labels: 'Help Wanted'
+assignees: ''
+
+---
+
+
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..326e0a44
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,16 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: 'Bug'
+assignees: ''
+
+---
+
+**What operating system and terminal are you using?**
+
+**An example that showcases the bug.**
+
+**What did you expect to see?**
+
+**What did you see instead?**
diff --git a/.github/ISSUE_TEMPLATE/others-suggestions.md b/.github/ISSUE_TEMPLATE/others-suggestions.md
new file mode 100644
index 00000000..6e7d6000
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/others-suggestions.md
@@ -0,0 +1,10 @@
+---
+name: Others/suggestions
+about: Suggestions and other topics
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+
diff --git a/README.md b/README.md
index 511c4140..4569ad7d 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,7 @@ func main() {
1. [Running the Prompts](#running-the-prompts)
1. [Prompts](#prompts)
1. [Input](#input)
+ 1. [Suggestion Options](#suggestion-options)
1. [Multiline](#multiline)
1. [Password](#password)
1. [Confirm](#confirm)
@@ -137,6 +138,23 @@ prompt := &survey.Input{
survey.AskOne(prompt, &name)
```
+#### Suggestion Options
+
+
+
+```golang
+file := ""
+prompt := &survey.Input{
+ Message: "inform a file to save:",
+ Suggest: func (toComplete string) []string {
+ files, _ := filepath.Glob(toComplete + "*")
+ return files
+ },
+}
+}
+survey.AskOne(prompt, &file)
+```
+
### Multiline
diff --git a/editor_test.go b/editor_test.go
index b6704370..97f1144a 100644
--- a/editor_test.go
+++ b/editor_test.go
@@ -102,6 +102,10 @@ func TestEditorRender(t *testing.T) {
}
func TestEditorPrompt(t *testing.T) {
+ if os.Getenv("SKIP_EDITOR_PROMPT_TESTS") != "" {
+ t.Skip("editor prompt tests skipped by dev")
+ }
+
if _, err := exec.LookPath("vi"); err != nil {
t.Skip("vi not found in PATH")
}
diff --git a/examples/inputfilesuggestion.go b/examples/inputfilesuggestion.go
new file mode 100644
index 00000000..8872cc4d
--- /dev/null
+++ b/examples/inputfilesuggestion.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/AlecAivazis/survey/v2"
+)
+
+func suggestFiles(toComplete string) []string {
+ files, _ := filepath.Glob(toComplete + "*")
+ return files
+}
+
+// the questions to ask
+var q = []*survey.Question{
+ {
+ Name: "file",
+ Prompt: &survey.Input{
+ Message: "Which file should be read?",
+ Suggest: suggestFiles,
+ Help: "Any file; do not need to exist yet",
+ },
+ Validate: survey.Required,
+ },
+}
+
+func main() {
+ answers := struct {
+ File string
+ }{}
+
+ // ask the question
+ err := survey.Ask(q, &answers)
+
+ if err != nil {
+ fmt.Println(err.Error())
+ return
+ }
+ // print the answers
+ fmt.Printf("File chosen %s.\n", answers.File)
+}
diff --git a/input.go b/input.go
index 407701a6..f34177fd 100644
--- a/input.go
+++ b/input.go
@@ -1,5 +1,10 @@
package survey
+import (
+ "github.com/AlecAivazis/survey/v2/core"
+ "github.com/AlecAivazis/survey/v2/terminal"
+)
+
/*
Input is a regular text input that prints each character the user types on the screen
and accepts the input with the enter key. Response type is a string.
@@ -10,18 +15,26 @@ and accepts the input with the enter key. Response type is a string.
*/
type Input struct {
Renderer
- Message string
- Default string
- Help string
+ Message string
+ Default string
+ Help string
+ Suggest func(toComplete string) []string
+ typedAnswer string
+ answer string
+ options []core.OptionAnswer
+ selectedIndex int
+ showingHelp bool
}
// data available to the templates when processing
type InputTemplateData struct {
Input
- Answer string
- ShowAnswer bool
- ShowHelp bool
- Config *PromptConfig
+ ShowAnswer bool
+ ShowHelp bool
+ Answer string
+ PageEntries []core.OptionAnswer
+ SelectedIndex int
+ Config *PromptConfig
}
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
@@ -31,11 +44,92 @@ var InputQuestionTemplate = `
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
{{- if .ShowAnswer}}
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
+{{- else if .PageEntries -}}
+ {{- .Answer}} [Use arrows to move, enter to select, type to continue]
+ {{- "\n"}}
+ {{- range $ix, $choice := .PageEntries}}
+ {{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
+ {{- $choice.Value}}
+ {{- color "reset"}}{{"\n"}}
+ {{- end}}
{{- else }}
- {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ print .Config.HelpInput }} for help]{{color "reset"}} {{end}}
+ {{- if or (and .Help (not .ShowHelp)) .Suggest }}{{color "cyan"}}[
+ {{- if and .Help (not .ShowHelp)}}{{ print .Config.HelpInput }} for help {{- if and .Suggest}}, {{end}}{{end -}}
+ {{- if and .Suggest }}{{color "cyan"}}{{ print .Config.SuggestInput }} for suggestions{{end -}}
+ ]{{color "reset"}} {{end}}
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
+ {{- .Answer -}}
{{- end}}`
+func (i *Input) OnChange(key rune, config *PromptConfig) (bool, error) {
+ if key == terminal.KeyEnter || key == '\n' {
+ if i.answer != config.HelpInput || i.Help == "" {
+ // we're done
+ return true, nil
+ } else {
+ i.answer = ""
+ i.showingHelp = true
+ }
+ } else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
+ i.answer = ""
+ } else if key == terminal.KeyEscape && i.Suggest != nil {
+ if len(i.options) > 0 {
+ i.answer = i.typedAnswer
+ }
+ i.options = nil
+ } else if key == terminal.KeyArrowUp && len(i.options) > 0 {
+ if i.selectedIndex == 0 {
+ i.selectedIndex = len(i.options) - 1
+ } else {
+ i.selectedIndex--
+ }
+ i.answer = i.options[i.selectedIndex].Value
+ } else if (key == terminal.KeyArrowDown || key == terminal.KeyTab) && len(i.options) > 0 {
+ if i.selectedIndex == len(i.options)-1 {
+ i.selectedIndex = 0
+ } else {
+ i.selectedIndex++
+ }
+ i.answer = i.options[i.selectedIndex].Value
+ } else if key == terminal.KeyTab && i.Suggest != nil {
+ options := i.Suggest(i.answer)
+ i.selectedIndex = 0
+ i.typedAnswer = i.answer
+ if len(options) > 0 {
+ i.answer = options[0]
+ if len(options) == 1 {
+ i.options = nil
+ } else {
+ i.options = core.OptionAnswerList(options)
+ }
+ }
+ } else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
+ if i.answer != "" {
+ i.answer = i.answer[0 : len(i.answer)-1]
+ }
+ } else if key >= terminal.KeySpace {
+ i.answer += string(key)
+ i.typedAnswer = i.answer
+ i.options = nil
+ }
+
+ pageSize := config.PageSize
+ opts, idx := paginate(pageSize, i.options, i.selectedIndex)
+ err := i.Render(
+ InputQuestionTemplate,
+ InputTemplateData{
+ Input: *i,
+ Answer: i.answer,
+ ShowHelp: i.showingHelp,
+ SelectedIndex: idx,
+ PageEntries: opts,
+ Config: config,
+ },
+ )
+
+ return err != nil, err
+}
+
func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
// render the template
err := i.Render(
@@ -55,41 +149,39 @@ func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
defer rr.RestoreTermMode()
cursor := i.NewCursor()
+ cursor.Hide() // hide the cursor
+ defer cursor.Show() // show the cursor when we're done
- line := []rune{}
- // get the next line
+ // start waiting for input
for {
- line, err = rr.ReadLine(0)
+ r, _, err := rr.ReadRune()
if err != nil {
- return string(line), err
+ return "", err
}
- // terminal will echo the \n so we need to jump back up one row
- cursor.Up(1)
-
- if string(line) == config.HelpInput && i.Help != "" {
- err = i.Render(
- InputQuestionTemplate,
- InputTemplateData{
- Input: *i,
- ShowHelp: true,
- Config: config,
- },
- )
- if err != nil {
- return "", err
- }
- continue
+ if r == terminal.KeyInterrupt {
+ return "", terminal.InterruptErr
+ }
+ if r == terminal.KeyEndTransmission {
+ break
+ }
+
+ b, err := i.OnChange(r, config)
+ if err != nil {
+ return "", err
+ }
+
+ if b {
+ break
}
- break
}
// if the line is empty
- if line == nil || len(line) == 0 {
+ if len(i.answer) == 0 {
// use the default value
return i.Default, err
}
- lineStr := string(line)
+ lineStr := i.answer
i.AppendRenderedText(lineStr)
@@ -102,7 +194,6 @@ func (i *Input) Cleanup(config *PromptConfig, val interface{}) error {
InputQuestionTemplate,
InputTemplateData{
Input: *i,
- Answer: val.(string),
ShowAnswer: true,
Config: config,
},
diff --git a/input_test.go b/input_test.go
index 564d15b1..52dce046 100644
--- a/input_test.go
+++ b/input_test.go
@@ -20,6 +20,8 @@ func init() {
func TestInputRender(t *testing.T) {
+ suggestFn := func(string) (s []string) { return s }
+
tests := []struct {
title string
prompt Input
@@ -41,7 +43,7 @@ func TestInputRender(t *testing.T) {
{
"Test Input answer output",
Input{Message: "What is your favorite month:"},
- InputTemplateData{Answer: "October", ShowAnswer: true},
+ InputTemplateData{ShowAnswer: true, Answer: "October"},
fmt.Sprintf("%s What is your favorite month: October\n", defaultIcons().Question.Text),
},
{
@@ -68,6 +70,47 @@ func TestInputRender(t *testing.T) {
InputTemplateData{ShowHelp: true},
fmt.Sprintf("%s This is helpful\n%s What is your favorite month: (April) ", defaultIcons().Help.Text, defaultIcons().Question.Text),
},
+ {
+ "Test Input question output with completion",
+ Input{Message: "What is your favorite month:", Suggest: suggestFn},
+ InputTemplateData{},
+ fmt.Sprintf("%s What is your favorite month: [%s for suggestions] ", defaultIcons().Question.Text, string(defaultPromptConfig().SuggestInput)),
+ },
+ {
+ "Test Input question output with suggestions and help hidden",
+ Input{Message: "What is your favorite month:", Suggest: suggestFn, Help: "This is helpful"},
+ InputTemplateData{},
+ fmt.Sprintf("%s What is your favorite month: [%s for help, %s for suggestions] ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput), string(defaultPromptConfig().SuggestInput)),
+ },
+ {
+ "Test Input question output with suggestions and default and help hidden",
+ Input{Message: "What is your favorite month:", Suggest: suggestFn, Help: "This is helpful", Default: "April"},
+ InputTemplateData{},
+ fmt.Sprintf("%s What is your favorite month: [%s for help, %s for suggestions] (April) ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput), string(defaultPromptConfig().SuggestInput)),
+ },
+ {
+ "Test Input question output with suggestions shown",
+ Input{Message: "What is your favorite month:", Suggest: suggestFn},
+ InputTemplateData{
+ Answer: "February",
+ PageEntries: core.OptionAnswerList([]string{"January", "February", "March", "etc..."}),
+ SelectedIndex: 1,
+ },
+ fmt.Sprintf(
+ "%s What is your favorite month: February [Use arrows to move, enter to select, type to continue]\n"+
+ " January\n%s February\n March\n etc...\n",
+ defaultIcons().Question.Text, defaultPromptConfig().Icons.SelectFocus.Text,
+ ),
+ },
+ {
+ "Test Input question output with suggestion complemented",
+ Input{Message: "What is your favorite month:", Suggest: suggestFn},
+ InputTemplateData{Answer: "February and"},
+ fmt.Sprintf(
+ "%s What is your favorite month: [%s for suggestions] February and",
+ defaultIcons().Question.Text, defaultPromptConfig().SuggestInput,
+ ),
+ },
}
for _, test := range tests {
@@ -95,6 +138,7 @@ func TestInputRender(t *testing.T) {
}
func TestInputPrompt(t *testing.T) {
+
tests := []PromptTest{
{
"Test Input prompt interaction",
@@ -165,6 +209,146 @@ func TestInputPrompt(t *testing.T) {
},
"R",
},
+ {
+ "Test Input prompt interaction when ask for suggestion with empty value",
+ &Input{
+ Message: "What is your favorite month?",
+ Suggest: func(string) []string {
+ return []string{"January", "February"}
+ },
+ },
+ func(c *expect.Console) {
+ c.ExpectString("What is your favorite month?")
+ c.Send(string(terminal.KeyTab))
+ c.ExpectString("January")
+ c.ExpectString("February")
+ c.SendLine("")
+ c.ExpectEOF()
+ },
+ "January",
+ },
+ {
+ "Test Input prompt interaction when ask for suggestion with some value",
+ &Input{
+ Message: "What is your favorite month?",
+ Suggest: func(string) []string {
+ return []string{"February"}
+ },
+ },
+ func(c *expect.Console) {
+ c.ExpectString("What is your favorite month?")
+ c.Send("feb")
+ c.Send(string(terminal.KeyTab))
+ c.SendLine("")
+ c.ExpectEOF()
+ },
+ "February",
+ },
+ {
+ "Test Input prompt interaction when ask for suggestion with some value, choosing the second one",
+ &Input{
+ Message: "What is your favorite month?",
+ Suggest: func(string) []string {
+ return []string{"January", "February", "March"}
+ },
+ },
+ func(c *expect.Console) {
+ c.ExpectString("What is your favorite month?")
+ c.Send(string(terminal.KeyTab))
+ c.Send(string(terminal.KeyArrowDown))
+ c.Send(string(terminal.KeyArrowDown))
+ c.SendLine("")
+ c.ExpectEOF()
+ },
+ "March",
+ },
+ {
+ "Test Input prompt interaction when ask for suggestion with some value, choosing the second one",
+ &Input{
+ Message: "What is your favorite month?",
+ Suggest: func(string) []string {
+ return []string{"January", "February", "March"}
+ },
+ },
+ func(c *expect.Console) {
+ c.ExpectString("What is your favorite month?")
+ c.Send(string(terminal.KeyTab))
+ c.Send(string(terminal.KeyArrowDown))
+ c.Send(string(terminal.KeyArrowDown))
+ c.Send(string(terminal.KeyArrowUp))
+ c.SendLine("")
+ c.ExpectEOF()
+ },
+ "February",
+ },
+ {
+ "Test Input prompt interaction when ask for suggestion, complementing it and get new suggestions",
+ &Input{
+ Message: "Where to save it?",
+ Suggest: func(complete string) []string {
+ if complete == "" {
+ return []string{"folder1/", "folder2/", "folder3/"}
+ }
+ return []string{"folder3/file1.txt", "folder3/file2.txt"}
+ },
+ },
+ func(c *expect.Console) {
+ c.ExpectString("Where to save it?")
+ c.Send(string(terminal.KeyTab))
+ c.ExpectString("folder1/")
+ c.Send(string(terminal.KeyArrowDown))
+ c.Send(string(terminal.KeyArrowDown))
+ c.Send("f")
+ c.Send(string(terminal.KeyTab))
+ c.ExpectString("folder3/file2.txt")
+ c.Send(string(terminal.KeyArrowDown))
+ c.SendLine("")
+ c.ExpectEOF()
+ },
+ "folder3/file2.txt",
+ },
+ {
+ "Test Input prompt interaction when asked suggestions, but abort suggestions",
+ &Input{
+ Message: "Wanna a suggestion?",
+ Suggest: func(string) []string {
+ return []string{"suggest1", "suggest2"}
+ },
+ },
+ func(c *expect.Console) {
+ c.ExpectString("Wanna a suggestion?")
+ c.Send("typed answer")
+ c.Send(string(terminal.KeyTab))
+ c.ExpectString("suggest1")
+ c.Send(string(terminal.KeyEscape))
+ c.ExpectString("typed answer")
+ c.SendLine("")
+ c.ExpectEOF()
+ },
+ "typed answer",
+ },
+ {
+ "Test Input prompt interaction with suggestions, when tabbed with list being shown, should select next suggestion",
+ &Input{
+ Message: "Choose the special one:",
+ Suggest: func(string) []string {
+ return []string{"suggest1", "suggest2", "special answer"}
+ },
+ },
+ func(c *expect.Console) {
+ c.ExpectString("Choose the special one:")
+ c.Send("s")
+ c.Send(string(terminal.KeyTab))
+ c.ExpectString("suggest1")
+ c.ExpectString("suggest2")
+ c.ExpectString("special answer")
+ c.Send(string(terminal.KeyTab))
+ c.Send(string(terminal.KeyTab))
+ c.SendLine("")
+ c.ExpectEOF()
+ },
+ "special answer",
+ },
}
for _, test := range tests {
diff --git a/multiselect.go b/multiselect.go
index cf0da038..0e434776 100644
--- a/multiselect.go
+++ b/multiselect.go
@@ -77,7 +77,7 @@ func (m *MultiSelect) OnChange(key rune, config *PromptConfig) {
// decrement the selected index
m.selectedIndex--
}
- } else if key == terminal.KeyArrowDown || (m.VimMode && key == 'j') {
+ } else if key == terminal.KeyTab || key == terminal.KeyArrowDown || (m.VimMode && key == 'j') {
// if we are at the bottom of the list
if m.selectedIndex == len(options)-1 {
// start at the top
diff --git a/multiselect_test.go b/multiselect_test.go
index 9c380c0d..c2fd403b 100644
--- a/multiselect_test.go
+++ b/multiselect_test.go
@@ -171,6 +171,26 @@ func TestMultiSelectPrompt(t *testing.T) {
},
[]core.OptionAnswer{core.OptionAnswer{Value: "Monday", Index: 1}},
},
+ {
+ "cycle to next when tab send",
+ &MultiSelect{
+ Message: "What days do you prefer:",
+ Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
+ },
+ func(c *expect.Console) {
+ c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]")
+ // Select Monday.
+ c.Send(string(terminal.KeyTab))
+ c.Send(" ")
+ c.Send(string(terminal.KeyArrowDown))
+ c.SendLine(" ")
+ c.ExpectEOF()
+ },
+ []core.OptionAnswer{
+ core.OptionAnswer{Value: "Monday", Index: 1},
+ core.OptionAnswer{Value: "Tuesday", Index: 2},
+ },
+ },
{
"default value as []string",
&MultiSelect{
@@ -468,7 +488,6 @@ func TestMultiSelectPrompt(t *testing.T) {
core.OptionAnswer{Value: "Saturday", Index: 6},
},
},
-
}
for _, test := range tests {
diff --git a/select.go b/select.go
index ac369ba9..bc564aa4 100644
--- a/select.go
+++ b/select.go
@@ -78,7 +78,7 @@ func (s *Select) OnChange(key rune, config *PromptConfig) bool {
return false
// if the user pressed the up arrow or 'k' to emulate vim
- } else if key == terminal.KeyArrowUp || (s.VimMode && key == 'k') && len(options) > 0 {
+ } else if (key == terminal.KeyArrowUp || (s.VimMode && key == 'k')) && len(options) > 0 {
s.useDefault = false
// if we are at the top of the list
@@ -91,7 +91,7 @@ func (s *Select) OnChange(key rune, config *PromptConfig) bool {
}
// if the user pressed down or 'j' to emulate vim
- } else if key == terminal.KeyArrowDown || (s.VimMode && key == 'j') && len(options) > 0 {
+ } else if (key == terminal.KeyTab || key == terminal.KeyArrowDown || (s.VimMode && key == 'j')) && len(options) > 0 {
s.useDefault = false
// if we are at the bottom of the list
if s.selectedIndex == len(options)-1 {
diff --git a/select_test.go b/select_test.go
index fc3b985d..c960e314 100644
--- a/select_test.go
+++ b/select_test.go
@@ -134,6 +134,22 @@ func TestSelectPrompt(t *testing.T) {
},
core.OptionAnswer{Index: 1, Value: "blue"},
},
+ {
+ "basic interaction",
+ &Select{
+ Message: "Choose a color:",
+ Options: []string{"red", "blue", "green"},
+ },
+ func(c *expect.Console) {
+ c.ExpectString("Choose a color:")
+ // Select blue.
+ c.Send(string(terminal.KeyArrowDown))
+ // Select green.
+ c.SendLine(string(terminal.KeyTab))
+ c.ExpectEOF()
+ },
+ core.OptionAnswer{Index: 2, Value: "green"},
+ },
{
"default value",
&Select{
diff --git a/survey.go b/survey.go
index ede27272..e004cf30 100644
--- a/survey.go
+++ b/survey.go
@@ -19,8 +19,9 @@ func defaultAskOptions() *AskOptions {
Err: os.Stderr,
},
PromptConfig: PromptConfig{
- PageSize: 7,
- HelpInput: "?",
+ PageSize: 7,
+ HelpInput: "?",
+ SuggestInput: "tab",
Icons: IconSet{
Error: Icon{
Text: "X",
@@ -107,11 +108,12 @@ type Question struct {
// PromptConfig holds the global configuration for a prompt
type PromptConfig struct {
- PageSize int
- Icons IconSet
- HelpInput string
- Filter func(filter string, option string, index int) bool
- KeepFilter bool
+ PageSize int
+ Icons IconSet
+ HelpInput string
+ SuggestInput string
+ Filter func(filter string, option string, index int) bool
+ KeepFilter bool
}
// Prompt is the primary interface for the objects that can take user input
diff --git a/terminal/sequences.go b/terminal/sequences.go
index a9158c63..6d9e8775 100644
--- a/terminal/sequences.go
+++ b/terminal/sequences.go
@@ -23,6 +23,7 @@ const (
SpecialKeyEnd = '\x11'
SpecialKeyDelete = '\x12'
IgnoreKey = '\000'
+ KeyTab = '\t'
)
func soundBell(out io.Writer) {