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

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
lucassabreu committed Jun 29, 2021
1 parent b852cfd commit 6e10ece
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Expand Up @@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v2

- name: Installer runner
run: go get -v github.com/alecaivazis/run
run: go test
env:
GO111MODULE: off

Expand Down
9 changes: 9 additions & 0 deletions examples/inputfilesuggestion.go
Expand Up @@ -14,6 +14,13 @@ func suggestFiles(toComplete string) []string {

// the questions to ask
var q = []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{
Message: "What is your name?",
},
Validate: survey.Required,
},
{
Name: "file",
Prompt: &survey.Input{
Expand All @@ -26,7 +33,9 @@ var q = []*survey.Question{
}

func main() {

answers := struct {
Name string
File string
}{}

Expand Down
111 changes: 74 additions & 37 deletions input.go
@@ -1,6 +1,8 @@
package survey

import (
"errors"

"github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/terminal"
)
Expand Down Expand Up @@ -58,46 +60,83 @@ var InputQuestionTemplate = `
{{- 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) OnSpecialKey(config *PromptConfig) func(rune) bool {
return func(key rune) bool {
if key == terminal.KeyArrowUp && len(i.options) > 0 {
func (i *Input) OnSpecialKey(config *PromptConfig) terminal.OnRuneFn {
return terminal.OnRuneFn(func(key rune, line []rune) ([]rune, bool, error) {
if i.Suggest == nil {
return line, false, nil
}

if i.options != nil && (key == terminal.KeyEnter || key == '\n') {
return []rune(i.answer), true, 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
return true
} 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
return true
} else if key == terminal.KeyTab && i.Suggest != nil {
} else if key == terminal.KeyTab {
i.answer = string(line)
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)
}
if len(options) == 0 {
return line, false, nil
}

i.answer = options[0]
if len(options) == 1 {
i.typedAnswer = i.answer
i.options = nil
} else {
i.options = core.OptionAnswerList(options)
}
} else {
if i.options == nil {
return line, false, nil
}

if key >= terminal.KeySpace {
i.answer += string(key)
i.typedAnswer = i.answer
}
return true

i.options = nil
}

return false
}
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,
},
)

if err == nil {
err = readLineAgain
}

return []rune(i.typedAnswer), true, err
})
}

var readLineAgain = errors.New("read line again")

func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
// render the template
err := i.Render(
Expand All @@ -123,10 +162,25 @@ func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
defer cursor.Show() // show the cursor when we're done
}

line, err := rr.ReadLine(0, i.OnSpecialKey(config))
if err != nil {
return "", err
var line []rune

for {
if i.options != nil {
line = []rune{}
}

line, err = rr.ReadLineWithDefault(0, line, i.OnSpecialKey(config))
if err == readLineAgain {
continue
}

if err != nil {
return "", err
}

break
}

i.answer = string(line)
// readline print an empty line, go up before we render the follow up
cursor.Up(1)
Expand All @@ -138,23 +192,6 @@ func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
return i.Prompt(config)
}

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,
},
)
if err != nil {
return "", err
}

// if the line is empty
if len(i.answer) == 0 {
// use the default value
Expand Down
9 changes: 0 additions & 9 deletions input_test.go
Expand Up @@ -102,15 +102,6 @@ func TestInputRender(t *testing.T) {
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 {
Expand Down
74 changes: 50 additions & 24 deletions survey_test.go
Expand Up @@ -2,6 +2,8 @@ package survey

import (
"fmt"
"os"
"os/exec"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -132,6 +134,23 @@ func TestPagination_lastHalf(t *testing.T) {
}

func TestAsk(t *testing.T) {

_, err := exec.LookPath("vi")
noEditor := err != nil || os.Getenv("SKIP_EDITOR_PROMPT_TESTS") != ""

filter := func(qs []*Question) []*Question { return qs }
if noEditor {
filter = func(qs []*Question) []*Question {
r := make([]*Question, 0)
for _, q := range qs {
if _, ok := q.Prompt.(*Editor); !ok {
r = append(r, q)
}
}
return r
}
}

tests := []struct {
name string
questions []*Question
Expand All @@ -140,7 +159,7 @@ func TestAsk(t *testing.T) {
}{
{
"Test Ask for all prompts",
[]*Question{
filter([]*Question{
{
Name: "pizza",
Prompt: &Confirm{
Expand Down Expand Up @@ -196,33 +215,35 @@ func TestAsk(t *testing.T) {
Options: []string{"red", "blue", "green", "yellow"},
},
},
},
}),
func(c *expect.Console) {
// Confirm
c.ExpectString("Is pizza your favorite food? (y/N)")
c.SendLine("Y")

// Editor
c.ExpectString("Edit git commit message [Enter to launch editor]")
c.SendLine("")
time.Sleep(time.Millisecond)
c.Send("iAdd editor prompt tests\x1b")
c.SendLine(":wq!")

// Editor validated
c.ExpectString("Edit git commit message [Enter to launch editor]")
c.SendLine("")
time.Sleep(time.Millisecond)
c.Send("i invalid input first try\x1b")
c.SendLine(":wq!")
time.Sleep(time.Millisecond)
c.ExpectString("invalid error message")
c.ExpectString("Edit git commit message [Enter to launch editor]")
c.SendLine("")
time.Sleep(time.Millisecond)
c.ExpectString("first try")
c.Send("ccAdd editor prompt tests\x1b")
c.SendLine(":wq!")
if !noEditor {
// Editor
c.ExpectString("Edit git commit message [Enter to launch editor]")
c.SendLine("")
time.Sleep(time.Millisecond)
c.Send("iAdd editor prompt tests\x1b")
c.SendLine(":wq!")

// Editor validated
c.ExpectString("Edit git commit message [Enter to launch editor]")
c.SendLine("")
time.Sleep(time.Millisecond)
c.Send("i invalid input first try\x1b")
c.SendLine(":wq!")
time.Sleep(time.Millisecond)
c.ExpectString("invalid error message")
c.ExpectString("Edit git commit message [Enter to launch editor]")
c.SendLine("")
time.Sleep(time.Millisecond)
c.ExpectString("first try")
c.Send("iAdd editor prompt tests\x1b")
c.SendLine(":wq!")
}

// Input
c.ExpectString("What is your name?")
Expand Down Expand Up @@ -311,11 +332,16 @@ func TestAsk(t *testing.T) {
// Capture range variable.
test := test
t.Run(test.name, func(t *testing.T) {
expectedAnswers := make(map[string]interface{}, 0)
for _, q := range test.questions {
expectedAnswers[q.Name] = test.expected[q.Name]
}

answers := make(map[string]interface{})
RunTest(t, test.procedure, func(stdio terminal.Stdio) error {
return Ask(test.questions, &answers, WithStdio(stdio.In, stdio.Out, stdio.Err))
})
require.Equal(t, test.expected, answers)
require.Equal(t, expectedAnswers, answers)
})
}
}
Expand Down
36 changes: 31 additions & 5 deletions terminal/runereader.go
Expand Up @@ -31,7 +31,13 @@ func (rr *RuneReader) printChar(char rune, mask rune) {
}
}

func (rr *RuneReader) ReadLine(mask rune, onRunes ...func(rune) bool) ([]rune, error) {
type OnRuneFn func(rune, []rune) ([]rune, bool, error)

func (rr *RuneReader) ReadLine(mask rune, onRunes ...OnRuneFn) ([]rune, error) {
return rr.ReadLineWithDefault(mask, []rune{}, onRunes...)
}

func (rr *RuneReader) ReadLineWithDefault(mask rune, d []rune, onRunes ...OnRuneFn) ([]rune, error) {
line := []rune{}
// we only care about horizontal displacements from the origin so start counting at 0
index := 0
Expand All @@ -41,11 +47,26 @@ func (rr *RuneReader) ReadLine(mask rune, onRunes ...func(rune) bool) ([]rune, e
Out: rr.stdio.Out,
}

onRune := func(r rune, line []rune) ([]rune, bool, error) {
return line, false, nil
}

// if the user pressed a key the caller was interested in capturing
if len(onRunes) > 0 {
onRune = onRunes[0]
}

// we get the terminal width and height (if resized after this point the property might become invalid)
terminalSize, _ := cursor.Size(rr.Buffer())
// we set the current location of the cursor once
cursorCurrent, _ := cursor.Location(rr.Buffer())

if len(d) > 0 {
index = len(d)
fmt.Fprint(rr.stdio.Out, string(d))
line = d
}

for {
// wait for some input
r, _, err := rr.ReadRune()
Expand All @@ -55,10 +76,15 @@ func (rr *RuneReader) ReadLine(mask rune, onRunes ...func(rune) bool) ([]rune, e
// increment cursor location
cursorCurrent.X++

// if the user pressed a key the caller was interested in capturing
if len(onRunes) > 0 && onRunes[0](r) {
return line, nil
}
if line, stop, err := onRune(r, line); stop || err != nil {
if stop {
return line, err
}

cursor.Back(index)
fmt.Fprint(rr.stdio.Out, string(d))
}

// if the user pressed enter or some other newline/termination like ctrl+d
if r == '\r' || r == '\n' || r == KeyEndTransmission {
// delete what's printed out on the console screen (cleanup)
Expand Down

0 comments on commit 6e10ece

Please sign in to comment.