Skip to content
This repository has been archived by the owner on Apr 19, 2024. It is now read-only.

Commit

Permalink
[#96] added input suggestions (#304)
Browse files Browse the repository at this point in the history
* (feat): issue templates based on CONTRIBUTION.md

* (chore): vi does not behave the same with vim users ☹️

* (feat): render struct

* (feat): implementing controls

* tab for complete (and complete the completion)
* arrows to navigate between suggestions
* cancel last suggestion to typed answer
* keeping previous scenarios intact

* (feat): usage example

* (feat): add to readme

* (fix): suggestions

* (fix): tab and help on the same brackets

* (feat): when tab with input suggestion, select next

* (feat): select and multiselect cycle next with tab
  • Loading branch information
lucassabreu committed Nov 2, 2020
1 parent 8780050 commit 52436f4
Show file tree
Hide file tree
Showing 14 changed files with 457 additions and 44 deletions.
10 changes: 10 additions & 0 deletions .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: ''

---


16 changes: 16 additions & 0 deletions .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?**
10 changes: 10 additions & 0 deletions .github/ISSUE_TEMPLATE/others-suggestions.md
@@ -0,0 +1,10 @@
---
name: Others/suggestions
about: Suggestions and other topics
title: ''
labels: ''
assignees: ''

---


18 changes: 18 additions & 0 deletions README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -137,6 +138,23 @@ prompt := &survey.Input{
survey.AskOne(prompt, &name)
```

#### Suggestion Options

<img src="https://i.imgur.com/Q7POpA1.gif" width="800px"/>

```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

<img src="https://thumbs.gfycat.com/ImperfectShimmeringBeagle-size_restricted.gif" width="400px"/>
Expand Down
4 changes: 4 additions & 0 deletions editor_test.go
Expand Up @@ -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")
}
Expand Down
42 changes: 42 additions & 0 deletions 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)
}
155 changes: 123 additions & 32 deletions 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.
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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)

Expand All @@ -102,7 +194,6 @@ func (i *Input) Cleanup(config *PromptConfig, val interface{}) error {
InputQuestionTemplate,
InputTemplateData{
Input: *i,
Answer: val.(string),
ShowAnswer: true,
Config: config,
},
Expand Down

0 comments on commit 52436f4

Please sign in to comment.