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

Commit

Permalink
(fix): arrow key behavior on input prompt (#361)
Browse files Browse the repository at this point in the history
* first pass at input using RuneReader.ReadLine with suggestions

* fix newline from readline

* better callback name

* (fix): skip tests with editor

* (feat): allow prompt callback on rune input and initial input

* (fix): fallback to ReadLine when not using "auto complete"

* (feat): tests input navagation

* (fix): suggestions can be dismissed

* (fix): editor blocking testing

Co-authored-by: Alec Aivazis <[email protected]>
  • Loading branch information
lucassabreu and AlecAivazis authored Jul 2, 2021
1 parent c5bc9bf commit 3ec04a9
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 98 deletions.
4 changes: 2 additions & 2 deletions _tasks.hcl
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
task "install-deps" {
description = "Install all of package dependencies"
pipeline = [
"go get {{.files}}",
"go get -v {{.files}}",
]
}

task "tests" {
description = "Run the test suite"
command = "go test {{.files}}"
command = "go test -v {{.files}}"
environment = {
GOFLAGS = "-mod=vendor"
}
Expand Down
12 changes: 4 additions & 8 deletions editor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,6 @@ 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 All @@ -122,7 +118,7 @@ func TestEditorPrompt(t *testing.T) {
c.SendLine("")
go c.ExpectEOF()
time.Sleep(time.Millisecond)
c.Send("iAdd editor prompt tests\x1b")
c.Send("ccAdd editor prompt tests\x1b")
c.SendLine(":wq!")
},
"Add editor prompt tests\n",
Expand Down Expand Up @@ -155,7 +151,7 @@ func TestEditorPrompt(t *testing.T) {
c.SendLine("")
go c.ExpectEOF()
time.Sleep(time.Millisecond)
c.Send("iAdd editor prompt tests\x1b")
c.Send("ccAdd editor prompt tests\x1b")
c.SendLine(":wq!")
},
"Add editor prompt tests\n",
Expand Down Expand Up @@ -196,7 +192,7 @@ func TestEditorPrompt(t *testing.T) {
c.SendLine("")
go c.ExpectEOF()
time.Sleep(time.Millisecond)
c.Send("iAdd editor prompt tests\x1b")
c.Send("ccAdd editor prompt tests\x1b")
c.SendLine(":wq!")
},
"Add editor prompt tests\n",
Expand Down Expand Up @@ -230,7 +226,7 @@ func TestEditorPrompt(t *testing.T) {
c.SendLine("")
go c.ExpectEOF()
time.Sleep(time.Millisecond)
c.Send("iAdd editor prompt tests\x1b")
c.Send("ccAdd editor prompt tests\x1b")
c.SendLine(":wq!")
},
"Add editor prompt tests\n",
Expand Down
161 changes: 87 additions & 74 deletions input.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package survey

import (
"errors"

"github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/terminal"
)
Expand All @@ -19,8 +21,8 @@ type Input struct {
Default string
Help string
Suggest func(toComplete string) []string
typedAnswer string
answer string
typedAnswer string
options []core.OptionAnswer
selectedIndex int
showingHelp bool
Expand Down Expand Up @@ -58,86 +60,90 @@ 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) 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 {
func (i *Input) onRune(config *PromptConfig) terminal.OnRuneFn {
return terminal.OnRuneFn(func(key rune, line []rune) ([]rune, bool, error) {
if i.options != nil && (key == terminal.KeyEnter || key == '\n') {
return []rune(i.answer), true, nil
} else if i.options != nil && key == terminal.KeyEscape {
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.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 {
i.answer = string(line)
i.typedAnswer = i.answer
options := i.Suggest(i.answer)
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 {
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

i.options = nil
}
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
if i.answer != "" {
runeAnswer := []rune(i.answer)
i.answer = string(runeAnswer[0 : len(runeAnswer)-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,
},
)
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 err != nil, err
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(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
Config: config,
Input: *i,
Config: config,
ShowHelp: i.showingHelp,
},
)
if err != nil {
Expand All @@ -155,27 +161,34 @@ func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
defer cursor.Show() // show the cursor when we're done
}

// start waiting for input
var line []rune

for {
r, _, err := rr.ReadRune()
if err != nil {
return "", err
if i.options != nil {
line = []rune{}
}
if r == terminal.KeyInterrupt {
return "", terminal.InterruptErr
}
if r == terminal.KeyEndTransmission {
break

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

b, err := i.OnChange(r, config)
if err != nil {
return "", err
}

if b {
break
}
break
}

i.answer = string(line)
// readline print an empty line, go up before we render the follow up
cursor.Up(1)

// if we ran into the help string
if i.answer == config.HelpInput && i.Help != "" {
// show the help and prompt again
i.showingHelp = true
return i.Prompt(config)
}

// if the line is empty
Expand Down
57 changes: 48 additions & 9 deletions input_test.go
Original file line number Diff line number Diff line change
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 Expand Up @@ -377,6 +368,54 @@ func TestInputPrompt(t *testing.T) {
},
"special answer",
},
{
"Test Input prompt must allow moving cursor using right and left arrows",
&Input{Message: "Filename to save:"},
func(c *expect.Console) {
c.ExpectString("Filename to save:")
c.Send("essay.txt")
c.Send(string(terminal.KeyArrowLeft))
c.Send(string(terminal.KeyArrowLeft))
c.Send(string(terminal.KeyArrowLeft))
c.Send(string(terminal.KeyArrowLeft))
c.Send("_final")
c.Send(string(terminal.KeyArrowRight))
c.Send(string(terminal.KeyArrowRight))
c.Send(string(terminal.KeyArrowRight))
c.Send(string(terminal.KeyArrowRight))
c.Send(string(terminal.KeyBackspace))
c.Send(string(terminal.KeyBackspace))
c.Send(string(terminal.KeyBackspace))
c.Send("md")
c.Send(string(terminal.KeyArrowLeft))
c.Send(string(terminal.KeyArrowLeft))
c.Send(string(terminal.KeyArrowLeft))
c.SendLine("2")
c.ExpectEOF()
},
"essay_final2.md",
},
{
"Test Input prompt must allow moving cursor using right and left arrows, even after suggestions",
&Input{Message: "Filename to save:", Suggest: func(string) []string { return []string{".txt", ".csv", ".go"} }},
func(c *expect.Console) {
c.ExpectString("Filename to save:")
c.Send(string(terminal.KeyTab))
c.ExpectString(".txt")
c.ExpectString(".csv")
c.ExpectString(".go")
c.Send(string(terminal.KeyTab))
c.Send(string(terminal.KeyArrowLeft))
c.Send(string(terminal.KeyArrowLeft))
c.Send(string(terminal.KeyArrowLeft))
c.Send(string(terminal.KeyArrowLeft))
c.Send(string(terminal.KeyArrowLeft))
c.Send("newtable")
c.SendLine("")
c.ExpectEOF()
},
"newtable.csv",
},
}

for _, test := range tests {
Expand Down
6 changes: 3 additions & 3 deletions survey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func TestAsk(t *testing.T) {
c.ExpectString("Edit git commit message [Enter to launch editor]")
c.SendLine("")
time.Sleep(time.Millisecond)
c.Send("iAdd editor prompt tests\x1b")
c.Send("ccAdd editor prompt tests\x1b")
c.SendLine(":wq!")

// Editor validated
Expand All @@ -221,7 +221,7 @@ func TestAsk(t *testing.T) {
c.SendLine("")
time.Sleep(time.Millisecond)
c.ExpectString("first try")
c.Send("ccAdd editor prompt tests\x1b")
c.Send("ccAdd editor prompt tests, but validated\x1b")
c.SendLine(":wq!")

// Input
Expand Down Expand Up @@ -253,7 +253,7 @@ func TestAsk(t *testing.T) {
map[string]interface{}{
"pizza": true,
"commit-message": "Add editor prompt tests\n",
"commit-message-validated": "Add editor prompt tests\n",
"commit-message-validated": "Add editor prompt tests, but validated\n",
"name": "Johnny Appleseed",
/* TODO
"day": []string{"Monday", "Wednesday"},
Expand Down
Loading

0 comments on commit 3ec04a9

Please sign in to comment.