├── .travis.yml
├── LICENSE
├── README.md
├── _tasks.yml
├── confirm.go
├── confirm_test.go
├── core
├── renderer.go
├── template.go
├── write.go
└── write_test.go
├── editor.go
├── editor_test.go
├── examples
├── longlist.go
├── map.go
├── simple.go
└── validation.go
├── go.mod
├── input.go
├── input_test.go
├── multiselect.go
├── multiselect_test.go
├── password.go
├── password_test.go
├── select.go
├── select_test.go
├── survey.go
├── survey_test.go
├── terminal
├── cursor.go
├── cursor_windows.go
├── display.go
├── display_posix.go
├── display_windows.go
├── error.go
├── output.go
├── output_windows.go
├── print.go
├── runereader.go
├── runereader_bsd.go
├── runereader_linux.go
├── runereader_posix.go
├── runereader_windows.go
├── sequences.go
├── syscall_windows.go
└── terminal.go
├── tests
├── README.md
├── ask.go
├── autoplay
│ ├── ask.go
│ ├── confirm.go
│ ├── doubleSelect.go
│ ├── help.go
│ ├── input.go
│ ├── multiselect.go
│ ├── password.go
│ ├── select.go
│ └── selectThenInput.go
├── confirm.go
├── doubleSelect.go
├── editor.go
├── help.go
├── input.go
├── longSelect.go
├── multiselect.go
├── password.go
├── select.go
├── selectThenInput.go
└── util
│ └── test.go
├── validate.go
└── validate_test.go
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go_import_path: github.com/tj/survey
4 |
5 | before_install:
6 | - go get github.com/AlecAivazis/run
7 |
8 | install:
9 | - run install-deps
10 |
11 | script:
12 | - run tests
13 | # - run autoplay-tests
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Alec Aivazis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Survey
2 | [](https://travis-ci.org/AlecAivazis/survey)
3 | [](https://godoc.org/github.com/tj/survey)
4 |
5 | A library for building interactive prompts. Heavily inspired by the great [inquirer.js](https://github.com/SBoudrias/Inquirer.js/).
6 |
7 | 
8 |
9 | ```go
10 | package main
11 |
12 | import (
13 | "fmt"
14 | "github.com/tj/survey"
15 | )
16 |
17 | // the questions to ask
18 | var qs = []*survey.Question{
19 | {
20 | Name: "name",
21 | Prompt: &survey.Input{Message: "What is your name?"},
22 | Validate: survey.Required,
23 | },
24 | {
25 | Name: "color",
26 | Prompt: &survey.Select{
27 | Message: "Choose a color:",
28 | Options: []string{"red", "blue", "green"},
29 | Default: "red",
30 | },
31 | },
32 | {
33 | Name: "age",
34 | Prompt: &survey.Input{Message: "How old are you?"},
35 | },
36 | }
37 |
38 | func main() {
39 | // the answers will be written to this struct
40 | answers := struct {
41 | Name string // survey will match the question and field names
42 | FavoriteColor string `survey:"color"` // or you can tag fields to match a specific name
43 | Age int // if the types don't match exactly, survey will try to convert for you
44 | }{}
45 |
46 | // perform the questions
47 | err := survey.Ask(qs, &answers)
48 | if err != nil {
49 | fmt.Println(err.Error())
50 | return
51 | }
52 |
53 | fmt.Printf("%s chose %s.", answers.Name, answers.FavoriteColor)
54 | }
55 | ```
56 |
57 | ## Table of Contents
58 |
59 | 1. [Examples](#examples)
60 | 1. [Prompts](#prompts)
61 | 1. [Input](#input)
62 | 1. [Password](#password)
63 | 1. [Confirm](#confirm)
64 | 1. [Select](#select)
65 | 1. [MultiSelect](#multiselect)
66 | 1. [Editor](#editor)
67 | 1. [Validation](#validation)
68 | 1. [Built-in Validators](#built-in-validators)
69 | 1. [Help Text](#help-text)
70 | 1. [Changing the input rune](#changing-the-input-run)
71 | 1. [Custom Types](#custom-types)
72 | 1. [Customizing Output](#customizing-output)
73 | 1. [Versioning](#versioning)
74 |
75 | ## Examples
76 |
77 | Examples can be found in the `examples/` directory. Run them
78 | to see basic behavior:
79 |
80 | ```bash
81 | go get github.com/tj/survey
82 |
83 | # ... navigate to the repo in your GOPATH
84 |
85 | go run examples/simple.go
86 | go run examples/validation.go
87 | ```
88 |
89 | ## Prompts
90 |
91 | ### Input
92 |
93 |
94 |
95 | ```golang
96 | name := ""
97 | prompt := &survey.Input{
98 | Message: "ping",
99 | }
100 | survey.AskOne(prompt, &name, nil)
101 | ```
102 |
103 |
104 | ### Password
105 |
106 |
107 |
108 | ```golang
109 | password := ""
110 | prompt := &survey.Password{
111 | Message: "Please type your password",
112 | }
113 | survey.AskOne(prompt, &password, nil)
114 | ```
115 |
116 |
117 | ### Confirm
118 |
119 |
120 |
121 | ```golang
122 | name := false
123 | prompt := &survey.Confirm{
124 | Message: "Do you like pie?",
125 | }
126 | survey.AskOne(prompt, &name, nil)
127 | ```
128 |
129 |
130 | ### Select
131 |
132 |
133 |
134 | ```golang
135 | color := ""
136 | prompt := &survey.Select{
137 | Message: "Choose a color:",
138 | Options: []string{"red", "blue", "green"},
139 | }
140 | survey.AskOne(prompt, &color, nil)
141 | ```
142 |
143 | By default, the select prompt is limited to showing 7 options at a time
144 | and will paginate lists of options longer than that. To increase, you can either
145 | change the global `survey.PageCount`, or set the `PageSize` field on the prompt:
146 |
147 | ```golang
148 | prompt := &survey.Select{..., PageSize: 10}
149 | ```
150 |
151 | ### MultiSelect
152 |
153 |
154 |
155 | ```golang
156 | days := []string{}
157 | prompt := &survey.MultiSelect{
158 | Message: "What days do you prefer:",
159 | Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
160 | }
161 | survey.AskOne(prompt, &days, nil)
162 | ```
163 |
164 | By default, the MultiSelect prompt is limited to showing 7 options at a time
165 | and will paginate lists of options longer than that. To increase, you can either
166 | change the global `survey.PageCount`, or set the `PageSize` field on the prompt:
167 |
168 | ```golang
169 | prompt := &survey.MultiSelect{..., PageSize: 10}
170 | ```
171 |
172 | ### Editor
173 |
174 | Launches the user's preferred editor (defined by the $EDITOR environment variable) on a
175 | temporary file. Once the user exits their editor, the contents of the temporary file are read in as
176 | the result. If neither of those are present, notepad (on Windows) or vim (Linux or Mac) is used.
177 |
178 |
179 | ## Validation
180 |
181 | Validating individual responses for a particular question can be done by defining a
182 | `Validate` field on the `survey.Question` to be validated. This function takes an
183 | `interface{}` type and returns an error to show to the user, prompting them for another
184 | response:
185 |
186 | ```golang
187 | q := &survey.Question{
188 | Prompt: &survey.Input{Message: "Hello world validation"},
189 | Validate: func (val interface{}) error {
190 | // since we are validating an Input, the assertion will always succeed
191 | if str, ok := val.(string) ; ok && len(str) > 10 {
192 | return errors.New("This response cannot be longer than 10 characters.")
193 | }
194 | }
195 | }
196 | ```
197 |
198 | ### Built-in Validators
199 |
200 | `survey` comes prepackaged with a few validators to fit common situations. Currently these
201 | validators include:
202 |
203 | | name | valid types | description |
204 | |--------------|-----------------|---------------------------------------------------------------|
205 | | Required | any | Rejects zero values of the response type |
206 | | MinLength(n) | string | Enforces that a response is at least the given length |
207 | | MaxLength(n) | string | Enforces that a response is no longer than the given length |
208 |
209 | ## Help Text
210 |
211 | All of the prompts have a `Help` field which can be defined to provide more information to your users:
212 |
213 |
214 |
215 | ```golang
216 | &survey.Input{
217 | Message: "What is your phone number:",
218 | Help: "Phone number should include the area code",
219 | }
220 | ```
221 |
222 | ### Changing the input rune
223 |
224 | In some situations, `?` is a perfectly valid response. To handle this, you can change the rune that survey
225 | looks for by setting the `HelpInputRune` variable in `survey/core`:
226 |
227 | ```golang
228 |
229 | import (
230 | "github.com/tj/survey"
231 | surveyCore "github.com/tj/survey/core"
232 | )
233 |
234 | number := ""
235 | prompt := &survey.Input{
236 | Message: "If you have this need, please give me a reasonable message.",
237 | Help: "I couldn't come up with one.",
238 | }
239 |
240 | surveyCore.HelpInputRune = '^'
241 |
242 | survey.AskOne(prompt, &number, nil)
243 | ```
244 |
245 | ## Custom Types
246 |
247 | survey will assign prompt answers to your custom types if they implement this interface:
248 |
249 | ```golang
250 | type settable interface {
251 | WriteAnswer(field string, value interface{}) error
252 | }
253 | ```
254 |
255 | Here is an example how to use them:
256 |
257 | ```golang
258 | type MyValue struct {
259 | value string
260 | }
261 | func (my *MyValue) WriteAnswer(name string, value interface{}) error {
262 | my.value = value.(string)
263 | }
264 |
265 | myval := MyValue{}
266 | survey.AskOne(
267 | &survey.Input{
268 | Message: "Enter something:",
269 | },
270 | &myval,
271 | nil,
272 | )
273 | ```
274 |
275 | ## Customizing Output
276 |
277 | Customizing the icons and various parts of survey can easily be done by setting the following variables
278 | in `survey/core`:
279 |
280 | | name | default | description |
281 | |---------------------|----------------|-------------------------------------------------------------------|
282 | | ErrorIcon | ✘ | Before an error |
283 | | HelpIcon | ⓘ | Before help text |
284 | | QuestionIcon | ? | Before the message of a prompt |
285 | | SelectFocusIcon | ❯ | Marks the current focus in `Select` and `MultiSelect` prompts |
286 | | MarkedOptionIcon | ◉ | Marks a chosen selection in a `MultiSelect` prompt |
287 | | UnmarkedOptionIcon | ◯ | Marks an unselected option in a `MultiSelect` prompt |
288 |
289 | ## Versioning
290 |
291 | This project tries to maintain semantic GitHub releases as closely as possible. And relies on [gopkg.in](http://labix.org/gopkg.in)
292 | to maintain those releasees. Importing v1 of survey could look something like
293 |
294 | ```golang
295 | package main
296 |
297 | import "github.com/tj/survey"
298 | ```
299 |
--------------------------------------------------------------------------------
/_tasks.yml:
--------------------------------------------------------------------------------
1 | autoplay-tests:
2 | summary: Replaying interactive tests
3 | command: |-
4 | cd tests
5 | set -e
6 | for test in autoplay/*.go; do
7 | echo "==> Running $test"
8 | go run $test
9 | done
10 |
11 | install-deps:
12 | summary: Install all of package dependencies
13 | command: |-
14 | go get -t {{.files}}
15 | # for autoplay tests
16 | go get github.com/kr/pty
17 |
18 | tests:
19 | summary: Run the test suite
20 | command: go test {{.files}}
21 |
22 | variables:
23 | files: '$(go list -v ./... | grep -iEv "tests|examples")'
24 |
--------------------------------------------------------------------------------
/confirm.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "regexp"
7 |
8 | "github.com/tj/survey/core"
9 | "github.com/tj/survey/terminal"
10 | )
11 |
12 | // Confirm is a regular text input that accept yes/no answers. Response type is a bool.
13 | type Confirm struct {
14 | core.Renderer
15 | Message string
16 | Default bool
17 | Help string
18 | }
19 |
20 | // data available to the templates when processing
21 | type ConfirmTemplateData struct {
22 | Confirm
23 | Answer string
24 | ShowHelp bool
25 | }
26 |
27 | // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
28 | var ConfirmQuestionTemplate = `
29 | {{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
30 | {{- color "default"}} {{ .Message }} {{color "reset"}}
31 | {{- if .Answer}}
32 | {{- color "gray"}}{{.Answer}}{{color "reset"}}{{"\n"}}
33 | {{- else }}
34 | {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}}
35 | {{- color "white"}}{{if .Default}}(Y/n) {{else}}(y/N) {{end}}{{color "reset"}}
36 | {{- end}}`
37 |
38 | // the regex for answers
39 | var (
40 | yesRx = regexp.MustCompile("^(?i:y(?:es)?)$")
41 | noRx = regexp.MustCompile("^(?i:n(?:o)?)$")
42 | )
43 |
44 | func yesNo(t bool) string {
45 | if t {
46 | return "Yes"
47 | }
48 | return "No"
49 | }
50 |
51 | func (c *Confirm) getBool(showHelp bool) (bool, error) {
52 | rr := terminal.NewRuneReader(os.Stdin)
53 | rr.SetTermMode()
54 | defer rr.RestoreTermMode()
55 | // start waiting for input
56 | for {
57 | line, err := rr.ReadLine(0)
58 | if err != nil {
59 | return false, err
60 | }
61 | // move back up a line to compensate for the \n echoed from terminal
62 | terminal.CursorPreviousLine(1)
63 | val := string(line)
64 |
65 | // get the answer that matches the
66 | var answer bool
67 | switch {
68 | case yesRx.Match([]byte(val)):
69 | answer = true
70 | case noRx.Match([]byte(val)):
71 | answer = false
72 | case val == "":
73 | answer = c.Default
74 | case val == string(core.HelpInputRune) && c.Help != "":
75 | err := c.Render(
76 | ConfirmQuestionTemplate,
77 | ConfirmTemplateData{Confirm: *c, ShowHelp: true},
78 | )
79 | if err != nil {
80 | // use the default value and bubble up
81 | return c.Default, err
82 | }
83 | showHelp = true
84 | continue
85 | default:
86 | // we didnt get a valid answer, so print error and prompt again
87 | if err := c.Error(fmt.Errorf("%q is not a valid answer, please try again.", val)); err != nil {
88 | return c.Default, err
89 | }
90 | err := c.Render(
91 | ConfirmQuestionTemplate,
92 | ConfirmTemplateData{Confirm: *c, ShowHelp: showHelp},
93 | )
94 | if err != nil {
95 | // use the default value and bubble up
96 | return c.Default, err
97 | }
98 | continue
99 | }
100 | return answer, nil
101 | }
102 | // should not get here
103 | return c.Default, nil
104 | }
105 |
106 | /*
107 | Prompt prompts the user with a simple text field and expects a reply followed
108 | by a carriage return.
109 |
110 | likesPie := false
111 | prompt := &survey.Confirm{ Message: "What is your name?" }
112 | survey.AskOne(prompt, &likesPie, nil)
113 | */
114 | func (c *Confirm) Prompt() (interface{}, error) {
115 | // render the question template
116 | err := c.Render(
117 | ConfirmQuestionTemplate,
118 | ConfirmTemplateData{Confirm: *c},
119 | )
120 | if err != nil {
121 | return "", err
122 | }
123 |
124 | // get input and return
125 | return c.getBool(false)
126 | }
127 |
128 | // Cleanup overwrite the line with the finalized formatted version
129 | func (c *Confirm) Cleanup(val interface{}) error {
130 | // if the value was previously true
131 | ans := yesNo(val.(bool))
132 | // render the template
133 | return c.Render(
134 | ConfirmQuestionTemplate,
135 | ConfirmTemplateData{Confirm: *c, Answer: ans},
136 | )
137 | }
138 |
--------------------------------------------------------------------------------
/confirm_test.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/tj/survey/core"
9 | "github.com/tj/survey/terminal"
10 | )
11 |
12 | func init() {
13 | // disable color output for all prompts to simplify testing
14 | core.DisableColor = true
15 | }
16 |
17 | func TestConfirmRender(t *testing.T) {
18 |
19 | tests := []struct {
20 | title string
21 | prompt Confirm
22 | data ConfirmTemplateData
23 | expected string
24 | }{
25 | {
26 | "Test Confirm question output with default true",
27 | Confirm{Message: "Is pizza your favorite food?", Default: true},
28 | ConfirmTemplateData{},
29 | `? Is pizza your favorite food? (Y/n) `,
30 | },
31 | {
32 | "Test Confirm question output with default false",
33 | Confirm{Message: "Is pizza your favorite food?", Default: false},
34 | ConfirmTemplateData{},
35 | `? Is pizza your favorite food? (y/N) `,
36 | },
37 | {
38 | "Test Confirm answer output",
39 | Confirm{Message: "Is pizza your favorite food?"},
40 | ConfirmTemplateData{Answer: "Yes"},
41 | "? Is pizza your favorite food? Yes\n",
42 | },
43 | {
44 | "Test Confirm with help but help message is hidden",
45 | Confirm{Message: "Is pizza your favorite food?", Help: "This is helpful"},
46 | ConfirmTemplateData{},
47 | "? Is pizza your favorite food? [? for help] (y/N) ",
48 | },
49 | {
50 | "Test Confirm help output with help message shown",
51 | Confirm{Message: "Is pizza your favorite food?", Help: "This is helpful"},
52 | ConfirmTemplateData{ShowHelp: true},
53 | `ⓘ This is helpful
54 | ? Is pizza your favorite food? (y/N) `,
55 | },
56 | }
57 |
58 | outputBuffer := bytes.NewBufferString("")
59 | terminal.Stdout = outputBuffer
60 |
61 | for _, test := range tests {
62 | outputBuffer.Reset()
63 | test.data.Confirm = test.prompt
64 | err := test.prompt.Render(
65 | ConfirmQuestionTemplate,
66 | test.data,
67 | )
68 | assert.Nil(t, err, test.title)
69 | assert.Equal(t, test.expected, outputBuffer.String(), test.title)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/core/renderer.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/tj/survey/terminal"
7 | )
8 |
9 | type Renderer struct {
10 | lineCount int
11 | errorLineCount int
12 | }
13 |
14 | var ErrorTemplate = `{{color "red"}} {{ ErrorIcon }} Sorry, your reply was invalid: {{.Error}}{{color "reset"}}
15 | `
16 |
17 | func (r *Renderer) Error(invalid error) error {
18 | // since errors are printed on top we need to reset the prompt
19 | // as well as any previous error print
20 | r.resetPrompt(r.lineCount + r.errorLineCount)
21 | // we just cleared the prompt lines
22 | r.lineCount = 0
23 | out, err := RunTemplate(ErrorTemplate, invalid)
24 | if err != nil {
25 | return err
26 | }
27 | // keep track of how many lines are printed so we can clean up later
28 | r.errorLineCount = strings.Count(out, "\n")
29 |
30 | // send the message to the user
31 | terminal.Print(out)
32 | return nil
33 | }
34 |
35 | func (r *Renderer) resetPrompt(lines int) {
36 | // clean out current line in case tmpl didnt end in newline
37 | terminal.CursorHorizontalAbsolute(0)
38 | terminal.EraseLine(terminal.ERASE_LINE_ALL)
39 | // clean up what we left behind last time
40 | for i := 0; i < lines; i++ {
41 | terminal.CursorPreviousLine(1)
42 | terminal.EraseLine(terminal.ERASE_LINE_ALL)
43 | }
44 | }
45 |
46 | func (r *Renderer) Render(tmpl string, data interface{}) error {
47 | r.resetPrompt(r.lineCount)
48 | // render the template summarizing the current state
49 | out, err := RunTemplate(tmpl, data)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | // keep track of how many lines are printed so we can clean up later
55 | r.lineCount = strings.Count(out, "\n")
56 |
57 | // print the summary
58 | terminal.Print(out)
59 |
60 | // nothing went wrong
61 | return nil
62 | }
63 |
--------------------------------------------------------------------------------
/core/template.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "bytes"
5 | "text/template"
6 |
7 | "github.com/mgutz/ansi"
8 | )
9 |
10 | var DisableColor = false
11 |
12 | var (
13 | HelpInputRune = '?'
14 |
15 | ErrorIcon = "×"
16 | HelpIcon = "ⓘ"
17 | QuestionIcon = "?"
18 |
19 | MarkedOptionIcon = "◉"
20 | UnmarkedOptionIcon = "◯"
21 |
22 | SelectFocusIcon = "❯"
23 | )
24 |
25 | var TemplateFuncs = map[string]interface{}{
26 | // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
27 | "color": func(color string) string {
28 | if DisableColor {
29 | return ""
30 | }
31 |
32 | switch color {
33 | case "default":
34 | return "\033[38;5;61m"
35 | case "red":
36 | return "\033[38;5;125m"
37 | case "gray", "grey":
38 | return "\033[38;5;102m"
39 | }
40 |
41 | return ansi.ColorCode(color)
42 | },
43 | "HelpInputRune": func() string {
44 | return string(HelpInputRune)
45 | },
46 | "ErrorIcon": func() string {
47 | return ErrorIcon
48 | },
49 | "HelpIcon": func() string {
50 | return HelpIcon
51 | },
52 | "QuestionIcon": func() string {
53 | return QuestionIcon
54 | },
55 | "MarkedOptionIcon": func() string {
56 | return MarkedOptionIcon
57 | },
58 | "UnmarkedOptionIcon": func() string {
59 | return UnmarkedOptionIcon
60 | },
61 | "SelectFocusIcon": func() string {
62 | return SelectFocusIcon
63 | },
64 | }
65 |
66 | var memoizedGetTemplate = map[string]*template.Template{}
67 |
68 | func getTemplate(tmpl string) (*template.Template, error) {
69 | if t, ok := memoizedGetTemplate[tmpl]; ok {
70 | return t, nil
71 | }
72 |
73 | t, err := template.New("prompt").Funcs(TemplateFuncs).Parse(tmpl)
74 | if err != nil {
75 | return nil, err
76 | }
77 |
78 | memoizedGetTemplate[tmpl] = t
79 | return t, nil
80 | }
81 |
82 | func RunTemplate(tmpl string, data interface{}) (string, error) {
83 | t, err := getTemplate(tmpl)
84 | if err != nil {
85 | return "", err
86 | }
87 | buf := bytes.NewBufferString("")
88 | err = t.Execute(buf, data)
89 | if err != nil {
90 | return "", err
91 | }
92 | return buf.String(), err
93 | }
94 |
--------------------------------------------------------------------------------
/core/write.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "reflect"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | // the tag used to denote the name of the question
12 | const tagName = "survey"
13 |
14 | // add a few interfaces so users can configure how the prompt values are set
15 | type settable interface {
16 | WriteAnswer(field string, value interface{}) error
17 | }
18 |
19 | func WriteAnswer(t interface{}, name string, v interface{}) (err error) {
20 | // if the field is a custom type
21 | if s, ok := t.(settable); ok {
22 | // use the interface method
23 | return s.WriteAnswer(name, v)
24 | }
25 |
26 | // the target to write to
27 | target := reflect.ValueOf(t)
28 | // the value to write from
29 | value := reflect.ValueOf(v)
30 |
31 | // make sure we are writing to a pointer
32 | if target.Kind() != reflect.Ptr {
33 | return errors.New("you must pass a pointer as the target of a Write operation")
34 | }
35 | // the object "inside" of the target pointer
36 | elem := target.Elem()
37 |
38 | // handle the special types
39 | switch elem.Kind() {
40 | // if we are writing to a struct
41 | case reflect.Struct:
42 | // get the name of the field that matches the string we were given
43 | fieldIndex, err := findFieldIndex(elem, name)
44 | // if something went wrong
45 | if err != nil {
46 | // bubble up
47 | return err
48 | }
49 | field := elem.Field(fieldIndex)
50 | // handle references to the settable interface aswell
51 | if s, ok := field.Interface().(settable); ok {
52 | // use the interface method
53 | return s.WriteAnswer(name, v)
54 | }
55 | if field.CanAddr() {
56 | if s, ok := field.Addr().Interface().(settable); ok {
57 | // use the interface method
58 | return s.WriteAnswer(name, v)
59 | }
60 | }
61 |
62 | // copy the value over to the normal struct
63 | return copy(field, value)
64 | case reflect.Map:
65 | mapType := reflect.TypeOf(t).Elem()
66 | if mapType.Key().Kind() != reflect.String || mapType.Elem().Kind() != reflect.Interface {
67 | return errors.New("answer maps must be of type map[string]interface")
68 | }
69 | mt := *t.(*map[string]interface{})
70 | mt[name] = value.Interface()
71 | return nil
72 | }
73 | // otherwise just copy the value to the target
74 | return copy(elem, value)
75 | }
76 |
77 | // BUG(AlecAivazis): the current implementation might cause weird conflicts if there are
78 | // two fields with same name that only differ by casing.
79 | func findFieldIndex(s reflect.Value, name string) (int, error) {
80 | // the type of the value
81 | sType := s.Type()
82 |
83 | // first look for matching tags so we can overwrite matching field names
84 | for i := 0; i < sType.NumField(); i++ {
85 | // the field we are current scanning
86 | field := sType.Field(i)
87 |
88 | // the value of the survey tag
89 | tag := field.Tag.Get(tagName)
90 | // if the tag matches the name we are looking for
91 | if tag != "" && tag == name {
92 | // then we found our index
93 | return i, nil
94 | }
95 | }
96 |
97 | // then look for matching names
98 | for i := 0; i < sType.NumField(); i++ {
99 | // the field we are current scanning
100 | field := sType.Field(i)
101 |
102 | // if the name of the field matches what we're looking for
103 | if strings.ToLower(field.Name) == strings.ToLower(name) {
104 | return i, nil
105 | }
106 | }
107 |
108 | // we didn't find the field
109 | return -1, fmt.Errorf("could not find field matching %v", name)
110 | }
111 |
112 | // isList returns true if the element is something we can Len()
113 | func isList(v reflect.Value) bool {
114 | switch v.Type().Kind() {
115 | case reflect.Array, reflect.Slice:
116 | return true
117 | default:
118 | return false
119 | }
120 | }
121 |
122 | // Write takes a value and copies it to the target
123 | func copy(t reflect.Value, v reflect.Value) (err error) {
124 | // if something ends up panicing we need to catch it in a deferred func
125 | defer func() {
126 | if r := recover(); r != nil {
127 | // if we paniced with an error
128 | if _, ok := r.(error); ok {
129 | // cast the result to an error object
130 | err = r.(error)
131 | } else if _, ok := r.(string); ok {
132 | // otherwise we could have paniced with a string so wrap it in an error
133 | err = errors.New(r.(string))
134 | }
135 | }
136 | }()
137 |
138 | // if we are copying from a string result to something else
139 | if v.Kind() == reflect.String && v.Type() != t.Type() {
140 | var castVal interface{}
141 | var casterr error
142 | vString := v.Interface().(string)
143 |
144 | switch t.Kind() {
145 | case reflect.Bool:
146 | castVal, casterr = strconv.ParseBool(vString)
147 | case reflect.Int:
148 | castVal, casterr = strconv.Atoi(vString)
149 | case reflect.Int8:
150 | var val64 int64
151 | val64, casterr = strconv.ParseInt(vString, 10, 8)
152 | if casterr == nil {
153 | castVal = int8(val64)
154 | }
155 | case reflect.Int16:
156 | var val64 int64
157 | val64, casterr = strconv.ParseInt(vString, 10, 16)
158 | if casterr == nil {
159 | castVal = int16(val64)
160 | }
161 | case reflect.Int32:
162 | var val64 int64
163 | val64, casterr = strconv.ParseInt(vString, 10, 32)
164 | if casterr == nil {
165 | castVal = int32(val64)
166 | }
167 | case reflect.Int64:
168 | castVal, casterr = strconv.ParseInt(vString, 10, 64)
169 | case reflect.Uint:
170 | var val64 uint64
171 | val64, casterr = strconv.ParseUint(vString, 10, 8)
172 | if casterr == nil {
173 | castVal = uint(val64)
174 | }
175 | case reflect.Uint8:
176 | var val64 uint64
177 | val64, casterr = strconv.ParseUint(vString, 10, 8)
178 | if casterr == nil {
179 | castVal = uint8(val64)
180 | }
181 | case reflect.Uint16:
182 | var val64 uint64
183 | val64, casterr = strconv.ParseUint(vString, 10, 16)
184 | if casterr == nil {
185 | castVal = uint16(val64)
186 | }
187 | case reflect.Uint32:
188 | var val64 uint64
189 | val64, casterr = strconv.ParseUint(vString, 10, 32)
190 | if casterr == nil {
191 | castVal = uint32(val64)
192 | }
193 | case reflect.Uint64:
194 | castVal, casterr = strconv.ParseUint(vString, 10, 64)
195 | case reflect.Float32:
196 | var val64 float64
197 | val64, casterr = strconv.ParseFloat(vString, 32)
198 | if casterr == nil {
199 | castVal = float32(val64)
200 | }
201 | case reflect.Float64:
202 | castVal, casterr = strconv.ParseFloat(vString, 64)
203 | default:
204 | return fmt.Errorf("Unable to convert from string to type %s", t.Kind())
205 | }
206 |
207 | if casterr != nil {
208 | return casterr
209 | }
210 |
211 | t.Set(reflect.ValueOf(castVal))
212 | return
213 | }
214 |
215 | // if we are copying from one slice or array to another
216 | if isList(v) && isList(t) {
217 | // loop over every item in the desired value
218 | for i := 0; i < v.Len(); i++ {
219 | // write to the target given its kind
220 | switch t.Kind() {
221 | // if its a slice
222 | case reflect.Slice:
223 | // an object of the correct type
224 | obj := reflect.Indirect(reflect.New(t.Type().Elem()))
225 |
226 | // write the appropriate value to the obj and catch any errors
227 | if err := copy(obj, v.Index(i)); err != nil {
228 | return err
229 | }
230 |
231 | // just append the value to the end
232 | t.Set(reflect.Append(t, obj))
233 | // otherwise it could be an array
234 | case reflect.Array:
235 | // set the index to the appropriate value
236 | copy(t.Slice(i, i+1).Index(0), v.Index(i))
237 | }
238 | }
239 | } else {
240 | // set the value to the target
241 | t.Set(v)
242 | }
243 |
244 | // we're done
245 | return
246 | }
247 |
--------------------------------------------------------------------------------
/core/write_test.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestWrite_returnsErrorIfTargetNotPtr(t *testing.T) {
12 | // try to copy a value to a non-pointer
13 | err := WriteAnswer(true, "hello", true)
14 | // make sure there was an error
15 | if err == nil {
16 | t.Error("Did not encounter error when writing to non-pointer.")
17 | }
18 | }
19 |
20 | func TestWrite_canWriteToBool(t *testing.T) {
21 | // a pointer to hold the boolean value
22 | ptr := true
23 |
24 | // try to copy a false value to the pointer
25 | WriteAnswer(&ptr, "", false)
26 |
27 | // if the value is true
28 | if ptr {
29 | // the test failed
30 | t.Error("Could not write a false bool to a pointer")
31 | }
32 | }
33 |
34 | func TestWrite_canWriteString(t *testing.T) {
35 | // a pointer to hold the boolean value
36 | ptr := ""
37 |
38 | // try to copy a false value to the pointer
39 | err := WriteAnswer(&ptr, "", "hello")
40 | if err != nil {
41 | t.Error(err)
42 | }
43 |
44 | // if the value is not what we wrote
45 | if ptr != "hello" {
46 | t.Error("Could not write a string value to a pointer")
47 | }
48 | }
49 |
50 | func TestWrite_canWriteSlice(t *testing.T) {
51 | // a pointer to hold the value
52 | ptr := []string{}
53 |
54 | // copy in a value
55 | WriteAnswer(&ptr, "", []string{"hello", "world"})
56 |
57 | // make sure there are two entries
58 | assert.Equal(t, []string{"hello", "world"}, ptr)
59 | }
60 |
61 | func TestWrite_recoversInvalidReflection(t *testing.T) {
62 | // a variable to mutate
63 | ptr := false
64 |
65 | // write a boolean value to the string
66 | err := WriteAnswer(&ptr, "", "hello")
67 |
68 | // if there was no error
69 | if err == nil {
70 | // the test failed
71 | t.Error("Did not encounter error when forced invalid write.")
72 | }
73 | }
74 |
75 | func TestWriteAnswer_handlesNonStructValues(t *testing.T) {
76 | // the value to write to
77 | ptr := ""
78 |
79 | // write a value to the pointer
80 | WriteAnswer(&ptr, "", "world")
81 |
82 | // if we didn't change the value appropriate
83 | if ptr != "world" {
84 | // the test failed
85 | t.Error("Did not write value to primitive pointer")
86 | }
87 | }
88 |
89 | func TestWriteAnswer_canMutateStruct(t *testing.T) {
90 | // the struct to hold the answer
91 | ptr := struct{ Name string }{}
92 |
93 | // write a value to an existing field
94 | err := WriteAnswer(&ptr, "name", "world")
95 | if err != nil {
96 | // the test failed
97 | t.Errorf("Encountered error while writing answer: %v", err.Error())
98 | // we're done here
99 | return
100 | }
101 |
102 | // make sure we changed the field
103 | if ptr.Name != "world" {
104 | // the test failed
105 | t.Error("Did not mutate struct field when writing answer.")
106 | }
107 | }
108 |
109 | func TestWriteAnswer_canMutateMap(t *testing.T) {
110 | // the map to hold the answer
111 | ptr := make(map[string]interface{})
112 |
113 | // write a value to an existing field
114 | err := WriteAnswer(&ptr, "name", "world")
115 | if err != nil {
116 | // the test failed
117 | t.Errorf("Encountered error while writing answer: %v", err.Error())
118 | // we're done here
119 | return
120 | }
121 |
122 | // make sure we changed the field
123 | if ptr["name"] != "world" {
124 | // the test failed
125 | t.Error("Did not mutate map when writing answer.")
126 | }
127 | }
128 |
129 | func TestWrite_returnsErrorIfInvalidMapType(t *testing.T) {
130 | // try to copy a value to a non map[string]interface{}
131 | ptr := make(map[int]string)
132 |
133 | err := WriteAnswer(&ptr, "name", "world")
134 | // make sure there was an error
135 | if err == nil {
136 | t.Error("Did not encounter error when writing to invalid map.")
137 | }
138 | }
139 |
140 | func TestWrite_writesStringSliceToIntSlice(t *testing.T) {
141 | // make a slice of int to write to
142 | target := []int{}
143 |
144 | // write the answer
145 | err := WriteAnswer(&target, "name", []string{"1", "2", "3"})
146 |
147 | // make sure there was no error
148 | assert.Nil(t, err, "WriteSlice to Int Slice")
149 | // and we got what we wanted
150 | assert.Equal(t, []int{1, 2, 3}, target)
151 | }
152 |
153 | func TestWrite_writesStringArrayToIntArray(t *testing.T) {
154 | // make an array of int to write to
155 | target := [3]int{}
156 |
157 | // write the answer
158 | err := WriteAnswer(&target, "name", [3]string{"1", "2", "3"})
159 |
160 | // make sure there was no error
161 | assert.Nil(t, err, "WriteArray to Int Array")
162 | // and we got what we wanted
163 | assert.Equal(t, [3]int{1, 2, 3}, target)
164 | }
165 |
166 | func TestWriteAnswer_returnsErrWhenFieldNotFound(t *testing.T) {
167 | // the struct to hold the answer
168 | ptr := struct{ Name string }{}
169 |
170 | // write a value to an existing field
171 | err := WriteAnswer(&ptr, "", "world")
172 |
173 | if err == nil {
174 | // the test failed
175 | t.Error("Did not encountered error while writing answer to non-existing field.")
176 | }
177 | }
178 |
179 | func TestFindFieldIndex_canFindExportedField(t *testing.T) {
180 | // create a reflective wrapper over the struct to look through
181 | val := reflect.ValueOf(struct{ Name string }{})
182 |
183 | // find the field matching "name"
184 | fieldIndex, err := findFieldIndex(val, "name")
185 | // if something went wrong
186 | if err != nil {
187 | // the test failed
188 | t.Error(err.Error())
189 | return
190 | }
191 |
192 | // make sure we got the right value
193 | if val.Type().Field(fieldIndex).Name != "Name" {
194 | // the test failed
195 | t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", val.Type().Field(fieldIndex).Name)
196 | }
197 | }
198 |
199 | func TestFindFieldIndex_canFindTaggedField(t *testing.T) {
200 | // the struct to look through
201 | val := reflect.ValueOf(struct {
202 | Username string `survey:"name"`
203 | }{})
204 |
205 | // find the field matching "name"
206 | fieldIndex, err := findFieldIndex(val, "name")
207 | // if something went wrong
208 | if err != nil {
209 | // the test failed
210 | t.Error(err.Error())
211 | return
212 | }
213 |
214 | // make sure we got the right value
215 | if val.Type().Field(fieldIndex).Name != "Username" {
216 | // the test failed
217 | t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", val.Type().Field(fieldIndex).Name)
218 | }
219 | }
220 |
221 | func TestFindFieldIndex_canHandleCapitalAnswerNames(t *testing.T) {
222 | // create a reflective wrapper over the struct to look through
223 | val := reflect.ValueOf(struct{ Name string }{})
224 |
225 | // find the field matching "name"
226 | fieldIndex, err := findFieldIndex(val, "Name")
227 | // if something went wrong
228 | if err != nil {
229 | // the test failed
230 | t.Error(err.Error())
231 | return
232 | }
233 |
234 | // make sure we got the right value
235 | if val.Type().Field(fieldIndex).Name != "Name" {
236 | // the test failed
237 | t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", val.Type().Field(fieldIndex).Name)
238 | }
239 | }
240 |
241 | func TestFindFieldIndex_tagOverwriteFieldName(t *testing.T) {
242 | // the struct to look through
243 | val := reflect.ValueOf(struct {
244 | Name string
245 | Username string `survey:"name"`
246 | }{})
247 |
248 | // find the field matching "name"
249 | fieldIndex, err := findFieldIndex(val, "name")
250 | // if something went wrong
251 | if err != nil {
252 | // the test failed
253 | t.Error(err.Error())
254 | return
255 | }
256 |
257 | // make sure we got the right value
258 | if val.Type().Field(fieldIndex).Name != "Username" {
259 | // the test failed
260 | t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", val.Type().Field(fieldIndex).Name)
261 | }
262 | }
263 |
264 | type testFieldSettable struct {
265 | Values map[string]string
266 | }
267 |
268 | type testStringSettable struct {
269 | Value string `survey:"string"`
270 | }
271 |
272 | type testTaggedStruct struct {
273 | TaggedValue testStringSettable `survey:"tagged"`
274 | }
275 |
276 | type testPtrTaggedStruct struct {
277 | TaggedValue *testStringSettable `survey:"tagged"`
278 | }
279 |
280 | func (t *testFieldSettable) WriteAnswer(name string, value interface{}) error {
281 | if t.Values == nil {
282 | t.Values = map[string]string{}
283 | }
284 | if v, ok := value.(string); ok {
285 | t.Values[name] = v
286 | return nil
287 | }
288 | return fmt.Errorf("Incompatible type %T", value)
289 | }
290 |
291 | func (t *testStringSettable) WriteAnswer(_ string, value interface{}) error {
292 | t.Value = value.(string)
293 | return nil
294 | }
295 |
296 | func TestWriteWithFieldSettable(t *testing.T) {
297 | testSet1 := testFieldSettable{}
298 | err := WriteAnswer(&testSet1, "values", "stringVal")
299 | assert.Nil(t, err)
300 | assert.Equal(t, map[string]string{"values": "stringVal"}, testSet1.Values)
301 |
302 | testSet2 := testFieldSettable{}
303 | err = WriteAnswer(&testSet2, "values", 123)
304 | assert.Error(t, fmt.Errorf("Incompatible type int64"), err)
305 | assert.Equal(t, map[string]string{}, testSet2.Values)
306 |
307 | testString1 := testStringSettable{}
308 | err = WriteAnswer(&testString1, "", "value1")
309 | assert.Nil(t, err)
310 | assert.Equal(t, testStringSettable{"value1"}, testString1)
311 |
312 | testSetStruct := testTaggedStruct{}
313 | err = WriteAnswer(&testSetStruct, "tagged", "stringVal1")
314 | assert.Nil(t, err)
315 | assert.Equal(t, testTaggedStruct{TaggedValue: testStringSettable{"stringVal1"}}, testSetStruct)
316 |
317 | testPtrSetStruct := testPtrTaggedStruct{&testStringSettable{}}
318 | err = WriteAnswer(&testPtrSetStruct, "tagged", "stringVal1")
319 | assert.Nil(t, err)
320 | assert.Equal(t, testPtrTaggedStruct{TaggedValue: &testStringSettable{"stringVal1"}}, testPtrSetStruct)
321 | }
322 |
323 | // CONVERSION TESTS
324 | func TestWrite_canStringToBool(t *testing.T) {
325 | // a pointer to hold the boolean value
326 | ptr := true
327 |
328 | // try to copy a false value to the pointer
329 | WriteAnswer(&ptr, "", "false")
330 |
331 | // if the value is true
332 | if ptr {
333 | // the test failed
334 | t.Error("Could not convert string to pointer type")
335 | }
336 | }
337 |
338 | func TestWrite_canStringToInt(t *testing.T) {
339 | // a pointer to hold the value
340 | var ptr int = 1
341 |
342 | // try to copy a value to the pointer
343 | WriteAnswer(&ptr, "", "2")
344 |
345 | // if the value is true
346 | if ptr != 2 {
347 | // the test failed
348 | t.Error("Could not convert string to pointer type")
349 | }
350 | }
351 |
352 | func TestWrite_canStringToInt8(t *testing.T) {
353 | // a pointer to hold the value
354 | var ptr int8 = 1
355 |
356 | // try to copy a value to the pointer
357 | WriteAnswer(&ptr, "", "2")
358 |
359 | // if the value is true
360 | if ptr != 2 {
361 | // the test failed
362 | t.Error("Could not convert string to pointer type")
363 | }
364 | }
365 |
366 | func TestWrite_canStringToInt16(t *testing.T) {
367 | // a pointer to hold the value
368 | var ptr int16 = 1
369 |
370 | // try to copy a value to the pointer
371 | WriteAnswer(&ptr, "", "2")
372 |
373 | // if the value is true
374 | if ptr != 2 {
375 | // the test failed
376 | t.Error("Could not convert string to pointer type")
377 | }
378 | }
379 |
380 | func TestWrite_canStringToInt32(t *testing.T) {
381 | // a pointer to hold the value
382 | var ptr int32 = 1
383 |
384 | // try to copy a value to the pointer
385 | WriteAnswer(&ptr, "", "2")
386 |
387 | // if the value is true
388 | if ptr != 2 {
389 | // the test failed
390 | t.Error("Could not convert string to pointer type")
391 | }
392 | }
393 |
394 | func TestWrite_canStringToInt64(t *testing.T) {
395 | // a pointer to hold the value
396 | var ptr int64 = 1
397 |
398 | // try to copy a value to the pointer
399 | WriteAnswer(&ptr, "", "2")
400 |
401 | // if the value is true
402 | if ptr != 2 {
403 | // the test failed
404 | t.Error("Could not convert string to pointer type")
405 | }
406 | }
407 |
408 | func TestWrite_canStringToUint(t *testing.T) {
409 | // a pointer to hold the value
410 | var ptr uint = 1
411 |
412 | // try to copy a value to the pointer
413 | WriteAnswer(&ptr, "", "2")
414 |
415 | // if the value is true
416 | if ptr != 2 {
417 | // the test failed
418 | t.Error("Could not convert string to pointer type")
419 | }
420 | }
421 |
422 | func TestWrite_canStringToUint8(t *testing.T) {
423 | // a pointer to hold the value
424 | var ptr uint8 = 1
425 |
426 | // try to copy a value to the pointer
427 | WriteAnswer(&ptr, "", "2")
428 |
429 | // if the value is true
430 | if ptr != 2 {
431 | // the test failed
432 | t.Error("Could not convert string to pointer type")
433 | }
434 | }
435 |
436 | func TestWrite_canStringToUint16(t *testing.T) {
437 | // a pointer to hold the value
438 | var ptr uint16 = 1
439 |
440 | // try to copy a value to the pointer
441 | WriteAnswer(&ptr, "", "2")
442 |
443 | // if the value is true
444 | if ptr != 2 {
445 | // the test failed
446 | t.Error("Could not convert string to pointer type")
447 | }
448 | }
449 |
450 | func TestWrite_canStringToUint32(t *testing.T) {
451 | // a pointer to hold the value
452 | var ptr uint32 = 1
453 |
454 | // try to copy a value to the pointer
455 | WriteAnswer(&ptr, "", "2")
456 |
457 | // if the value is true
458 | if ptr != 2 {
459 | // the test failed
460 | t.Error("Could not convert string to pointer type")
461 | }
462 | }
463 |
464 | func TestWrite_canStringToUint64(t *testing.T) {
465 | // a pointer to hold the value
466 | var ptr uint64 = 1
467 |
468 | // try to copy a value to the pointer
469 | WriteAnswer(&ptr, "", "2")
470 |
471 | // if the value is true
472 | if ptr != 2 {
473 | // the test failed
474 | t.Error("Could not convert string to pointer type")
475 | }
476 | }
477 |
478 | func TestWrite_canStringToFloat32(t *testing.T) {
479 | // a pointer to hold the value
480 | var ptr float32 = 1.0
481 |
482 | // try to copy a value to the pointer
483 | WriteAnswer(&ptr, "", "2.5")
484 |
485 | // if the value is true
486 | if ptr != 2.5 {
487 | // the test failed
488 | t.Error("Could not convert string to pointer type")
489 | }
490 | }
491 |
492 | func TestWrite_canStringToFloat64(t *testing.T) {
493 | // a pointer to hold the value
494 | var ptr float64 = 1.0
495 |
496 | // try to copy a value to the pointer
497 | WriteAnswer(&ptr, "", "2.5")
498 |
499 | // if the value is true
500 | if ptr != 2.5 {
501 | // the test failed
502 | t.Error("Could not convert string to pointer type")
503 | }
504 | }
505 |
506 | func TestWrite_canConvertStructFieldTypes(t *testing.T) {
507 | // the struct to hold the answer
508 | ptr := struct {
509 | Name string
510 | Age uint
511 | Male bool
512 | Height float64
513 | }{}
514 |
515 | // write the values as strings
516 | check(t, WriteAnswer(&ptr, "name", "Bob"))
517 | check(t, WriteAnswer(&ptr, "age", "22"))
518 | check(t, WriteAnswer(&ptr, "male", "true"))
519 | check(t, WriteAnswer(&ptr, "height", "6.2"))
520 |
521 | // make sure we changed the fields
522 | if ptr.Name != "Bob" {
523 | t.Error("Did not mutate Name when writing answer.")
524 | }
525 |
526 | if ptr.Age != 22 {
527 | t.Error("Did not mutate Age when writing answer.")
528 | }
529 |
530 | if !ptr.Male {
531 | t.Error("Did not mutate Male when writing answer.")
532 | }
533 |
534 | if ptr.Height != 6.2 {
535 | t.Error("Did not mutate Height when writing answer.")
536 | }
537 | }
538 |
539 | func check(t *testing.T, err error) {
540 | if err != nil {
541 | t.Fatalf("Encountered error while writing answer: %v", err.Error())
542 | }
543 | }
544 |
--------------------------------------------------------------------------------
/editor.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "os"
7 | "os/exec"
8 | "runtime"
9 |
10 | "github.com/tj/survey/core"
11 | "github.com/tj/survey/terminal"
12 | )
13 |
14 | /*
15 | Editor launches an instance of the users preferred editor on a temporary file.
16 | The editor to use is determined by reading the $VISUAL or $EDITOR environment
17 | variables. If neither of those are present, notepad (on Windows) or vim
18 | (others) is used.
19 | The launch of the editor is triggered by the enter key. Since the response may
20 | be long, it will not be echoed as Input does, instead, it print .
21 | Response type is a string.
22 |
23 | message := ""
24 | prompt := &survey.Editor{ Message: "What is your commit message?" }
25 | survey.AskOne(prompt, &message, nil)
26 | */
27 | type Editor struct {
28 | core.Renderer
29 | Message string
30 | Default string
31 | Help string
32 | }
33 |
34 | // data available to the templates when processing
35 | type EditorTemplateData struct {
36 | Editor
37 | Answer string
38 | ShowAnswer bool
39 | ShowHelp bool
40 | }
41 |
42 | // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
43 | var EditorQuestionTemplate = `
44 | {{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
45 | {{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}}
46 | {{- color "default+hb"}}{{ .Message }} {{color "reset"}}
47 | {{- if .ShowAnswer}}
48 | {{- color "gray"}}{{.Answer}}{{color "reset"}}{{"\n"}}
49 | {{- else }}
50 | {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}}
51 | {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
52 | {{- color "cyan"}}[Enter to launch editor] {{color "reset"}}
53 | {{- end}}`
54 |
55 | var (
56 | bom = []byte{0xef, 0xbb, 0xbf}
57 | editor = "vim"
58 | )
59 |
60 | func init() {
61 | if runtime.GOOS == "windows" {
62 | editor = "notepad"
63 | }
64 | if v := os.Getenv("VISUAL"); v != "" {
65 | editor = v
66 | } else if e := os.Getenv("EDITOR"); e != "" {
67 | editor = e
68 | }
69 | }
70 |
71 | func (e *Editor) Prompt() (interface{}, error) {
72 | // render the template
73 | err := e.Render(
74 | EditorQuestionTemplate,
75 | EditorTemplateData{Editor: *e},
76 | )
77 | if err != nil {
78 | return "", err
79 | }
80 |
81 | // start reading runes from the standard in
82 | rr := terminal.NewRuneReader(os.Stdin)
83 | rr.SetTermMode()
84 | defer rr.RestoreTermMode()
85 |
86 | terminal.CursorHide()
87 | defer terminal.CursorShow()
88 |
89 | for {
90 | r, _, err := rr.ReadRune()
91 | if err != nil {
92 | return "", err
93 | }
94 | if r == '\r' || r == '\n' {
95 | break
96 | }
97 | if r == terminal.KeyInterrupt {
98 | return "", terminal.InterruptErr
99 | }
100 | if r == terminal.KeyEndTransmission {
101 | break
102 | }
103 | if r == core.HelpInputRune && e.Help != "" {
104 | err = e.Render(
105 | EditorQuestionTemplate,
106 | EditorTemplateData{Editor: *e, ShowHelp: true},
107 | )
108 | if err != nil {
109 | return "", err
110 | }
111 | }
112 | continue
113 | }
114 |
115 | // prepare the temp file
116 | f, err := ioutil.TempFile("", "survey")
117 | if err != nil {
118 | return "", err
119 | }
120 | defer os.Remove(f.Name())
121 |
122 | // write utf8 BOM header
123 | // The reason why we do this is because notepad.exe on Windows determines the
124 | // encoding of an "empty" text file by the locale, for example, GBK in China,
125 | // while golang string only handles utf8 well. However, a text file with utf8
126 | // BOM header is not considered "empty" on Windows, and the encoding will then
127 | // be determined utf8 by notepad.exe, instead of GBK or other encodings.
128 | if _, err := f.Write(bom); err != nil {
129 | return "", err
130 | }
131 | // close the fd to prevent the editor unable to save file
132 | if err := f.Close(); err != nil {
133 | return "", err
134 | }
135 |
136 | // open the editor
137 | cmd := exec.Command(editor, f.Name())
138 | cmd.Stdin = os.Stdin
139 | cmd.Stdout = os.Stdout
140 | cmd.Stderr = os.Stderr
141 | terminal.CursorShow()
142 | if err := cmd.Run(); err != nil {
143 | return "", err
144 | }
145 |
146 | // raw is a BOM-unstripped UTF8 byte slice
147 | raw, err := ioutil.ReadFile(f.Name())
148 | if err != nil {
149 | return "", err
150 | }
151 |
152 | // strip BOM header
153 | text := string(bytes.TrimPrefix(raw, bom))
154 |
155 | // check length, return default value on empty
156 | if len(text) == 0 {
157 | return e.Default, nil
158 | }
159 |
160 | return text, nil
161 | }
162 |
163 | func (e *Editor) Cleanup(val interface{}) error {
164 | return e.Render(
165 | EditorQuestionTemplate,
166 | EditorTemplateData{Editor: *e, Answer: "", ShowAnswer: true},
167 | )
168 | }
169 |
--------------------------------------------------------------------------------
/editor_test.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/tj/survey/core"
9 | "github.com/tj/survey/terminal"
10 | )
11 |
12 | func init() {
13 | // disable color output for all prompts to simplify testing
14 | core.DisableColor = true
15 | }
16 |
17 | func TestEditorRender(t *testing.T) {
18 | tests := []struct {
19 | title string
20 | prompt Editor
21 | data EditorTemplateData
22 | expected string
23 | }{
24 | {
25 | "Test Editor question output without default",
26 | Editor{Message: "What is your favorite month:"},
27 | EditorTemplateData{},
28 | "? What is your favorite month: [Enter to launch editor] ",
29 | },
30 | {
31 | "Test Editor question output with default",
32 | Editor{Message: "What is your favorite month:", Default: "April"},
33 | EditorTemplateData{},
34 | "? What is your favorite month: (April) [Enter to launch editor] ",
35 | },
36 | {
37 | "Test Editor answer output",
38 | Editor{Message: "What is your favorite month:"},
39 | EditorTemplateData{Answer: "October", ShowAnswer: true},
40 | "? What is your favorite month: October\n",
41 | },
42 | {
43 | "Test Editor question output without default but with help hidden",
44 | Editor{Message: "What is your favorite month:", Help: "This is helpful"},
45 | EditorTemplateData{},
46 | "? What is your favorite month: [? for help] [Enter to launch editor] ",
47 | },
48 | {
49 | "Test Editor question output with default and with help hidden",
50 | Editor{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"},
51 | EditorTemplateData{},
52 | "? What is your favorite month: [? for help] (April) [Enter to launch editor] ",
53 | },
54 | {
55 | "Test Editor question output without default but with help shown",
56 | Editor{Message: "What is your favorite month:", Help: "This is helpful"},
57 | EditorTemplateData{ShowHelp: true},
58 | `ⓘ This is helpful
59 | ? What is your favorite month: [Enter to launch editor] `,
60 | },
61 | {
62 | "Test Editor question output with default and with help shown",
63 | Editor{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"},
64 | EditorTemplateData{ShowHelp: true},
65 | `ⓘ This is helpful
66 | ? What is your favorite month: (April) [Enter to launch editor] `,
67 | },
68 | }
69 |
70 | outputBuffer := bytes.NewBufferString("")
71 | terminal.Stdout = outputBuffer
72 |
73 | for _, test := range tests {
74 | outputBuffer.Reset()
75 | test.data.Editor = test.prompt
76 | err := test.prompt.Render(
77 | EditorQuestionTemplate,
78 | test.data,
79 | )
80 | assert.Nil(t, err, test.title)
81 | assert.Equal(t, test.expected, outputBuffer.String(), test.title)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/examples/longlist.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/tj/survey"
7 | )
8 |
9 | // the questions to ask
10 | var simpleQs = []*survey.Question{
11 | {
12 | Name: "letter",
13 | Prompt: &survey.Select{
14 | Message: "Choose a letter:",
15 | Options: []string{
16 | "a",
17 | "b",
18 | "c",
19 | "d",
20 | "e",
21 | "f",
22 | "g",
23 | "h",
24 | "i",
25 | "j",
26 | },
27 | },
28 | Validate: survey.Required,
29 | },
30 | }
31 |
32 | func main() {
33 | answers := struct {
34 | Letter string
35 | }{}
36 |
37 | // ask the question
38 | err := survey.Ask(simpleQs, &answers)
39 |
40 | if err != nil {
41 | fmt.Println(err.Error())
42 | return
43 | }
44 | // print the answers
45 | fmt.Printf("you chose %s.\n", answers.Letter)
46 | }
47 |
--------------------------------------------------------------------------------
/examples/map.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/tj/survey"
7 | )
8 |
9 | // the questions to ask
10 | var simpleQs = []*survey.Question{
11 | {
12 | Name: "name",
13 | Prompt: &survey.Input{
14 | Message: "What is your name?",
15 | },
16 | Validate: survey.Required,
17 | },
18 | {
19 | Name: "color",
20 | Prompt: &survey.Select{
21 | Message: "Choose a color:",
22 | Options: []string{"red", "blue", "green"},
23 | },
24 | Validate: survey.Required,
25 | },
26 | }
27 |
28 | func main() {
29 | ansmap := make(map[string]interface{})
30 |
31 | // ask the question
32 | err := survey.Ask(simpleQs, &ansmap)
33 |
34 | if err != nil {
35 | fmt.Println(err.Error())
36 | return
37 | }
38 | // print the answers
39 | fmt.Printf("%s chose %s.\n", ansmap["name"], ansmap["color"])
40 | }
41 |
--------------------------------------------------------------------------------
/examples/simple.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/tj/survey"
7 | )
8 |
9 | // the questions to ask
10 | var simpleQs = []*survey.Question{
11 | {
12 | Name: "name",
13 | Prompt: &survey.Input{
14 | Message: "What is your name?",
15 | },
16 | Validate: survey.Required,
17 | },
18 | {
19 | Name: "color",
20 | Prompt: &survey.Select{
21 | Message: "Choose a color:",
22 | Options: []string{"red", "blue", "green"},
23 | },
24 | Validate: survey.Required,
25 | },
26 | }
27 |
28 | func main() {
29 | answers := struct {
30 | Name string
31 | Color string
32 | }{}
33 |
34 | // ask the question
35 | err := survey.Ask(simpleQs, &answers)
36 |
37 | if err != nil {
38 | fmt.Println(err.Error())
39 | return
40 | }
41 | // print the answers
42 | fmt.Printf("%s chose %s.\n", answers.Name, answers.Color)
43 | }
44 |
--------------------------------------------------------------------------------
/examples/validation.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/tj/survey"
7 | )
8 |
9 | // the questions to ask
10 | var validationQs = []*survey.Question{
11 | {
12 | Name: "name",
13 | Prompt: &survey.Input{Message: "What is your name?"},
14 | Validate: survey.Required,
15 | },
16 | {
17 | Name: "valid",
18 | Prompt: &survey.Input{Message: "Enter 'foo':", Default: "not foo"},
19 | Validate: func(val interface{}) error {
20 | // if the input matches the expectation
21 | if str := val.(string); str != "foo" {
22 | return fmt.Errorf("You entered %s, not 'foo'.", str)
23 | }
24 | // nothing was wrong
25 | return nil
26 | },
27 | },
28 | }
29 |
30 | func main() {
31 | // the place to hold the answers
32 | answers := struct {
33 | Name string
34 | Valid string
35 | }{}
36 | err := survey.Ask(validationQs, &answers)
37 |
38 | if err != nil {
39 | fmt.Println("\n", err.Error())
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tj/survey
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/mattn/go-colorable v0.1.2 // indirect
7 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
8 | github.com/stretchr/testify v1.4.0
9 | )
10 |
--------------------------------------------------------------------------------
/input.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/tj/survey/core"
7 | "github.com/tj/survey/terminal"
8 | )
9 |
10 | /*
11 | Input is a regular text input that prints each character the user types on the screen
12 | and 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, nil)
17 | */
18 | type Input struct {
19 | core.Renderer
20 | Message string
21 | Default string
22 | Help string
23 | }
24 |
25 | // data available to the templates when processing
26 | type InputTemplateData struct {
27 | Input
28 | Answer string
29 | ShowAnswer bool
30 | ShowHelp bool
31 | }
32 |
33 | // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
34 | var InputQuestionTemplate = `
35 | {{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
36 | {{- color "default"}} {{ .Message }} {{color "reset"}}
37 | {{- if .ShowAnswer}}
38 | {{- color "gray"}}{{.Answer}}{{color "reset"}}{{"\n"}}
39 | {{- else }}
40 | {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}}
41 | {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
42 | {{- end}}`
43 |
44 | func (i *Input) Prompt() (interface{}, error) {
45 | // render the template
46 | err := i.Render(
47 | InputQuestionTemplate,
48 | InputTemplateData{Input: *i},
49 | )
50 | if err != nil {
51 | return "", err
52 | }
53 |
54 | // start reading runes from the standard in
55 | rr := terminal.NewRuneReader(os.Stdin)
56 | rr.SetTermMode()
57 | defer rr.RestoreTermMode()
58 |
59 | line := []rune{}
60 | // get the next line
61 | for {
62 | line, err = rr.ReadLine(0)
63 | if err != nil {
64 | return string(line), err
65 | }
66 | // terminal will echo the \n so we need to jump back up one row
67 | terminal.CursorPreviousLine(1)
68 |
69 | if string(line) == string(core.HelpInputRune) && i.Help != "" {
70 | err = i.Render(
71 | InputQuestionTemplate,
72 | InputTemplateData{Input: *i, ShowHelp: true},
73 | )
74 | if err != nil {
75 | return "", err
76 | }
77 | continue
78 | }
79 | break
80 | }
81 |
82 | // if the line is empty
83 | if line == nil || len(line) == 0 {
84 | // use the default value
85 | return i.Default, err
86 | }
87 |
88 | // we're done
89 | return string(line), err
90 | }
91 |
92 | func (i *Input) Cleanup(val interface{}) error {
93 | return i.Render(
94 | InputQuestionTemplate,
95 | InputTemplateData{Input: *i, Answer: val.(string), ShowAnswer: true},
96 | )
97 | }
98 |
--------------------------------------------------------------------------------
/input_test.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/tj/survey/core"
9 | "github.com/tj/survey/terminal"
10 | )
11 |
12 | func init() {
13 | // disable color output for all prompts to simplify testing
14 | core.DisableColor = true
15 | }
16 |
17 | func TestInputRender(t *testing.T) {
18 |
19 | tests := []struct {
20 | title string
21 | prompt Input
22 | data InputTemplateData
23 | expected string
24 | }{
25 | {
26 | "Test Input question output without default",
27 | Input{Message: "What is your favorite month:"},
28 | InputTemplateData{},
29 | "? What is your favorite month: ",
30 | },
31 | {
32 | "Test Input question output with default",
33 | Input{Message: "What is your favorite month:", Default: "April"},
34 | InputTemplateData{},
35 | "? What is your favorite month: (April) ",
36 | },
37 | {
38 | "Test Input answer output",
39 | Input{Message: "What is your favorite month:"},
40 | InputTemplateData{Answer: "October", ShowAnswer: true},
41 | "? What is your favorite month: October\n",
42 | },
43 | {
44 | "Test Input question output without default but with help hidden",
45 | Input{Message: "What is your favorite month:", Help: "This is helpful"},
46 | InputTemplateData{},
47 | "? What is your favorite month: [? for help] ",
48 | },
49 | {
50 | "Test Input question output with default and with help hidden",
51 | Input{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"},
52 | InputTemplateData{},
53 | "? What is your favorite month: [? for help] (April) ",
54 | },
55 | {
56 | "Test Input question output without default but with help shown",
57 | Input{Message: "What is your favorite month:", Help: "This is helpful"},
58 | InputTemplateData{ShowHelp: true},
59 | `ⓘ This is helpful
60 | ? What is your favorite month: `,
61 | },
62 | {
63 | "Test Input question output with default and with help shown",
64 | Input{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"},
65 | InputTemplateData{ShowHelp: true},
66 | `ⓘ This is helpful
67 | ? What is your favorite month: (April) `,
68 | },
69 | }
70 |
71 | outputBuffer := bytes.NewBufferString("")
72 | terminal.Stdout = outputBuffer
73 |
74 | for _, test := range tests {
75 | outputBuffer.Reset()
76 | test.data.Input = test.prompt
77 | err := test.prompt.Render(
78 | InputQuestionTemplate,
79 | test.data,
80 | )
81 | assert.Nil(t, err, test.title)
82 | assert.Equal(t, test.expected, outputBuffer.String(), test.title)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/multiselect.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "strings"
7 |
8 | "github.com/tj/survey/core"
9 | "github.com/tj/survey/terminal"
10 | )
11 |
12 | /*
13 | MultiSelect is a prompt that presents a list of various options to the user
14 | for them to select using the arrow keys and enter. Response type is a slice of strings.
15 |
16 | days := []string{}
17 | prompt := &survey.MultiSelect{
18 | Message: "What days do you prefer:",
19 | Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
20 | }
21 | survey.AskOne(prompt, &days, nil)
22 | */
23 | type MultiSelect struct {
24 | core.Renderer
25 | Message string
26 | Options []string
27 | Default []string
28 | Help string
29 | PageSize int
30 | selectedIndex int
31 | checked map[string]bool
32 | showingHelp bool
33 | }
34 |
35 | // data available to the templates when processing
36 | type MultiSelectTemplateData struct {
37 | MultiSelect
38 | Answer string
39 | ShowAnswer bool
40 | Checked map[string]bool
41 | SelectedIndex int
42 | ShowHelp bool
43 | PageEntries []string
44 | }
45 |
46 | var MultiSelectQuestionTemplate = `
47 | {{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
48 | {{- color "default"}} {{ .Message }}{{color "reset"}}
49 | {{- if .ShowAnswer}}{{color "gray"}} {{.Answer}}{{color "reset"}}{{"\n"}}
50 | {{- else }}
51 | {{- if and .Help (not .ShowHelp)}} {{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}}{{end}}
52 | {{- "\n"}}
53 | {{- range $ix, $option := .PageEntries}}
54 | {{- if eq $ix $.SelectedIndex}}{{color "default"}} {{ SelectFocusIcon }}{{color "reset"}}{{else}} {{end}}
55 | {{- if index $.Checked $option}}{{color "default"}} {{ MarkedOptionIcon }}{{else}}{{color "default"}} {{ UnmarkedOptionIcon }}{{end}}
56 | {{- color "gray"}}
57 | {{- " "}}{{$option}}{{"\n"}}
58 | {{- color "reset"}}
59 | {{- end}}
60 | {{- end}}`
61 |
62 | // OnChange is called on every keypress.
63 | func (m *MultiSelect) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
64 | if key == terminal.KeyArrowUp {
65 | // if we are at the top of the list
66 | if m.selectedIndex == 0 {
67 | // go to the bottom
68 | m.selectedIndex = len(m.Options) - 1
69 | } else {
70 | // decrement the selected index
71 | m.selectedIndex--
72 | }
73 | } else if key == terminal.KeyArrowDown {
74 | // if we are at the bottom of the list
75 | if m.selectedIndex == len(m.Options)-1 {
76 | // start at the top
77 | m.selectedIndex = 0
78 | } else {
79 | // increment the selected index
80 | m.selectedIndex++
81 | }
82 | // if the user pressed down and there is room to move
83 | } else if key == terminal.KeySpace {
84 | if old, ok := m.checked[m.Options[m.selectedIndex]]; !ok {
85 | // otherwise just invert the current value
86 | m.checked[m.Options[m.selectedIndex]] = true
87 | } else {
88 | // otherwise just invert the current value
89 | m.checked[m.Options[m.selectedIndex]] = !old
90 | }
91 | // only show the help message if we have one to show
92 | } else if key == core.HelpInputRune && m.Help != "" {
93 | m.showingHelp = true
94 | }
95 |
96 | // paginate the options
97 | opts, idx := paginate(m.PageSize, m.Options, m.selectedIndex)
98 |
99 | // render the options
100 | m.Render(
101 | MultiSelectQuestionTemplate,
102 | MultiSelectTemplateData{
103 | MultiSelect: *m,
104 | SelectedIndex: idx,
105 | Checked: m.checked,
106 | ShowHelp: m.showingHelp,
107 | PageEntries: opts,
108 | },
109 | )
110 |
111 | // if we are not pressing ent
112 | return line, 0, true
113 | }
114 |
115 | func (m *MultiSelect) Prompt() (interface{}, error) {
116 | // compute the default state
117 | m.checked = make(map[string]bool)
118 | // if there is a default
119 | if len(m.Default) > 0 {
120 | for _, dflt := range m.Default {
121 | for _, opt := range m.Options {
122 | // if the option correponds to the default
123 | if opt == dflt {
124 | // we found our initial value
125 | m.checked[opt] = true
126 | // stop looking
127 | break
128 | }
129 | }
130 | }
131 | }
132 |
133 | // if there are no options to render
134 | if len(m.Options) == 0 {
135 | // we failed
136 | return "", errors.New("please provide options to select from")
137 | }
138 |
139 | // hide the cursor
140 | terminal.CursorHide()
141 | // show the cursor when we're done
142 | defer terminal.CursorShow()
143 |
144 | // paginate the options
145 | opts, idx := paginate(m.PageSize, m.Options, m.selectedIndex)
146 |
147 | // ask the question
148 | err := m.Render(
149 | MultiSelectQuestionTemplate,
150 | MultiSelectTemplateData{
151 | MultiSelect: *m,
152 | SelectedIndex: idx,
153 | Checked: m.checked,
154 | PageEntries: opts,
155 | },
156 | )
157 | if err != nil {
158 | return "", err
159 | }
160 |
161 | rr := terminal.NewRuneReader(os.Stdin)
162 | rr.SetTermMode()
163 | defer rr.RestoreTermMode()
164 |
165 | // start waiting for input
166 | for {
167 | r, _, _ := rr.ReadRune()
168 | if r == '\r' || r == '\n' {
169 | break
170 | }
171 | if r == terminal.KeyInterrupt {
172 | return "", terminal.InterruptErr
173 | }
174 | if r == terminal.KeyEndTransmission {
175 | break
176 | }
177 | m.OnChange(nil, 0, r)
178 | }
179 |
180 | answers := []string{}
181 | for _, option := range m.Options {
182 | if val, ok := m.checked[option]; ok && val {
183 | answers = append(answers, option)
184 | }
185 | }
186 |
187 | return answers, nil
188 | }
189 |
190 | // Cleanup removes the options section, and renders the ask like a normal question.
191 | func (m *MultiSelect) Cleanup(val interface{}) error {
192 | // execute the output summary template with the answer
193 | return m.Render(
194 | MultiSelectQuestionTemplate,
195 | MultiSelectTemplateData{
196 | MultiSelect: *m,
197 | SelectedIndex: m.selectedIndex,
198 | Checked: m.checked,
199 | Answer: strings.Join(val.([]string), ", "),
200 | ShowAnswer: true,
201 | },
202 | )
203 | }
204 |
--------------------------------------------------------------------------------
/multiselect_test.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/tj/survey/core"
9 | "github.com/tj/survey/terminal"
10 | )
11 |
12 | func init() {
13 | // disable color output for all prompts to simplify testing
14 | core.DisableColor = true
15 | }
16 |
17 | func TestMultiSelectRender(t *testing.T) {
18 |
19 | prompt := MultiSelect{
20 | Message: "Pick your words:",
21 | Options: []string{"foo", "bar", "baz", "buz"},
22 | Default: []string{"bar", "buz"},
23 | }
24 |
25 | helpfulPrompt := prompt
26 | helpfulPrompt.Help = "This is helpful"
27 |
28 | tests := []struct {
29 | title string
30 | prompt MultiSelect
31 | data MultiSelectTemplateData
32 | expected string
33 | }{
34 | {
35 | "Test MultiSelect question output",
36 | prompt,
37 | MultiSelectTemplateData{
38 | SelectedIndex: 2,
39 | PageEntries: prompt.Options,
40 | Checked: map[string]bool{"bar": true, "buz": true},
41 | },
42 | `? Pick your words:
43 | ◯ foo
44 | ◉ bar
45 | ❯ ◯ baz
46 | ◉ buz
47 | `,
48 | },
49 | {
50 | "Test MultiSelect answer output",
51 | prompt,
52 | MultiSelectTemplateData{
53 | Answer: "foo, buz",
54 | ShowAnswer: true,
55 | },
56 | "? Pick your words: foo, buz\n",
57 | },
58 | {
59 | "Test MultiSelect question output with help hidden",
60 | helpfulPrompt,
61 | MultiSelectTemplateData{
62 | SelectedIndex: 2,
63 | PageEntries: prompt.Options,
64 | Checked: map[string]bool{"bar": true, "buz": true},
65 | },
66 | `? Pick your words: [? for help]
67 | ◯ foo
68 | ◉ bar
69 | ❯ ◯ baz
70 | ◉ buz
71 | `,
72 | },
73 | {
74 | "Test MultiSelect question output with help shown",
75 | helpfulPrompt,
76 | MultiSelectTemplateData{
77 | SelectedIndex: 2,
78 | PageEntries: prompt.Options,
79 | Checked: map[string]bool{"bar": true, "buz": true},
80 | ShowHelp: true,
81 | },
82 | `ⓘ This is helpful
83 | ? Pick your words:
84 | ◯ foo
85 | ◉ bar
86 | ❯ ◯ baz
87 | ◉ buz
88 | `,
89 | },
90 | }
91 |
92 | outputBuffer := bytes.NewBufferString("")
93 | terminal.Stdout = outputBuffer
94 |
95 | for _, test := range tests {
96 | outputBuffer.Reset()
97 | test.data.MultiSelect = test.prompt
98 | err := test.prompt.Render(
99 | MultiSelectQuestionTemplate,
100 | test.data,
101 | )
102 | assert.Nil(t, err, test.title)
103 | assert.Equal(t, test.expected, outputBuffer.String(), test.title)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/password.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/tj/survey/core"
7 | "github.com/tj/survey/terminal"
8 | )
9 |
10 | /*
11 | Password is like a normal Input but the text shows up as *'s and there is no default. Response
12 | type is a string.
13 |
14 | password := ""
15 | prompt := &survey.Password{ Message: "Please type your password" }
16 | survey.AskOne(prompt, &password, nil)
17 | */
18 | type Password struct {
19 | core.Renderer
20 | Message string
21 | Help string
22 | }
23 |
24 | type PasswordTemplateData struct {
25 | Password
26 | ShowHelp bool
27 | }
28 |
29 | // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
30 | var PasswordQuestionTemplate = `
31 | {{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
32 | {{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}}
33 | {{- color "default+hb"}}{{ .Message }} {{color "reset"}}
34 | {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}}`
35 |
36 | func (p *Password) Prompt() (line interface{}, err error) {
37 | // render the question template
38 | out, err := core.RunTemplate(
39 | PasswordQuestionTemplate,
40 | PasswordTemplateData{Password: *p},
41 | )
42 | terminal.Print(out)
43 | if err != nil {
44 | return "", err
45 | }
46 |
47 | rr := terminal.NewRuneReader(os.Stdin)
48 | rr.SetTermMode()
49 | defer rr.RestoreTermMode()
50 |
51 | // no help msg? Just return any response
52 | if p.Help == "" {
53 | line, err := rr.ReadLine('*')
54 | return string(line), err
55 | }
56 |
57 | // process answers looking for help prompt answer
58 | for {
59 | line, err := rr.ReadLine('*')
60 | if err != nil {
61 | return string(line), err
62 | }
63 |
64 | if string(line) == string(core.HelpInputRune) {
65 | // terminal will echo the \n so we need to jump back up one row
66 | terminal.CursorPreviousLine(1)
67 |
68 | err = p.Render(
69 | PasswordQuestionTemplate,
70 | PasswordTemplateData{Password: *p, ShowHelp: true},
71 | )
72 | if err != nil {
73 | return "", err
74 | }
75 | continue
76 | }
77 | return string(line), err
78 | }
79 | }
80 |
81 | // Cleanup hides the string with a fixed number of characters.
82 | func (prompt *Password) Cleanup(val interface{}) error {
83 | return nil
84 | }
85 |
--------------------------------------------------------------------------------
/password_test.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | "github.com/tj/survey/core"
8 | )
9 |
10 | func init() {
11 | // disable color output for all prompts to simplify testing
12 | core.DisableColor = true
13 | }
14 |
15 | func TestPasswordRender(t *testing.T) {
16 |
17 | tests := []struct {
18 | title string
19 | prompt Password
20 | data PasswordTemplateData
21 | expected string
22 | }{
23 | {
24 | "Test Password question output",
25 | Password{Message: "Tell me your secret:"},
26 | PasswordTemplateData{},
27 | "? Tell me your secret: ",
28 | },
29 | {
30 | "Test Password question output with help hidden",
31 | Password{Message: "Tell me your secret:", Help: "This is helpful"},
32 | PasswordTemplateData{},
33 | "? Tell me your secret: [? for help] ",
34 | },
35 | {
36 | "Test Password question output with help shown",
37 | Password{Message: "Tell me your secret:", Help: "This is helpful"},
38 | PasswordTemplateData{ShowHelp: true},
39 | `ⓘ This is helpful
40 | ? Tell me your secret: `,
41 | },
42 | }
43 |
44 | for _, test := range tests {
45 | test.data.Password = test.prompt
46 | actual, err := core.RunTemplate(
47 | PasswordQuestionTemplate,
48 | &test.data,
49 | )
50 | assert.Nil(t, err, test.title)
51 | assert.Equal(t, test.expected, actual, test.title)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/select.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "errors"
5 | "os"
6 |
7 | "github.com/tj/survey/core"
8 | "github.com/tj/survey/terminal"
9 | )
10 |
11 | /*
12 | Select is a prompt that presents a list of various options to the user
13 | for them to select using the arrow keys and enter. Response type is a string.
14 |
15 | color := ""
16 | prompt := &survey.Select{
17 | Message: "Choose a color:",
18 | Options: []string{"red", "blue", "green"},
19 | }
20 | survey.AskOne(prompt, &color, nil)
21 | */
22 | type Select struct {
23 | core.Renderer
24 | Message string
25 | Options []string
26 | Default string
27 | Help string
28 | PageSize int
29 | selectedIndex int
30 | useDefault bool
31 | showingHelp bool
32 | }
33 |
34 | // the data available to the templates when processing
35 | type SelectTemplateData struct {
36 | Select
37 | PageEntries []string
38 | SelectedIndex int
39 | Answer string
40 | ShowAnswer bool
41 | ShowHelp bool
42 | }
43 |
44 | var SelectQuestionTemplate = `
45 | {{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
46 | {{- color "default"}} {{ .Message }}{{color "reset"}}
47 | {{- if .ShowAnswer}}{{color "gray"}} {{.Answer}}{{color "reset"}}{{"\n"}}
48 | {{- else}}
49 | {{- if and .Help (not .ShowHelp)}} {{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}}{{end}}
50 | {{- "\n"}}
51 | {{- range $ix, $choice := .PageEntries}}
52 | {{- if eq $ix $.SelectedIndex}}{{color "default"}} {{ SelectFocusIcon }} {{else}} {{color "gray"}} {{end}}
53 | {{- $choice}}
54 | {{- color "reset"}}{{"\n"}}
55 | {{- end}}
56 | {{- end}}`
57 |
58 | // OnChange is called on every keypress.
59 | func (s *Select) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
60 | // if the user pressed the enter key
61 | if key == terminal.KeyEnter {
62 | return []rune(s.Options[s.selectedIndex]), 0, true
63 | // if the user pressed the up arrow
64 | } else if key == terminal.KeyArrowUp {
65 | s.useDefault = false
66 |
67 | // if we are at the top of the list
68 | if s.selectedIndex == 0 {
69 | // start from the button
70 | s.selectedIndex = len(s.Options) - 1
71 | } else {
72 | // otherwise we are not at the top of the list so decrement the selected index
73 | s.selectedIndex--
74 | }
75 | // if the user pressed down and there is room to move
76 | } else if key == terminal.KeyArrowDown {
77 | s.useDefault = false
78 | // if we are at the bottom of the list
79 | if s.selectedIndex == len(s.Options)-1 {
80 | // start from the top
81 | s.selectedIndex = 0
82 | } else {
83 | // increment the selected index
84 | s.selectedIndex++
85 | }
86 | // only show the help message if we have one
87 | } else if key == core.HelpInputRune && s.Help != "" {
88 | s.showingHelp = true
89 | }
90 |
91 | // figure out the options and index to render
92 | opts, idx := paginate(s.PageSize, s.Options, s.selectedIndex)
93 |
94 | // render the options
95 | s.Render(
96 | SelectQuestionTemplate,
97 | SelectTemplateData{
98 | Select: *s,
99 | SelectedIndex: idx,
100 | ShowHelp: s.showingHelp,
101 | PageEntries: opts,
102 | },
103 | )
104 |
105 | // if we are not pressing ent
106 | return []rune(s.Options[s.selectedIndex]), 0, true
107 | }
108 |
109 | func (s *Select) Prompt() (interface{}, error) {
110 | // if there are no options to render
111 | if len(s.Options) == 0 {
112 | // we failed
113 | return "", errors.New("please provide options to select from")
114 | }
115 |
116 | // start off with the first option selected
117 | sel := 0
118 | // if there is a default
119 | if s.Default != "" {
120 | // find the choice
121 | for i, opt := range s.Options {
122 | // if the option correponds to the default
123 | if opt == s.Default {
124 | // we found our initial value
125 | sel = i
126 | // stop looking
127 | break
128 | }
129 | }
130 | }
131 | // save the selected index
132 | s.selectedIndex = sel
133 |
134 | // figure out the options and index to render
135 | opts, idx := paginate(s.PageSize, s.Options, sel)
136 |
137 | // ask the question
138 | err := s.Render(
139 | SelectQuestionTemplate,
140 | SelectTemplateData{
141 | Select: *s,
142 | PageEntries: opts,
143 | SelectedIndex: idx,
144 | },
145 | )
146 | if err != nil {
147 | return "", err
148 | }
149 |
150 | // hide the cursor
151 | terminal.CursorHide()
152 | // show the cursor when we're done
153 | defer terminal.CursorShow()
154 |
155 | // by default, use the default value
156 | s.useDefault = true
157 |
158 | rr := terminal.NewRuneReader(os.Stdin)
159 | rr.SetTermMode()
160 | defer rr.RestoreTermMode()
161 | // start waiting for input
162 | for {
163 | r, _, err := rr.ReadRune()
164 | if err != nil {
165 | return "", err
166 | }
167 | if r == '\r' || r == '\n' {
168 | break
169 | }
170 | if r == terminal.KeyInterrupt {
171 | return "", terminal.InterruptErr
172 | }
173 | if r == terminal.KeyEndTransmission {
174 | break
175 | }
176 | s.OnChange(nil, 0, r)
177 | }
178 |
179 | var val string
180 | // if we are supposed to use the default value
181 | if s.useDefault {
182 | // if there is a default value
183 | if s.Default != "" {
184 | // use the default value
185 | val = s.Default
186 | } else {
187 | // there is no default value so use the first
188 | val = s.Options[0]
189 | }
190 | // otherwise the selected index points to the value
191 | } else {
192 | // the
193 | val = s.Options[s.selectedIndex]
194 | }
195 |
196 | return val, err
197 | }
198 |
199 | func (s *Select) Cleanup(val interface{}) error {
200 | return s.Render(
201 | SelectQuestionTemplate,
202 | SelectTemplateData{
203 | Select: *s,
204 | Answer: val.(string),
205 | ShowAnswer: true,
206 | },
207 | )
208 | }
209 |
--------------------------------------------------------------------------------
/select_test.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/tj/survey/core"
9 | "github.com/tj/survey/terminal"
10 | )
11 |
12 | func init() {
13 | // disable color output for all prompts to simplify testing
14 | core.DisableColor = true
15 | }
16 |
17 | func TestSelectRender(t *testing.T) {
18 |
19 | prompt := Select{
20 | Message: "Pick your word:",
21 | Options: []string{"foo", "bar", "baz", "buz"},
22 | Default: "baz",
23 | }
24 |
25 | helpfulPrompt := prompt
26 | helpfulPrompt.Help = "This is helpful"
27 |
28 | tests := []struct {
29 | title string
30 | prompt Select
31 | data SelectTemplateData
32 | expected string
33 | }{
34 | {
35 | "Test Select question output",
36 | prompt,
37 | SelectTemplateData{SelectedIndex: 2, PageEntries: prompt.Options},
38 | `? Pick your word:
39 | foo
40 | bar
41 | ❯ baz
42 | buz
43 | `,
44 | },
45 | {
46 | "Test Select answer output",
47 | prompt,
48 | SelectTemplateData{Answer: "buz", ShowAnswer: true, PageEntries: prompt.Options},
49 | "? Pick your word: buz\n",
50 | },
51 | {
52 | "Test Select question output with help hidden",
53 | helpfulPrompt,
54 | SelectTemplateData{SelectedIndex: 2, PageEntries: prompt.Options},
55 | `? Pick your word: [? for help]
56 | foo
57 | bar
58 | ❯ baz
59 | buz
60 | `,
61 | },
62 | {
63 | "Test Select question output with help shown",
64 | helpfulPrompt,
65 | SelectTemplateData{SelectedIndex: 2, ShowHelp: true, PageEntries: prompt.Options},
66 | `ⓘ This is helpful
67 | ? Pick your word:
68 | foo
69 | bar
70 | ❯ baz
71 | buz
72 | `,
73 | },
74 | }
75 |
76 | outputBuffer := bytes.NewBufferString("")
77 | terminal.Stdout = outputBuffer
78 |
79 | for _, test := range tests {
80 | outputBuffer.Reset()
81 | test.data.Select = test.prompt
82 | err := test.prompt.Render(
83 | SelectQuestionTemplate,
84 | test.data,
85 | )
86 | assert.Nil(t, err, test.title)
87 | assert.Equal(t, test.expected, outputBuffer.String(), test.title)
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/survey.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/tj/survey/core"
7 | )
8 |
9 | // PageSize is the default maximum number of items to show in select/multiselect prompts
10 | var PageSize = 7
11 |
12 | // Validator is a function passed to a Question after a user has provided a response.
13 | // If the function returns an error, then the user will be prompted again for another
14 | // response.
15 | type Validator func(interface{}) error
16 |
17 | // Question is the core data structure for a survey questionnaire.
18 | type Question struct {
19 | Name string
20 | Prompt Prompt
21 | Validate Validator
22 | }
23 |
24 | // Prompt is the primary interface for the objects that can take user input
25 | // and return a response.
26 | type Prompt interface {
27 | Prompt() (interface{}, error)
28 | Cleanup(interface{}) error
29 | Error(error) error
30 | }
31 |
32 | /*
33 | AskOne performs the prompt for a single prompt and asks for validation if required.
34 | Response types should be something that can be casted from the response type designated
35 | in the documentation. For example:
36 |
37 | name := ""
38 | prompt := &survey.Input{
39 | Message: "name",
40 | }
41 |
42 | survey.AskOne(prompt, &name, nil)
43 |
44 | */
45 | func AskOne(p Prompt, response interface{}, v Validator) error {
46 | err := Ask([]*Question{{Prompt: p, Validate: v}}, response)
47 | if err != nil {
48 | return err
49 | }
50 |
51 | return nil
52 | }
53 |
54 | /*
55 | Ask performs the prompt loop, asking for validation when appropriate. The response
56 | type can be one of two options. If a struct is passed, the answer will be written to
57 | the field whose name matches the Name field on the corresponding question. Field types
58 | should be something that can be casted from the response type designated in the
59 | documentation. Note, a survey tag can also be used to identify a Otherwise, a
60 | map[string]interface{} can be passed, responses will be written to the key with the
61 | matching name. For example:
62 |
63 | qs := []*survey.Question{
64 | {
65 | Name: "name",
66 | Prompt: &survey.Input{Message: "What is your name?"},
67 | Validate: survey.Required,
68 | },
69 | }
70 |
71 | answers := struct{ Name string }{}
72 |
73 |
74 | err := survey.Ask(qs, &answers)
75 | */
76 | func Ask(qs []*Question, response interface{}) error {
77 |
78 | // if we weren't passed a place to record the answers
79 | if response == nil {
80 | // we can't go any further
81 | return errors.New("cannot call Ask() with a nil reference to record the answers")
82 | }
83 |
84 | // go over every question
85 | for _, q := range qs {
86 | // grab the user input and save it
87 | ans, err := q.Prompt.Prompt()
88 | // if there was a problem
89 | if err != nil {
90 | return err
91 | }
92 |
93 | // if there is a validate handler for this question
94 | if q.Validate != nil {
95 | // wait for a valid response
96 | for invalid := q.Validate(ans); invalid != nil; invalid = q.Validate(ans) {
97 | err := q.Prompt.Error(invalid)
98 | // if there was a problem
99 | if err != nil {
100 | return err
101 | }
102 |
103 | // ask for more input
104 | ans, err = q.Prompt.Prompt()
105 | // if there was a problem
106 | if err != nil {
107 | return err
108 | }
109 | }
110 | }
111 |
112 | // tell the prompt to cleanup with the validated value
113 | q.Prompt.Cleanup(ans)
114 |
115 | // if something went wrong
116 | if err != nil {
117 | // stop listening
118 | return err
119 | }
120 |
121 | // add it to the map
122 | err = core.WriteAnswer(response, q.Name, ans)
123 | // if something went wrong
124 | if err != nil {
125 | return err
126 | }
127 |
128 | }
129 | // return the response
130 | return nil
131 | }
132 |
133 | // paginate returns a single page of choices given the page size, the total list of
134 | // possible choices, and the current selected index in the total list.
135 | func paginate(page int, choices []string, sel int) ([]string, int) {
136 | // the number of elements to show in a single page
137 | var pageSize int
138 | // if the select has a specific page size
139 | if page != 0 {
140 | // use the specified one
141 | pageSize = page
142 | // otherwise the select does not have a page size
143 | } else {
144 | // use the package default
145 | pageSize = PageSize
146 | }
147 |
148 | var start, end, cursor int
149 |
150 | if len(choices) < pageSize {
151 | // if we dont have enough options to fill a page
152 | start = 0
153 | end = len(choices)
154 | cursor = sel
155 |
156 | } else if sel < pageSize/2 {
157 | // if we are in the first half page
158 | start = 0
159 | end = pageSize
160 | cursor = sel
161 |
162 | } else if len(choices)-sel-1 < pageSize/2 {
163 | // if we are in the last half page
164 | start = len(choices) - pageSize
165 | end = len(choices)
166 | cursor = sel - start
167 |
168 | } else {
169 | // somewhere in the middle
170 | above := pageSize / 2
171 | below := pageSize - above
172 |
173 | cursor = pageSize / 2
174 | start = sel - above
175 | end = sel + below
176 | }
177 |
178 | // return the subset we care about and the index
179 | return choices[start:end], cursor
180 | }
181 |
--------------------------------------------------------------------------------
/survey_test.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/tj/survey/core"
9 | )
10 |
11 | func init() {
12 | // disable color output for all prompts to simplify testing
13 | core.DisableColor = true
14 | }
15 |
16 | func TestValidationError(t *testing.T) {
17 |
18 | err := fmt.Errorf("Football is not a valid month")
19 |
20 | actual, err := core.RunTemplate(
21 | core.ErrorTemplate,
22 | err,
23 | )
24 | if err != nil {
25 | t.Errorf("Failed to run template to format error: %s", err)
26 | }
27 |
28 | expected := `✘ Sorry, your reply was invalid: Football is not a valid month
29 | `
30 |
31 | if actual != expected {
32 | t.Errorf("Formatted error was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected)
33 | }
34 | }
35 |
36 | func TestAsk_returnsErrorIfTargetIsNil(t *testing.T) {
37 | // pass an empty place to leave the answers
38 | err := Ask([]*Question{}, nil)
39 |
40 | // if we didn't get an error
41 | if err == nil {
42 | // the test failed
43 | t.Error("Did not encounter error when asking with no where to record.")
44 | }
45 | }
46 |
47 | func TestPagination_tooFew(t *testing.T) {
48 | // a small list of options
49 | choices := []string{"choice1", "choice2", "choice3"}
50 |
51 | // a page bigger than the total number
52 | pageSize := 4
53 | // the current selection
54 | sel := 3
55 |
56 | // compute the page info
57 | page, idx := paginate(pageSize, choices, sel)
58 |
59 | // make sure we see the full list of options
60 | assert.Equal(t, choices, page)
61 | // with the second index highlighted (no change)
62 | assert.Equal(t, 3, idx)
63 | }
64 |
65 | func TestPagination_firstHalf(t *testing.T) {
66 | // the choices for the test
67 | choices := []string{"choice1", "choice2", "choice3", "choice4", "choice5", "choice6"}
68 |
69 | // section the choices into groups of 4 so the choice is somewhere in the middle
70 | // to verify there is no displacement of the page
71 | pageSize := 4
72 | // test the second item
73 | sel := 2
74 |
75 | // compute the page info
76 | page, idx := paginate(pageSize, choices, sel)
77 |
78 | // we should see the first three options
79 | assert.Equal(t, choices[0:4], page)
80 | // with the second index highlighted
81 | assert.Equal(t, 2, idx)
82 | }
83 |
84 | func TestPagination_middle(t *testing.T) {
85 | // the choices for the test
86 | choices := []string{"choice0", "choice1", "choice2", "choice3", "choice4", "choice5"}
87 |
88 | // section the choices into groups of 3
89 | pageSize := 2
90 | // test the second item so that we can verify we are in the middle of the list
91 | sel := 3
92 |
93 | // compute the page info
94 | page, idx := paginate(pageSize, choices, sel)
95 |
96 | // we should see the first three options
97 | assert.Equal(t, choices[2:4], page)
98 | // with the second index highlighted
99 | assert.Equal(t, 1, idx)
100 | }
101 |
102 | func TestPagination_lastHalf(t *testing.T) {
103 | // the choices for the test
104 | choices := []string{"choice0", "choice1", "choice2", "choice3", "choice4", "choice5"}
105 |
106 | // section the choices into groups of 3
107 | pageSize := 3
108 | // test the last item to verify we're not in the middle
109 | sel := 5
110 |
111 | // compute the page info
112 | page, idx := paginate(pageSize, choices, sel)
113 |
114 | // we should see the first three options
115 | assert.Equal(t, choices[3:6], page)
116 | // we should be at the bottom of the list
117 | assert.Equal(t, 2, idx)
118 | }
119 |
--------------------------------------------------------------------------------
/terminal/cursor.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package terminal
4 |
5 | import (
6 | "bufio"
7 | "fmt"
8 | "os"
9 | "regexp"
10 | "strconv"
11 | "strings"
12 | )
13 |
14 | // CursorUp moves the cursor n cells to up.
15 | func CursorUp(n int) {
16 | fmt.Printf("\x1b[%dA", n)
17 | }
18 |
19 | // CursorDown moves the cursor n cells to down.
20 | func CursorDown(n int) {
21 | fmt.Printf("\x1b[%dB", n)
22 | }
23 |
24 | // CursorForward moves the cursor n cells to right.
25 | func CursorForward(n int) {
26 | fmt.Printf("\x1b[%dC", n)
27 | }
28 |
29 | // CursorBack moves the cursor n cells to left.
30 | func CursorBack(n int) {
31 | fmt.Printf("\x1b[%dD", n)
32 | }
33 |
34 | // CursorNextLine moves cursor to beginning of the line n lines down.
35 | func CursorNextLine(n int) {
36 | fmt.Printf("\x1b[%dE", n)
37 | }
38 |
39 | // CursorPreviousLine moves cursor to beginning of the line n lines up.
40 | func CursorPreviousLine(n int) {
41 | fmt.Printf("\x1b[%dF", n)
42 | }
43 |
44 | // CursorHorizontalAbsolute moves cursor horizontally to x.
45 | func CursorHorizontalAbsolute(x int) {
46 | fmt.Printf("\x1b[%dG", x)
47 | }
48 |
49 | // CursorShow shows the cursor.
50 | func CursorShow() {
51 | fmt.Print("\x1b[?25h")
52 | }
53 |
54 | // CursorHide hide the cursor.
55 | func CursorHide() {
56 | fmt.Print("\x1b[?25l")
57 | }
58 |
59 | // CursorMove moves the cursor to a specific x,y location.
60 | func CursorMove(x int, y int) {
61 | fmt.Printf("\x1b[%d;%df", x, y)
62 | }
63 |
64 | // CursorLocation returns the current location of the cursor in the terminal
65 | func CursorLocation() (*Coord, error) {
66 | // print the escape sequence to recieve the position in our stdin
67 | fmt.Print("\x1b[6n")
68 |
69 | // read from stdin to get the response
70 | reader := bufio.NewReader(os.Stdin)
71 | // spec says we read 'til R, so do that
72 | text, err := reader.ReadSlice('R')
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | // spec also says they're split by ;, so do that too
78 | if strings.Contains(string(text), ";") {
79 | // a regex to parse the output of the ansi code
80 | re := regexp.MustCompile(`\d+;\d+`)
81 | line := re.FindString(string(text))
82 |
83 | // find the column and rows embedded in the string
84 | coords := strings.Split(line, ";")
85 |
86 | // try to cast the col number to an int
87 | col, err := strconv.Atoi(coords[1])
88 | if err != nil {
89 | return nil, err
90 | }
91 |
92 | // try to cast the row number to an int
93 | row, err := strconv.Atoi(coords[0])
94 | if err != nil {
95 | return nil, err
96 | }
97 |
98 | // return the coordinate object with the col and row we calculated
99 | return &Coord{Short(col), Short(row)}, nil
100 | }
101 |
102 | // it didn't work so return an error
103 | return nil, fmt.Errorf("could not compute the cursor position using ascii escape sequences")
104 | }
105 |
106 | // Size returns the height and width of the terminal.
107 | func Size() (*Coord, error) {
108 | // the general approach here is to move the cursor to the very bottom
109 | // of the terminal, ask for the current location and then move the
110 | // cursor back where we started
111 |
112 | // save the current location of the cursor
113 | origin, err := CursorLocation()
114 | if err != nil {
115 | return nil, err
116 | }
117 |
118 | // move the cursor to the very bottom of the terminal
119 | CursorMove(999, 999)
120 |
121 | // ask for the current location
122 | bottom, err := CursorLocation()
123 | if err != nil {
124 | return nil, err
125 | }
126 |
127 | // move back where we began
128 | CursorUp(int(bottom.Y - origin.Y))
129 | CursorHorizontalAbsolute(int(origin.X))
130 |
131 | // sice the bottom was calcuated in the lower right corner, it
132 | // is the dimensions we are looking for
133 | return bottom, nil
134 | }
135 |
--------------------------------------------------------------------------------
/terminal/cursor_windows.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "os"
5 | "syscall"
6 | "unsafe"
7 | )
8 |
9 | func CursorUp(n int) {
10 | cursorMove(0, n)
11 | }
12 |
13 | func CursorDown(n int) {
14 | cursorMove(0, -1*n)
15 | }
16 |
17 | func CursorForward(n int) {
18 | cursorMove(n, 0)
19 | }
20 |
21 | func CursorBack(n int) {
22 | cursorMove(-1*n, 0)
23 | }
24 |
25 | func cursorMove(x int, y int) {
26 | handle := syscall.Handle(os.Stdout.Fd())
27 |
28 | var csbi consoleScreenBufferInfo
29 | procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
30 |
31 | var cursor Coord
32 | cursor.X = csbi.cursorPosition.X + Short(x)
33 | cursor.Y = csbi.cursorPosition.Y + Short(y)
34 |
35 | procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
36 | }
37 |
38 | func CursorNextLine(n int) {
39 | CursorUp(n)
40 | CursorHorizontalAbsolute(0)
41 | }
42 |
43 | func CursorPreviousLine(n int) {
44 | CursorDown(n)
45 | CursorHorizontalAbsolute(0)
46 | }
47 |
48 | func CursorHorizontalAbsolute(x int) {
49 | handle := syscall.Handle(os.Stdout.Fd())
50 |
51 | var csbi consoleScreenBufferInfo
52 | procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
53 |
54 | var cursor Coord
55 | cursor.X = Short(x)
56 | cursor.Y = csbi.cursorPosition.Y
57 |
58 | if csbi.size.X < cursor.X {
59 | cursor.X = csbi.size.X
60 | }
61 |
62 | procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
63 | }
64 |
65 | func CursorShow() {
66 | handle := syscall.Handle(os.Stdout.Fd())
67 |
68 | var cci consoleCursorInfo
69 | procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
70 | cci.visible = 1
71 |
72 | procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
73 | }
74 |
75 | func CursorHide() {
76 | handle := syscall.Handle(os.Stdout.Fd())
77 |
78 | var cci consoleCursorInfo
79 | procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
80 | cci.visible = 0
81 |
82 | procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
83 | }
84 |
85 | func CursorLocation() (Coord, error) {
86 | handle := syscall.Handle(os.Stdout.Fd())
87 |
88 | var csbi consoleScreenBufferInfo
89 | procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
90 |
91 | return csbi.cursorPosition, nil
92 | }
93 |
94 | func Size() (Coord, error) {
95 | handle := syscall.Handle(os.Stdout.Fd())
96 |
97 | var csbi consoleScreenBufferInfo
98 | procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
99 |
100 | return csbi.size, nil
101 | }
102 |
--------------------------------------------------------------------------------
/terminal/display.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | type EraseLineMode int
4 |
5 | const (
6 | ERASE_LINE_END EraseLineMode = iota
7 | ERASE_LINE_START
8 | ERASE_LINE_ALL
9 | )
10 |
--------------------------------------------------------------------------------
/terminal/display_posix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package terminal
4 |
5 | import (
6 | "fmt"
7 | )
8 |
9 | func EraseLine(mode EraseLineMode) {
10 | fmt.Printf("\x1b[%dK", mode)
11 | }
12 |
--------------------------------------------------------------------------------
/terminal/display_windows.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "os"
5 | "syscall"
6 | "unsafe"
7 | )
8 |
9 | func EraseLine(mode EraseLineMode) {
10 | handle := syscall.Handle(os.Stdout.Fd())
11 |
12 | var csbi consoleScreenBufferInfo
13 | procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
14 |
15 | var w uint32
16 | var x Short
17 | cursor := csbi.cursorPosition
18 | switch mode {
19 | case ERASE_LINE_END:
20 | x = csbi.size.X
21 | case ERASE_LINE_START:
22 | x = 0
23 | case ERASE_LINE_ALL:
24 | cursor.X = 0
25 | x = csbi.size.X
26 | }
27 | procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
28 | }
29 |
--------------------------------------------------------------------------------
/terminal/error.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var (
8 | InterruptErr = errors.New("interrupt")
9 | )
10 |
--------------------------------------------------------------------------------
/terminal/output.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package terminal
4 |
5 | import (
6 | "io"
7 | "os"
8 | )
9 |
10 | // Returns special stdout, which converts escape sequences to Windows API calls
11 | // on Windows environment.
12 | func NewAnsiStdout() io.Writer {
13 | return os.Stdout
14 | }
15 |
16 | // Returns special stderr, which converts escape sequences to Windows API calls
17 | // on Windows environment.
18 | func NewAnsiStderr() io.Writer {
19 | return os.Stderr
20 | }
21 |
--------------------------------------------------------------------------------
/terminal/output_windows.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "os"
8 | "strconv"
9 | "strings"
10 | "syscall"
11 | "unsafe"
12 |
13 | "github.com/mattn/go-isatty"
14 | )
15 |
16 | var (
17 | singleArgFunctions = map[rune]func(int){
18 | 'A': CursorUp,
19 | 'B': CursorDown,
20 | 'C': CursorForward,
21 | 'D': CursorBack,
22 | 'E': CursorNextLine,
23 | 'F': CursorPreviousLine,
24 | 'G': CursorHorizontalAbsolute,
25 | }
26 | )
27 |
28 | const (
29 | foregroundBlue = 0x1
30 | foregroundGreen = 0x2
31 | foregroundRed = 0x4
32 | foregroundIntensity = 0x8
33 | foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
34 | backgroundBlue = 0x10
35 | backgroundGreen = 0x20
36 | backgroundRed = 0x40
37 | backgroundIntensity = 0x80
38 | backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
39 | )
40 |
41 | type Writer struct {
42 | out io.Writer
43 | handle syscall.Handle
44 | orgAttr word
45 | }
46 |
47 | func NewAnsiStdout() io.Writer {
48 | var csbi consoleScreenBufferInfo
49 | out := os.Stdout
50 | if !isatty.IsTerminal(out.Fd()) {
51 | return out
52 | }
53 | handle := syscall.Handle(out.Fd())
54 | procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
55 | return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
56 | }
57 |
58 | func NewAnsiStderr() io.Writer {
59 | var csbi consoleScreenBufferInfo
60 | out := os.Stderr
61 | if !isatty.IsTerminal(out.Fd()) {
62 | return out
63 | }
64 | handle := syscall.Handle(out.Fd())
65 | procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
66 | return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
67 | }
68 |
69 | func (w *Writer) Write(data []byte) (n int, err error) {
70 | r := bytes.NewReader(data)
71 |
72 | for {
73 | ch, size, err := r.ReadRune()
74 | if err != nil {
75 | break
76 | }
77 | n += size
78 |
79 | switch ch {
80 | case '\x1b':
81 | size, err = w.handleEscape(r)
82 | n += size
83 | if err != nil {
84 | break
85 | }
86 | default:
87 | fmt.Fprint(w.out, string(ch))
88 | }
89 | }
90 | return
91 | }
92 |
93 | func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
94 | buf := make([]byte, 0, 10)
95 | buf = append(buf, "\x1b"...)
96 |
97 | // Check '[' continues after \x1b
98 | ch, size, err := r.ReadRune()
99 | if err != nil {
100 | fmt.Fprint(w.out, string(buf))
101 | return
102 | }
103 | n += size
104 | if ch != '[' {
105 | fmt.Fprint(w.out, string(buf))
106 | return
107 | }
108 |
109 | // Parse escape code
110 | var code rune
111 | argBuf := make([]byte, 0, 10)
112 | for {
113 | ch, size, err = r.ReadRune()
114 | if err != nil {
115 | fmt.Fprint(w.out, string(buf))
116 | return
117 | }
118 | n += size
119 | if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') {
120 | code = ch
121 | break
122 | }
123 | argBuf = append(argBuf, string(ch)...)
124 | }
125 |
126 | w.applyEscapeCode(buf, string(argBuf), code)
127 | return
128 | }
129 |
130 | func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) {
131 | switch arg + string(code) {
132 | case "?25h":
133 | CursorShow()
134 | return
135 | case "?25l":
136 | CursorHide()
137 | return
138 | }
139 |
140 | if f, ok := singleArgFunctions[code]; ok {
141 | if n, err := strconv.Atoi(arg); err == nil {
142 | f(n)
143 | return
144 | }
145 | }
146 |
147 | switch code {
148 | case 'm':
149 | w.applySelectGraphicRendition(arg)
150 | default:
151 | buf = append(buf, string(code)...)
152 | fmt.Fprint(w.out, string(buf))
153 | }
154 | }
155 |
156 | // Original implementation: https://github.com/mattn/go-colorable
157 | func (w *Writer) applySelectGraphicRendition(arg string) {
158 | if arg == "" {
159 | procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
160 | return
161 | }
162 |
163 | var csbi consoleScreenBufferInfo
164 | procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
165 | attr := csbi.attributes
166 |
167 | for _, param := range strings.Split(arg, ";") {
168 | n, err := strconv.Atoi(param)
169 | if err != nil {
170 | continue
171 | }
172 |
173 | switch {
174 | case n == 0 || n == 100:
175 | attr = w.orgAttr
176 | case 1 <= n && n <= 5:
177 | attr |= foregroundIntensity
178 | case 30 <= n && n <= 37:
179 | attr = (attr & backgroundMask)
180 | if (n-30)&1 != 0 {
181 | attr |= foregroundRed
182 | }
183 | if (n-30)&2 != 0 {
184 | attr |= foregroundGreen
185 | }
186 | if (n-30)&4 != 0 {
187 | attr |= foregroundBlue
188 | }
189 | case 40 <= n && n <= 47:
190 | attr = (attr & foregroundMask)
191 | if (n-40)&1 != 0 {
192 | attr |= backgroundRed
193 | }
194 | if (n-40)&2 != 0 {
195 | attr |= backgroundGreen
196 | }
197 | if (n-40)&4 != 0 {
198 | attr |= backgroundBlue
199 | }
200 | }
201 | }
202 |
203 | procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
204 | }
205 |
--------------------------------------------------------------------------------
/terminal/print.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | var (
8 | Stdout = NewAnsiStdout()
9 | )
10 |
11 | // Print prints given arguments with escape sequence conversion for windows.
12 | func Print(a ...interface{}) (n int, err error) {
13 | return fmt.Fprint(Stdout, a...)
14 | }
15 |
16 | // Printf prints a given format with escape sequence conversion for windows.
17 | func Printf(format string, a ...interface{}) (n int, err error) {
18 | return fmt.Fprintf(Stdout, format, a...)
19 | }
20 |
21 | // Println prints given arguments with newline and escape sequence conversion
22 | // for windows.
23 | func Println(a ...interface{}) (n int, err error) {
24 | return fmt.Fprintln(Stdout, a...)
25 | }
26 |
--------------------------------------------------------------------------------
/terminal/runereader.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "os"
5 | "unicode"
6 | )
7 |
8 | type RuneReader struct {
9 | Input *os.File
10 |
11 | state runeReaderState
12 | }
13 |
14 | func NewRuneReader(input *os.File) *RuneReader {
15 | return &RuneReader{
16 | Input: input,
17 | state: newRuneReaderState(input),
18 | }
19 | }
20 |
21 | func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) {
22 | line := []rune{}
23 |
24 | // we only care about horizontal displacements from the origin so start counting at 0
25 | index := 0
26 |
27 | for {
28 | // wait for some input
29 | r, _, err := rr.ReadRune()
30 | if err != nil {
31 | return line, err
32 | }
33 |
34 | // if the user pressed enter or some other newline/termination like ctrl+d
35 | if r == '\r' || r == '\n' || r == KeyEndTransmission {
36 | // go to the beginning of the next line
37 | Print("\r\n")
38 |
39 | // we're done processing the input
40 | return line, nil
41 | }
42 |
43 | // if the user interrupts (ie with ctrl+c)
44 | if r == KeyInterrupt {
45 | // go to the beginning of the next line
46 | Print("\r\n")
47 |
48 | // we're done processing the input, and treat interrupt like an error
49 | return line, InterruptErr
50 | }
51 |
52 | // allow for backspace/delete editing of inputs
53 | if r == KeyBackspace || r == KeyDelete {
54 | // and we're not at the beginning of the line
55 | if index > 0 && len(line) > 0 {
56 | // if we are at the end of the word
57 | if index == len(line) {
58 | // just remove the last letter from the internal representation
59 | line = line[:len(line)-1]
60 |
61 | // go back one
62 | CursorBack(1)
63 |
64 | // clear the rest of the line
65 | EraseLine(ERASE_LINE_END)
66 | } else {
67 | // we need to remove a character from the middle of the word
68 |
69 | // remove the current index from the list
70 | line = append(line[:index-1], line[index:]...)
71 |
72 | // go back one space so we can clear the rest
73 | CursorBack(1)
74 |
75 | // clear the rest of the line
76 | EraseLine(ERASE_LINE_END)
77 |
78 | // print what comes after
79 | Print(string(line[index-1:]))
80 |
81 | // leave the cursor where the user left it
82 | CursorBack(len(line) - index + 1)
83 | }
84 |
85 | // decrement the index
86 | index--
87 | } else {
88 | // otherwise the user pressed backspace while at the beginning of the line
89 | soundBell()
90 | }
91 |
92 | // we're done processing this key
93 | continue
94 | }
95 |
96 | // if the left arrow is pressed
97 | if r == KeyArrowLeft {
98 | // and we have space to the left
99 | if index > 0 {
100 | // move the cursor to the left
101 | CursorBack(1)
102 | // decrement the index
103 | index--
104 |
105 | } else {
106 | // otherwise we are at the beginning of where we started reading lines
107 | // sound the bell
108 | soundBell()
109 | }
110 |
111 | // we're done processing this key press
112 | continue
113 | }
114 |
115 | // if the right arrow is pressed
116 | if r == KeyArrowRight {
117 | // and we have space to the right of the word
118 | if index < len(line) {
119 | // move the cursor to the right
120 | CursorForward(1)
121 | // increment the index
122 | index++
123 |
124 | } else {
125 | // otherwise we are at the end of the word and can't go past
126 | // sound the bell
127 | soundBell()
128 | }
129 |
130 | // we're done processing this key press
131 | continue
132 | }
133 |
134 | // if the letter is another escape sequence
135 | if unicode.IsControl(r) {
136 | // ignore it
137 | continue
138 | }
139 |
140 | // the user pressed a regular key
141 |
142 | // if we are at the end of the line
143 | if index == len(line) {
144 | // just append the character at the end of the line
145 | line = append(line, r)
146 | // increment the location counter
147 | index++
148 |
149 | // if we don't need to mask the input
150 | if mask == 0 {
151 | // just print the character the user pressed
152 | Printf("%c", r)
153 | } else {
154 | // otherwise print the mask we were given
155 | Printf("%c", mask)
156 | }
157 | } else {
158 | // we are in the middle of the word so we need to insert the character the user pressed
159 | line = append(line[:index], append([]rune{r}, line[index:]...)...)
160 |
161 | // visually insert the character by deleting the rest of the line
162 | EraseLine(ERASE_LINE_END)
163 |
164 | // print the rest of the word after
165 | for _, char := range line[index:] {
166 | // if we don't need to mask the input
167 | if mask == 0 {
168 | // just print the character the user pressed
169 | Printf("%c", char)
170 | } else {
171 | // otherwise print the mask we were given
172 | Printf("%c", mask)
173 | }
174 | }
175 |
176 | // leave the cursor where the user left it
177 | CursorBack(len(line) - index - 1)
178 |
179 | // accomodate the new letter in our counter
180 | index++
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/terminal/runereader_bsd.go:
--------------------------------------------------------------------------------
1 | // copied from: https://github.com/golang/crypto/blob/master/ssh/terminal/util_bsd.go
2 | // Copyright 2013 The Go Authors. All rights reserved.
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file.
5 |
6 | // +build darwin dragonfly freebsd netbsd openbsd
7 |
8 | package terminal
9 |
10 | import "syscall"
11 |
12 | const ioctlReadTermios = syscall.TIOCGETA
13 | const ioctlWriteTermios = syscall.TIOCSETA
14 |
--------------------------------------------------------------------------------
/terminal/runereader_linux.go:
--------------------------------------------------------------------------------
1 | // copied from https://github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go
2 | // Copyright 2013 The Go Authors. All rights reserved.
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file.
5 |
6 | package terminal
7 |
8 | // These constants are declared here, rather than importing
9 | // them from the syscall package as some syscall packages, even
10 | // on linux, for example gccgo, do not declare them.
11 | const ioctlReadTermios = 0x5401 // syscall.TCGETS
12 | const ioctlWriteTermios = 0x5402 // syscall.TCSETS
13 |
--------------------------------------------------------------------------------
/terminal/runereader_posix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | // The terminal mode manipluation code is derived heavily from:
4 | // https://github.com/golang/crypto/blob/master/ssh/terminal/util.go:
5 | // Copyright 2011 The Go Authors. All rights reserved.
6 | // Use of this source code is governed by a BSD-style
7 | // license that can be found in the LICENSE file.
8 |
9 | package terminal
10 |
11 | import (
12 | "bufio"
13 | "fmt"
14 | "os"
15 | "syscall"
16 | "unsafe"
17 | )
18 |
19 | type runeReaderState struct {
20 | term syscall.Termios
21 | buf *bufio.Reader
22 | }
23 |
24 | func newRuneReaderState(input *os.File) runeReaderState {
25 | return runeReaderState{
26 | buf: bufio.NewReader(input),
27 | }
28 | }
29 |
30 | // For reading runes we just want to disable echo.
31 | func (rr *RuneReader) SetTermMode() error {
32 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.Input.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 {
33 | return err
34 | }
35 |
36 | newState := rr.state.term
37 | newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG
38 |
39 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.Input.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 {
40 | return err
41 | }
42 |
43 | return nil
44 | }
45 |
46 | func (rr *RuneReader) RestoreTermMode() error {
47 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.Input.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 {
48 | return err
49 | }
50 | return nil
51 | }
52 |
53 | func (rr *RuneReader) ReadRune() (rune, int, error) {
54 | r, size, err := rr.state.buf.ReadRune()
55 | if err != nil {
56 | return r, size, err
57 | }
58 | // parse ^[ sequences to look for arrow keys
59 | if r == '\033' {
60 | r, size, err = rr.state.buf.ReadRune()
61 | if err != nil {
62 | return r, size, err
63 | }
64 | if r != '[' {
65 | return r, size, fmt.Errorf("Unexpected Escape Sequence: %q", []rune{'\033', r})
66 | }
67 | r, size, err = rr.state.buf.ReadRune()
68 | if err != nil {
69 | return r, size, err
70 | }
71 | switch r {
72 | case 'D':
73 | return KeyArrowLeft, 1, nil
74 | case 'C':
75 | return KeyArrowRight, 1, nil
76 | case 'A':
77 | return KeyArrowUp, 1, nil
78 | case 'B':
79 | return KeyArrowDown, 1, nil
80 | }
81 | return r, size, fmt.Errorf("Unknown Escape Sequence: %q", []rune{'\033', '[', r})
82 | }
83 | return r, size, err
84 | }
85 |
--------------------------------------------------------------------------------
/terminal/runereader_windows.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "os"
5 | "syscall"
6 | "unsafe"
7 | )
8 |
9 | var (
10 | dll = syscall.NewLazyDLL("kernel32.dll")
11 | setConsoleMode = dll.NewProc("SetConsoleMode")
12 | getConsoleMode = dll.NewProc("GetConsoleMode")
13 | readConsoleInput = dll.NewProc("ReadConsoleInputW")
14 | )
15 |
16 | const (
17 | EVENT_KEY = 0x0001
18 |
19 | // key codes for arrow keys
20 | // https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
21 | VK_LEFT = 0x25
22 | VK_UP = 0x26
23 | VK_RIGHT = 0x27
24 | VK_DOWN = 0x28
25 |
26 | RIGHT_CTRL_PRESSED = 0x0004
27 | LEFT_CTRL_PRESSED = 0x0008
28 |
29 | ENABLE_ECHO_INPUT uint32 = 0x0004
30 | ENABLE_LINE_INPUT uint32 = 0x0002
31 | ENABLE_PROCESSED_INPUT uint32 = 0x0001
32 | )
33 |
34 | type inputRecord struct {
35 | eventType uint16
36 | padding uint16
37 | event [16]byte
38 | }
39 |
40 | type keyEventRecord struct {
41 | bKeyDown int32
42 | wRepeatCount uint16
43 | wVirtualKeyCode uint16
44 | wVirtualScanCode uint16
45 | unicodeChar uint16
46 | wdControlKeyState uint32
47 | }
48 |
49 | type runeReaderState struct {
50 | term uint32
51 | }
52 |
53 | func newRuneReaderState(input *os.File) runeReaderState {
54 | return runeReaderState{}
55 | }
56 |
57 | func (rr *RuneReader) SetTermMode() error {
58 | r, _, err := getConsoleMode.Call(uintptr(rr.Input.Fd()), uintptr(unsafe.Pointer(&rr.state.term)))
59 | // windows return 0 on error
60 | if r == 0 {
61 | return err
62 | }
63 |
64 | newState := rr.state.term
65 | newState &^= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT
66 | r, _, err = setConsoleMode.Call(uintptr(rr.Input.Fd()), uintptr(newState))
67 | // windows return 0 on error
68 | if r == 0 {
69 | return err
70 | }
71 | return nil
72 | }
73 |
74 | func (rr *RuneReader) RestoreTermMode() error {
75 | r, _, err := setConsoleMode.Call(uintptr(rr.Input.Fd()), uintptr(rr.state.term))
76 | // windows return 0 on error
77 | if r == 0 {
78 | return err
79 | }
80 | return nil
81 | }
82 |
83 | func (rr *RuneReader) ReadRune() (rune, int, error) {
84 | ir := &inputRecord{}
85 | bytesRead := 0
86 | for {
87 | rv, _, e := readConsoleInput.Call(rr.Input.Fd(), uintptr(unsafe.Pointer(ir)), 1, uintptr(unsafe.Pointer(&bytesRead)))
88 | // windows returns non-zero to indicate success
89 | if rv == 0 && e != nil {
90 | return 0, 0, e
91 | }
92 |
93 | if ir.eventType != EVENT_KEY {
94 | continue
95 | }
96 |
97 | // the event data is really a c struct union, so here we have to do an usafe
98 | // cast to put the data into the keyEventRecord (since we have already verified
99 | // above that this event does correspond to a key event
100 | key := (*keyEventRecord)(unsafe.Pointer(&ir.event[0]))
101 | // we only care about key down events
102 | if key.bKeyDown == 0 {
103 | continue
104 | }
105 | if key.wdControlKeyState&(LEFT_CTRL_PRESSED|RIGHT_CTRL_PRESSED) != 0 && key.unicodeChar == 'C' {
106 | return KeyInterrupt, bytesRead, nil
107 | }
108 |
109 | // not a normal character so look up the input sequence from the
110 | // virtual key code mappings (VK_*)
111 | if key.unicodeChar == 0 {
112 | switch key.wVirtualKeyCode {
113 | case VK_DOWN:
114 | return KeyArrowDown, bytesRead, nil
115 | case VK_LEFT:
116 | return KeyArrowLeft, bytesRead, nil
117 | case VK_RIGHT:
118 | return KeyArrowRight, bytesRead, nil
119 | case VK_UP:
120 | return KeyArrowUp, bytesRead, nil
121 | default:
122 | // not a virtual key that we care about so just continue on to
123 | // the next input key
124 | continue
125 | }
126 | }
127 | r := rune(key.unicodeChar)
128 | return r, bytesRead, nil
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/terminal/sequences.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | const (
4 | KeyArrowLeft = '\x02'
5 | KeyArrowRight = '\x06'
6 | KeyArrowUp = '\x10'
7 | KeyArrowDown = '\x0e'
8 | KeySpace = ' '
9 | KeyEnter = '\r'
10 | KeyBackspace = '\b'
11 | KeyDelete = '\x7f'
12 | KeyInterrupt = '\x03'
13 | KeyEndTransmission = '\x04'
14 | )
15 |
16 | func soundBell() {
17 | Print("\a")
18 | }
19 |
--------------------------------------------------------------------------------
/terminal/syscall_windows.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "syscall"
5 | )
6 |
7 | var (
8 | kernel32 = syscall.NewLazyDLL("kernel32.dll")
9 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
10 | procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute")
11 | procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
12 | procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
13 | procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
14 | procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
15 | )
16 |
17 | type wchar uint16
18 | type dword uint32
19 | type word uint16
20 |
21 | type smallRect struct {
22 | left Short
23 | top Short
24 | right Short
25 | bottom Short
26 | }
27 |
28 | type consoleScreenBufferInfo struct {
29 | size Coord
30 | cursorPosition Coord
31 | attributes word
32 | window smallRect
33 | maximumWindowSize Coord
34 | }
35 |
36 | type consoleCursorInfo struct {
37 | size dword
38 | visible int32
39 | }
40 |
--------------------------------------------------------------------------------
/terminal/terminal.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | type Short int16
4 |
5 | type Coord struct {
6 | X Short
7 | Y Short
8 | }
9 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # survey/tests
2 |
3 | Because of the nature of this library, I was having a hard time finding a reliable
4 | way to run unit tests, therefore I decided to try to create a suite
5 | of integration tests which must be run successfully before a PR can be merged.
6 | I will try to add to this suite as new edge cases are known.
7 |
--------------------------------------------------------------------------------
/tests/ask.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/tj/survey"
7 | )
8 |
9 | // the questions to ask
10 | var simpleQs = []*survey.Question{
11 | {
12 | Name: "name",
13 | Prompt: &survey.Input{
14 | Message: "What is your name?",
15 | Default: "Johnny Appleseed",
16 | },
17 | },
18 | {
19 | Name: "color",
20 | Prompt: &survey.Select{
21 | Message: "Choose a color:",
22 | Options: []string{"red", "blue", "green", "yellow"},
23 | Default: "yellow",
24 | },
25 | Validate: survey.Required,
26 | },
27 | }
28 |
29 | func main() {
30 |
31 | fmt.Println("Asking many.")
32 | // a place to store the answers
33 | ans := struct {
34 | Name string
35 | Color string
36 | }{}
37 | err := survey.Ask(simpleQs, &ans)
38 | if err != nil {
39 | fmt.Println(err.Error())
40 | return
41 | }
42 |
43 | fmt.Println("Asking one.")
44 | answer := ""
45 | err = survey.AskOne(simpleQs[0].Prompt, &answer, nil)
46 | if err != nil {
47 | fmt.Println(err.Error())
48 | return
49 | }
50 | fmt.Printf("Answered with %v.\n", answer)
51 |
52 | fmt.Println("Asking one with validation.")
53 | vAns := ""
54 | err = survey.AskOne(&survey.Input{Message: "What is your name?"}, &vAns, survey.Required)
55 | if err != nil {
56 | fmt.Println(err.Error())
57 | return
58 | }
59 | fmt.Printf("Answered with %v.\n", vAns)
60 | }
61 |
--------------------------------------------------------------------------------
/tests/autoplay/ask.go:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE!
3 | //
4 | // This file was automatically generated via the commands:
5 | //
6 | // go get github.com/coryb/autoplay
7 | // autoplay -n autoplay/ask.go go run ask.go
8 | //
9 | ////////////////////////////////////////////////////////////////////////////////
10 | package main
11 |
12 | import (
13 | "bufio"
14 | "fmt"
15 | "github.com/kr/pty"
16 | "os"
17 | "os/exec"
18 | "strconv"
19 | "strings"
20 | )
21 |
22 | const (
23 | RED = "\033[31m"
24 | RESET = "\033[0m"
25 | )
26 |
27 | func main() {
28 | fh, tty, _ := pty.Open()
29 | defer tty.Close()
30 | defer fh.Close()
31 | c := exec.Command("go", "run", "ask.go")
32 | c.Stdin = tty
33 | c.Stdout = tty
34 | c.Stderr = tty
35 | c.Start()
36 | buf := bufio.NewReaderSize(fh, 1024)
37 |
38 | expect("Asking many.\r\n", buf)
39 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your name? \x1b[0m\x1b[37m(Johnny Appleseed) \x1b[0m", buf)
40 | fh.Write([]byte("L"))
41 | expect("L", buf)
42 | fh.Write([]byte("a"))
43 | expect("a", buf)
44 | fh.Write([]byte("r"))
45 | expect("r", buf)
46 | fh.Write([]byte("r"))
47 | expect("r", buf)
48 | fh.Write([]byte("y"))
49 | expect("y", buf)
50 | fh.Write([]byte(" "))
51 | expect(" ", buf)
52 | fh.Write([]byte("B"))
53 | expect("B", buf)
54 | fh.Write([]byte("i"))
55 | expect("i", buf)
56 | fh.Write([]byte("r"))
57 | expect("r", buf)
58 | fh.Write([]byte("d"))
59 | expect("d", buf)
60 | fh.Write([]byte("\r"))
61 | expect("\r\r\n", buf)
62 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your name? \x1b[0m\x1b[36mLarry Bird\x1b[0m\r\n", buf)
63 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\r\n", buf)
64 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
65 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
66 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
67 | expect("\x1b[1;36m❯ yellow\x1b[0m\r\n", buf)
68 | expect("\x1b[?25l", buf)
69 | fh.Write([]byte("\x1b"))
70 | fh.Write([]byte("["))
71 | fh.Write([]byte("A"))
72 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\r\n", buf)
73 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
74 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
75 | expect("\x1b[1;36m❯ green\x1b[0m\r\n", buf)
76 | expect("\x1b[1;99m yellow\x1b[0m\r\n", buf)
77 | fh.Write([]byte("\x1b"))
78 | fh.Write([]byte("["))
79 | fh.Write([]byte("A"))
80 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\r\n", buf)
81 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
82 | expect("\x1b[1;36m❯ blue\x1b[0m\r\n", buf)
83 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
84 | expect("\x1b[1;99m yellow\x1b[0m\r\n", buf)
85 | fh.Write([]byte("\r"))
86 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\x1b[36m blue\x1b[0m\r\n", buf)
87 | expect("Asking one.\r\n", buf)
88 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your name? \x1b[0m\x1b[37m(Johnny Appleseed) \x1b[0m", buf)
89 | fh.Write([]byte("L"))
90 | expect("L", buf)
91 | fh.Write([]byte("a"))
92 | expect("a", buf)
93 | fh.Write([]byte("r"))
94 | expect("r", buf)
95 | fh.Write([]byte("r"))
96 | expect("r", buf)
97 | fh.Write([]byte("y"))
98 | expect("y", buf)
99 | fh.Write([]byte(" "))
100 | expect(" ", buf)
101 | fh.Write([]byte("K"))
102 | expect("K", buf)
103 | fh.Write([]byte("i"))
104 | expect("i", buf)
105 | fh.Write([]byte("n"))
106 | expect("n", buf)
107 | fh.Write([]byte("g"))
108 | expect("g", buf)
109 | fh.Write([]byte("\r"))
110 | expect("\r\r\n", buf)
111 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your name? \x1b[0m\x1b[36mLarry King\x1b[0m\r\n", buf)
112 | expect("Answered with Larry King.\r\n", buf)
113 | expect("Asking one with validation.\r\n", buf)
114 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your name? \x1b[0m", buf)
115 | fh.Write([]byte("\r"))
116 | expect("\r\r\n", buf)
117 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[31m✘ Sorry, your reply was invalid: Value is required\x1b[0m\r\n", buf)
118 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your name? \x1b[0m", buf)
119 | fh.Write([]byte("L"))
120 | expect("L", buf)
121 | fh.Write([]byte("a"))
122 | expect("a", buf)
123 | fh.Write([]byte("r"))
124 | expect("r", buf)
125 | fh.Write([]byte("r"))
126 | expect("r", buf)
127 | fh.Write([]byte("y"))
128 | expect("y", buf)
129 | fh.Write([]byte(" "))
130 | expect(" ", buf)
131 | fh.Write([]byte("W"))
132 | expect("W", buf)
133 | fh.Write([]byte("a"))
134 | expect("a", buf)
135 | fh.Write([]byte("l"))
136 | expect("l", buf)
137 | fh.Write([]byte("l"))
138 | expect("l", buf)
139 | fh.Write([]byte("\r"))
140 | expect("\r\r\n", buf)
141 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your name? \x1b[0m\x1b[36mLarry Wall\x1b[0m\r\n", buf)
142 | expect("Answered with Larry Wall.\r\n", buf)
143 |
144 | c.Wait()
145 | tty.Close()
146 | fh.Close()
147 | }
148 |
149 | func expect(expected string, buf *bufio.Reader) {
150 | sofar := []rune{}
151 | for _, r := range expected {
152 | got, _, _ := buf.ReadRune()
153 | sofar = append(sofar, got)
154 | if got != r {
155 | fmt.Fprintln(os.Stderr, RESET)
156 |
157 | // we want to quote the string but we also want to make the unexpected character RED
158 | // so we use the strconv.Quote function but trim off the quoted characters so we can
159 | // merge multiple quoted strings into one.
160 | expStart := strings.TrimSuffix(strconv.Quote(expected[:len(sofar)-1]), "\"")
161 | expMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(expected[len(sofar)-1])), "\""), "\"")
162 | expEnd := strings.TrimPrefix(strconv.Quote(expected[len(sofar):]), "\"")
163 |
164 | fmt.Fprintf(os.Stderr, "Expected: %s%s%s%s%s\n", expStart, RED, expMiss, RESET, expEnd)
165 |
166 | // read the rest of the buffer
167 | p := make([]byte, buf.Buffered())
168 | buf.Read(p)
169 |
170 | gotStart := strings.TrimSuffix(strconv.Quote(string(sofar[:len(sofar)-1])), "\"")
171 | gotMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(sofar[len(sofar)-1])), "\""), "\"")
172 | gotEnd := strings.TrimPrefix(strconv.Quote(string(p)), "\"")
173 |
174 | fmt.Fprintf(os.Stderr, "Got: %s%s%s%s%s\n", gotStart, RED, gotMiss, RESET, gotEnd)
175 | panic(fmt.Errorf("Unexpected Rune %q, Expected %q\n", got, r))
176 | } else {
177 | fmt.Printf("%c", r)
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/tests/autoplay/confirm.go:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE!
3 | //
4 | // This file was automatically generated via the commands:
5 | //
6 | // go get github.com/coryb/autoplay
7 | // autoplay -n autoplay/confirm.go go run confirm.go
8 | //
9 | ////////////////////////////////////////////////////////////////////////////////
10 | package main
11 |
12 | import (
13 | "bufio"
14 | "fmt"
15 | "github.com/kr/pty"
16 | "os"
17 | "os/exec"
18 | "strconv"
19 | "strings"
20 | )
21 |
22 | const (
23 | RED = "\033[31m"
24 | RESET = "\033[0m"
25 | )
26 |
27 | func main() {
28 | fh, tty, _ := pty.Open()
29 | defer tty.Close()
30 | defer fh.Close()
31 | c := exec.Command("go", "run", "confirm.go")
32 | c.Stdin = tty
33 | c.Stdout = tty
34 | c.Stderr = tty
35 | c.Start()
36 | buf := bufio.NewReaderSize(fh, 1024)
37 |
38 | expect("Enter 'yes'\r\n", buf)
39 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[37m(y/N) \x1b[0m", buf)
40 | fh.Write([]byte("y"))
41 | expect("y", buf)
42 | fh.Write([]byte("e"))
43 | expect("e", buf)
44 | fh.Write([]byte("s"))
45 | expect("s", buf)
46 | fh.Write([]byte("\r"))
47 | expect("\r\r\n", buf)
48 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[36mYes\x1b[0m\r\n", buf)
49 | expect("Answered true.\r\n", buf)
50 | expect("---------------------\r\n", buf)
51 | expect("Enter 'no'\r\n", buf)
52 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[37m(y/N) \x1b[0m", buf)
53 | fh.Write([]byte("n"))
54 | expect("n", buf)
55 | fh.Write([]byte("o"))
56 | expect("o", buf)
57 | fh.Write([]byte("\r"))
58 | expect("\r\r\n", buf)
59 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[36mNo\x1b[0m\r\n", buf)
60 | expect("Answered false.\r\n", buf)
61 | expect("---------------------\r\n", buf)
62 | expect("default\r\n", buf)
63 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[37m(Y/n) \x1b[0m", buf)
64 | fh.Write([]byte("\r"))
65 | expect("\r\r\n", buf)
66 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[36mYes\x1b[0m\r\n", buf)
67 | expect("Answered true.\r\n", buf)
68 | expect("---------------------\r\n", buf)
69 | expect("not recognized (enter random letter)\r\n", buf)
70 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[37m(Y/n) \x1b[0m", buf)
71 | fh.Write([]byte("x"))
72 | expect("x", buf)
73 | fh.Write([]byte("\r"))
74 | expect("\r\r\n", buf)
75 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[31m✘ Sorry, your reply was invalid: \"x\" is not a valid answer, please try again.\x1b[0m\r\n", buf)
76 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[37m(Y/n) \x1b[0m", buf)
77 | fh.Write([]byte("\r"))
78 | expect("\r\r\n", buf)
79 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[36mYes\x1b[0m\r\n", buf)
80 | expect("Answered true.\r\n", buf)
81 | expect("---------------------\r\n", buf)
82 | expect("no help - type '?'\r\n", buf)
83 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[37m(Y/n) \x1b[0m", buf)
84 | fh.Write([]byte("?"))
85 | expect("?", buf)
86 | fh.Write([]byte("\r"))
87 | expect("\r\r\n", buf)
88 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[31m✘ Sorry, your reply was invalid: \"?\" is not a valid answer, please try again.\x1b[0m\r\n", buf)
89 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[37m(Y/n) \x1b[0m", buf)
90 | fh.Write([]byte("\r"))
91 | expect("\r\r\n", buf)
92 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99myes: \x1b[0m\x1b[36mYes\x1b[0m\r\n", buf)
93 | expect("Answered true.\r\n", buf)
94 | expect("---------------------\r\n", buf)
95 |
96 | c.Wait()
97 | tty.Close()
98 | fh.Close()
99 | }
100 |
101 | func expect(expected string, buf *bufio.Reader) {
102 | sofar := []rune{}
103 | for _, r := range expected {
104 | got, _, _ := buf.ReadRune()
105 | sofar = append(sofar, got)
106 | if got != r {
107 | fmt.Fprintln(os.Stderr, RESET)
108 |
109 | // we want to quote the string but we also want to make the unexpected character RED
110 | // so we use the strconv.Quote function but trim off the quoted characters so we can
111 | // merge multiple quoted strings into one.
112 | expStart := strings.TrimSuffix(strconv.Quote(expected[:len(sofar)-1]), "\"")
113 | expMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(expected[len(sofar)-1])), "\""), "\"")
114 | expEnd := strings.TrimPrefix(strconv.Quote(expected[len(sofar):]), "\"")
115 |
116 | fmt.Fprintf(os.Stderr, "Expected: %s%s%s%s%s\n", expStart, RED, expMiss, RESET, expEnd)
117 |
118 | // read the rest of the buffer
119 | p := make([]byte, buf.Buffered())
120 | buf.Read(p)
121 |
122 | gotStart := strings.TrimSuffix(strconv.Quote(string(sofar[:len(sofar)-1])), "\"")
123 | gotMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(sofar[len(sofar)-1])), "\""), "\"")
124 | gotEnd := strings.TrimPrefix(strconv.Quote(string(p)), "\"")
125 |
126 | fmt.Fprintf(os.Stderr, "Got: %s%s%s%s%s\n", gotStart, RED, gotMiss, RESET, gotEnd)
127 | panic(fmt.Errorf("Unexpected Rune %q, Expected %q\n", got, r))
128 | } else {
129 | fmt.Printf("%c", r)
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/tests/autoplay/doubleSelect.go:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE!
3 | //
4 | // This file was automatically generated via the commands:
5 | //
6 | // go get github.com/coryb/autoplay
7 | // autoplay -n autoplay/doubleSelect.go go run doubleSelect.go
8 | //
9 | ////////////////////////////////////////////////////////////////////////////////
10 | package main
11 |
12 | import (
13 | "bufio"
14 | "fmt"
15 | "github.com/kr/pty"
16 | "os"
17 | "os/exec"
18 | "strconv"
19 | "strings"
20 | )
21 |
22 | const (
23 | RED = "\033[31m"
24 | RESET = "\033[0m"
25 | )
26 |
27 | func main() {
28 | fh, tty, _ := pty.Open()
29 | defer tty.Close()
30 | defer fh.Close()
31 | c := exec.Command("go", "run", "doubleSelect.go")
32 | c.Stdin = tty
33 | c.Stdout = tty
34 | c.Stderr = tty
35 | c.Start()
36 | buf := bufio.NewReaderSize(fh, 1024)
37 |
38 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mselect1:\x1b[0m\r\n", buf)
39 | expect("\x1b[1;36m❯ red\x1b[0m\r\n", buf)
40 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
41 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
42 | expect("\x1b[?25l", buf)
43 | fh.Write([]byte("\x1b"))
44 | fh.Write([]byte("["))
45 | fh.Write([]byte("B"))
46 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mselect1:\x1b[0m\r\n", buf)
47 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
48 | expect("\x1b[1;36m❯ blue\x1b[0m\r\n", buf)
49 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
50 | fh.Write([]byte("\r"))
51 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mselect1:\x1b[0m\x1b[36m blue\x1b[0m\r\n", buf)
52 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mselect2:\x1b[0m\r\n", buf)
53 | expect("\x1b[1;36m❯ red\x1b[0m\r\n", buf)
54 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
55 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
56 | expect("\x1b[?25l", buf)
57 | fh.Write([]byte("\x1b"))
58 | fh.Write([]byte("["))
59 | fh.Write([]byte("B"))
60 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mselect2:\x1b[0m\r\n", buf)
61 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
62 | expect("\x1b[1;36m❯ blue\x1b[0m\r\n", buf)
63 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
64 | fh.Write([]byte("\r"))
65 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mselect2:\x1b[0m\x1b[36m blue\x1b[0m\r\n", buf)
66 | expect("blue and blue.\r\n", buf)
67 |
68 | c.Wait()
69 | tty.Close()
70 | fh.Close()
71 | }
72 |
73 | func expect(expected string, buf *bufio.Reader) {
74 | sofar := []rune{}
75 | for _, r := range expected {
76 | got, _, _ := buf.ReadRune()
77 | sofar = append(sofar, got)
78 | if got != r {
79 | fmt.Fprintln(os.Stderr, RESET)
80 |
81 | // we want to quote the string but we also want to make the unexpected character RED
82 | // so we use the strconv.Quote function but trim off the quoted characters so we can
83 | // merge multiple quoted strings into one.
84 | expStart := strings.TrimSuffix(strconv.Quote(expected[:len(sofar)-1]), "\"")
85 | expMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(expected[len(sofar)-1])), "\""), "\"")
86 | expEnd := strings.TrimPrefix(strconv.Quote(expected[len(sofar):]), "\"")
87 |
88 | fmt.Fprintf(os.Stderr, "Expected: %s%s%s%s%s\n", expStart, RED, expMiss, RESET, expEnd)
89 |
90 | // read the rest of the buffer
91 | p := make([]byte, buf.Buffered())
92 | buf.Read(p)
93 |
94 | gotStart := strings.TrimSuffix(strconv.Quote(string(sofar[:len(sofar)-1])), "\"")
95 | gotMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(sofar[len(sofar)-1])), "\""), "\"")
96 | gotEnd := strings.TrimPrefix(strconv.Quote(string(p)), "\"")
97 |
98 | fmt.Fprintf(os.Stderr, "Got: %s%s%s%s%s\n", gotStart, RED, gotMiss, RESET, gotEnd)
99 | panic(fmt.Errorf("Unexpected Rune %q, Expected %q\n", got, r))
100 | } else {
101 | fmt.Printf("%c", r)
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/tests/autoplay/help.go:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE!
3 | //
4 | // This file was automatically generated via the commands:
5 | //
6 | // go get github.com/coryb/autoplay
7 | // autoplay -n autoplay/help.go go run help.go
8 | //
9 | ////////////////////////////////////////////////////////////////////////////////
10 | package main
11 |
12 | import (
13 | "bufio"
14 | "fmt"
15 | "github.com/kr/pty"
16 | "os"
17 | "os/exec"
18 | "strconv"
19 | "strings"
20 | )
21 |
22 | const (
23 | RED = "\033[31m"
24 | RESET = "\033[0m"
25 | )
26 |
27 | func main() {
28 | fh, tty, _ := pty.Open()
29 | defer tty.Close()
30 | defer fh.Close()
31 | c := exec.Command("go", "run", "help.go")
32 | c.Stdin = tty
33 | c.Stdout = tty
34 | c.Stderr = tty
35 | c.Start()
36 | buf := bufio.NewReaderSize(fh, 1024)
37 |
38 | expect("confirm\r\n", buf)
39 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mIs it raining? \x1b[0m\x1b[36m[? for help]\x1b[0m \x1b[37m(y/N) \x1b[0m", buf)
40 | fh.Write([]byte("?"))
41 | expect("?", buf)
42 | fh.Write([]byte("\r"))
43 | expect("\r\r\n", buf)
44 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[36mⓘ Go outside, if your head becomes wet the answer is probably 'yes'\x1b[0m\r\n", buf)
45 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mIs it raining? \x1b[0m\x1b[37m(y/N) \x1b[0m", buf)
46 | fh.Write([]byte("y"))
47 | expect("y", buf)
48 | fh.Write([]byte("e"))
49 | expect("e", buf)
50 | fh.Write([]byte("s"))
51 | expect("s", buf)
52 | fh.Write([]byte("\r"))
53 | expect("\r\r\n", buf)
54 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mIs it raining? \x1b[0m\x1b[36mYes\x1b[0m\r\n", buf)
55 | expect("Answered true.\r\n", buf)
56 | expect("---------------------\r\n", buf)
57 | expect("input\r\n", buf)
58 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your phone number: \x1b[0m\x1b[36m[? for help]\x1b[0m ", buf)
59 | fh.Write([]byte("?"))
60 | expect("?", buf)
61 | fh.Write([]byte("\r"))
62 | expect("\r\r\n", buf)
63 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[36mⓘ Phone number should include the area code, parentheses optional\x1b[0m\r\n", buf)
64 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your phone number: \x1b[0m", buf)
65 | fh.Write([]byte("1"))
66 | expect("1", buf)
67 | fh.Write([]byte("2"))
68 | expect("2", buf)
69 | fh.Write([]byte("3"))
70 | expect("3", buf)
71 | fh.Write([]byte("-"))
72 | expect("-", buf)
73 | fh.Write([]byte("1"))
74 | expect("1", buf)
75 | fh.Write([]byte("2"))
76 | expect("2", buf)
77 | fh.Write([]byte("3"))
78 | expect("3", buf)
79 | fh.Write([]byte("-"))
80 | expect("-", buf)
81 | fh.Write([]byte("1"))
82 | expect("1", buf)
83 | fh.Write([]byte("2"))
84 | expect("2", buf)
85 | fh.Write([]byte("3"))
86 | expect("3", buf)
87 | fh.Write([]byte("4"))
88 | expect("4", buf)
89 | fh.Write([]byte("\r"))
90 | expect("\r\r\n", buf)
91 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your phone number: \x1b[0m\x1b[36m123-123-1234\x1b[0m\r\n", buf)
92 | expect("Answered 123-123-1234.\r\n", buf)
93 | expect("---------------------\r\n", buf)
94 | expect("select\r\n", buf)
95 | expect("\x1b[?25l\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days are you available:\x1b[0m \x1b[36m[? for help]\x1b[0m\r\n", buf)
96 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Monday\r\n", buf)
97 | expect(" \x1b[32m ◉ \x1b[0m Tuesday\r\n", buf)
98 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
99 | expect(" \x1b[32m ◉ \x1b[0m Thursday\r\n", buf)
100 | expect(" \x1b[32m ◉ \x1b[0m Friday\r\n", buf)
101 | fh.Write([]byte("?"))
102 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[36mⓘ We are closed weekends and avaibility is limited on Wednesday\x1b[0m\r\n", buf)
103 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days are you available:\x1b[0m\r\n", buf)
104 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Monday\r\n", buf)
105 | expect(" \x1b[32m ◉ \x1b[0m Tuesday\r\n", buf)
106 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
107 | expect(" \x1b[32m ◉ \x1b[0m Thursday\r\n", buf)
108 | expect(" \x1b[32m ◉ \x1b[0m Friday\r\n", buf)
109 | fh.Write([]byte("\x1b"))
110 | fh.Write([]byte("["))
111 | fh.Write([]byte("B"))
112 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[36mⓘ We are closed weekends and avaibility is limited on Wednesday\x1b[0m\r\n", buf)
113 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days are you available:\x1b[0m\r\n", buf)
114 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
115 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Tuesday\r\n", buf)
116 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
117 | expect(" \x1b[32m ◉ \x1b[0m Thursday\r\n", buf)
118 | expect(" \x1b[32m ◉ \x1b[0m Friday\r\n", buf)
119 | fh.Write([]byte(" "))
120 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[36mⓘ We are closed weekends and avaibility is limited on Wednesday\x1b[0m\r\n", buf)
121 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days are you available:\x1b[0m\r\n", buf)
122 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
123 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
124 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
125 | expect(" \x1b[32m ◉ \x1b[0m Thursday\r\n", buf)
126 | expect(" \x1b[32m ◉ \x1b[0m Friday\r\n", buf)
127 | fh.Write([]byte("\x1b"))
128 | fh.Write([]byte("["))
129 | fh.Write([]byte("B"))
130 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[36mⓘ We are closed weekends and avaibility is limited on Wednesday\x1b[0m\r\n", buf)
131 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days are you available:\x1b[0m\r\n", buf)
132 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
133 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
134 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
135 | expect(" \x1b[32m ◉ \x1b[0m Thursday\r\n", buf)
136 | expect(" \x1b[32m ◉ \x1b[0m Friday\r\n", buf)
137 | fh.Write([]byte("\x1b"))
138 | fh.Write([]byte("["))
139 | fh.Write([]byte("B"))
140 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[36mⓘ We are closed weekends and avaibility is limited on Wednesday\x1b[0m\r\n", buf)
141 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days are you available:\x1b[0m\r\n", buf)
142 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
143 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
144 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
145 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Thursday\r\n", buf)
146 | expect(" \x1b[32m ◉ \x1b[0m Friday\r\n", buf)
147 | fh.Write([]byte(" "))
148 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[36mⓘ We are closed weekends and avaibility is limited on Wednesday\x1b[0m\r\n", buf)
149 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days are you available:\x1b[0m\r\n", buf)
150 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
151 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
152 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
153 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
154 | expect(" \x1b[32m ◉ \x1b[0m Friday\r\n", buf)
155 | fh.Write([]byte("\r"))
156 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days are you available:\x1b[0m\x1b[36m Monday, Friday\x1b[0m\r\n", buf)
157 | expect("Answered [Monday Friday].\r\n", buf)
158 | expect("---------------------\r\n", buf)
159 | expect("select\r\n", buf)
160 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m \x1b[36m[? for help]\x1b[0m\r\n", buf)
161 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
162 | expect("\x1b[1;36m❯ blue\x1b[0m\r\n", buf)
163 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
164 | expect("\x1b[?25l", buf)
165 | fh.Write([]byte("?"))
166 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[36mⓘ Blue is the best color, but it is your choice\x1b[0m\r\n", buf)
167 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\r\n", buf)
168 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
169 | expect("\x1b[1;36m❯ blue\x1b[0m\r\n", buf)
170 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
171 | fh.Write([]byte("\r"))
172 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\x1b[36m blue\x1b[0m\r\n", buf)
173 | expect("Answered blue.\r\n", buf)
174 | expect("---------------------\r\n", buf)
175 | expect("password\r\n", buf)
176 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mEnter a secret: \x1b[0m\x1b[36m[? for help]\x1b[0m ", buf)
177 | fh.Write([]byte("?"))
178 | expect("*", buf)
179 | fh.Write([]byte("\r"))
180 | expect("\r\r\n", buf)
181 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[36mⓘ Don't really enter a secret, this is just for testing\x1b[0m\r\n", buf)
182 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mEnter a secret: \x1b[0m", buf)
183 | fh.Write([]byte("f"))
184 | expect("*", buf)
185 | fh.Write([]byte("o"))
186 | expect("*", buf)
187 | fh.Write([]byte("o"))
188 | expect("*", buf)
189 | fh.Write([]byte("b"))
190 | expect("*", buf)
191 | fh.Write([]byte("a"))
192 | expect("*", buf)
193 | fh.Write([]byte("r"))
194 | expect("*", buf)
195 | fh.Write([]byte("\r"))
196 | expect("\r\r\n", buf)
197 | expect("Answered foobar.\r\n", buf)
198 | expect("---------------------\r\n", buf)
199 |
200 | c.Wait()
201 | tty.Close()
202 | fh.Close()
203 | }
204 |
205 | func expect(expected string, buf *bufio.Reader) {
206 | sofar := []rune{}
207 | for _, r := range expected {
208 | got, _, _ := buf.ReadRune()
209 | sofar = append(sofar, got)
210 | if got != r {
211 | fmt.Fprintln(os.Stderr, RESET)
212 |
213 | // we want to quote the string but we also want to make the unexpected character RED
214 | // so we use the strconv.Quote function but trim off the quoted characters so we can
215 | // merge multiple quoted strings into one.
216 | expStart := strings.TrimSuffix(strconv.Quote(expected[:len(sofar)-1]), "\"")
217 | expMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(expected[len(sofar)-1])), "\""), "\"")
218 | expEnd := strings.TrimPrefix(strconv.Quote(expected[len(sofar):]), "\"")
219 |
220 | fmt.Fprintf(os.Stderr, "Expected: %s%s%s%s%s\n", expStart, RED, expMiss, RESET, expEnd)
221 |
222 | // read the rest of the buffer
223 | p := make([]byte, buf.Buffered())
224 | buf.Read(p)
225 |
226 | gotStart := strings.TrimSuffix(strconv.Quote(string(sofar[:len(sofar)-1])), "\"")
227 | gotMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(sofar[len(sofar)-1])), "\""), "\"")
228 | gotEnd := strings.TrimPrefix(strconv.Quote(string(p)), "\"")
229 |
230 | fmt.Fprintf(os.Stderr, "Got: %s%s%s%s%s\n", gotStart, RED, gotMiss, RESET, gotEnd)
231 | panic(fmt.Errorf("Unexpected Rune %q, Expected %q\n", got, r))
232 | } else {
233 | fmt.Printf("%c", r)
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/tests/autoplay/input.go:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE!
3 | //
4 | // This file was automatically generated via the commands:
5 | //
6 | // go get github.com/coryb/autoplay
7 | // autoplay -n autoplay/input.go go run input.go
8 | //
9 | ////////////////////////////////////////////////////////////////////////////////
10 | package main
11 |
12 | import (
13 | "bufio"
14 | "fmt"
15 | "github.com/kr/pty"
16 | "os"
17 | "os/exec"
18 | "strconv"
19 | "strings"
20 | )
21 |
22 | const (
23 | RED = "\033[31m"
24 | RESET = "\033[0m"
25 | )
26 |
27 | func main() {
28 | fh, tty, _ := pty.Open()
29 | defer tty.Close()
30 | defer fh.Close()
31 | c := exec.Command("go", "run", "input.go")
32 | c.Stdin = tty
33 | c.Stdout = tty
34 | c.Stderr = tty
35 | c.Start()
36 | buf := bufio.NewReaderSize(fh, 1024)
37 |
38 | expect("no default\r\n", buf)
39 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mHello world \x1b[0m", buf)
40 | fh.Write([]byte("a"))
41 | expect("a", buf)
42 | fh.Write([]byte("l"))
43 | expect("l", buf)
44 | fh.Write([]byte("e"))
45 | expect("e", buf)
46 | fh.Write([]byte("c"))
47 | expect("c", buf)
48 | fh.Write([]byte("\r"))
49 | expect("\r\r\n", buf)
50 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mHello world \x1b[0m\x1b[36malec\x1b[0m\r\n", buf)
51 | expect("Answered alec.\r\n", buf)
52 | expect("---------------------\r\n", buf)
53 | expect("default\r\n", buf)
54 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mHello world \x1b[0m\x1b[37m(default) \x1b[0m", buf)
55 | fh.Write([]byte("\r"))
56 | expect("\r\r\n", buf)
57 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mHello world \x1b[0m\x1b[36mdefault\x1b[0m\r\n", buf)
58 | expect("Answered default.\r\n", buf)
59 | expect("---------------------\r\n", buf)
60 | expect("no help, send '?'\r\n", buf)
61 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mHello world \x1b[0m", buf)
62 | fh.Write([]byte("?"))
63 | expect("?", buf)
64 | fh.Write([]byte("\r"))
65 | expect("\r\r\n", buf)
66 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mHello world \x1b[0m\x1b[36m?\x1b[0m\r\n", buf)
67 | expect("Answered ?.\r\n", buf)
68 | expect("---------------------\r\n", buf)
69 | expect("input text in random location\r\n", buf)
70 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mHello \x1b[0m", buf)
71 | fh.Write([]byte("h"))
72 | expect("h", buf)
73 | fh.Write([]byte("e"))
74 | expect("e", buf)
75 | fh.Write([]byte("l"))
76 | expect("l", buf)
77 | fh.Write([]byte("l"))
78 | expect("l", buf)
79 | fh.Write([]byte("o"))
80 | expect("o", buf)
81 | fh.Write([]byte(" "))
82 | expect(" ", buf)
83 | fh.Write([]byte("w"))
84 | expect("w", buf)
85 | fh.Write([]byte("o"))
86 | expect("o", buf)
87 | fh.Write([]byte("r"))
88 | expect("r", buf)
89 | fh.Write([]byte("l"))
90 | expect("l", buf)
91 | fh.Write([]byte("d"))
92 | expect("d", buf)
93 | fh.Write([]byte("\x1b"))
94 | fh.Write([]byte("["))
95 | fh.Write([]byte("D"))
96 | expect("\x1b[1D", buf)
97 | fh.Write([]byte("\x1b"))
98 | fh.Write([]byte("["))
99 | fh.Write([]byte("B"))
100 | fh.Write([]byte("a"))
101 | expect("\x1b[0Kad\x1b[1D", buf)
102 | fh.Write([]byte("\x1b"))
103 | fh.Write([]byte("["))
104 | fh.Write([]byte("D"))
105 | expect("\x1b[1D", buf)
106 | fh.Write([]byte("\x1b"))
107 | fh.Write([]byte("["))
108 | fh.Write([]byte("D"))
109 | expect("\x1b[1D", buf)
110 | fh.Write([]byte("a"))
111 | expect("\x1b[0Kalad\x1b[3D", buf)
112 | fh.Write([]byte("\x1b"))
113 | fh.Write([]byte("["))
114 | fh.Write([]byte("D"))
115 | expect("\x1b[1D", buf)
116 | fh.Write([]byte("\x1b"))
117 | fh.Write([]byte("["))
118 | fh.Write([]byte("D"))
119 | expect("\x1b[1D", buf)
120 | fh.Write([]byte("\x1b"))
121 | fh.Write([]byte("["))
122 | fh.Write([]byte("D"))
123 | expect("\x1b[1D", buf)
124 | fh.Write([]byte("\x1b"))
125 | fh.Write([]byte("["))
126 | fh.Write([]byte("D"))
127 | expect("\x1b[1D", buf)
128 | fh.Write([]byte("\u007f"))
129 | expect("\x1b[1D\x1b[0Kworalad\x1b[7D", buf)
130 | fh.Write([]byte("\u007f"))
131 | expect("\x1b[1D\x1b[0Kworalad\x1b[7D", buf)
132 | fh.Write([]byte("\u007f"))
133 | expect("\x1b[1D\x1b[0Kworalad\x1b[7D", buf)
134 | fh.Write([]byte("\r"))
135 | expect("\r\r\n", buf)
136 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mHello \x1b[0m\x1b[36mhelworalad\x1b[0m\r\n", buf)
137 | expect("Answered helworalad.\r\n", buf)
138 | expect("---------------------\r\n", buf)
139 |
140 | c.Wait()
141 | tty.Close()
142 | fh.Close()
143 | }
144 |
145 | func expect(expected string, buf *bufio.Reader) {
146 | sofar := []rune{}
147 | for _, r := range expected {
148 | got, _, _ := buf.ReadRune()
149 | sofar = append(sofar, got)
150 | if got != r {
151 | fmt.Fprintln(os.Stderr, RESET)
152 |
153 | // we want to quote the string but we also want to make the unexpected character RED
154 | // so we use the strconv.Quote function but trim off the quoted characters so we can
155 | // merge multiple quoted strings into one.
156 | expStart := strings.TrimSuffix(strconv.Quote(expected[:len(sofar)-1]), "\"")
157 | expMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(expected[len(sofar)-1])), "\""), "\"")
158 | expEnd := strings.TrimPrefix(strconv.Quote(expected[len(sofar):]), "\"")
159 |
160 | fmt.Fprintf(os.Stderr, "Expected: %s%s%s%s%s\n", expStart, RED, expMiss, RESET, expEnd)
161 |
162 | // read the rest of the buffer
163 | p := make([]byte, buf.Buffered())
164 | buf.Read(p)
165 |
166 | gotStart := strings.TrimSuffix(strconv.Quote(string(sofar[:len(sofar)-1])), "\"")
167 | gotMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(sofar[len(sofar)-1])), "\""), "\"")
168 | gotEnd := strings.TrimPrefix(strconv.Quote(string(p)), "\"")
169 |
170 | fmt.Fprintf(os.Stderr, "Got: %s%s%s%s%s\n", gotStart, RED, gotMiss, RESET, gotEnd)
171 | panic(fmt.Errorf("Unexpected Rune %q, Expected %q\n", got, r))
172 | } else {
173 | fmt.Printf("%c", r)
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/tests/autoplay/multiselect.go:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE!
3 | //
4 | // This file was automatically generated via the commands:
5 | //
6 | // go get github.com/coryb/autoplay
7 | // autoplay -n autoplay/multiselect.go go run multiselect.go
8 | //
9 | ////////////////////////////////////////////////////////////////////////////////
10 | package main
11 |
12 | import (
13 | "bufio"
14 | "fmt"
15 | "github.com/kr/pty"
16 | "os"
17 | "os/exec"
18 | "strconv"
19 | "strings"
20 | )
21 |
22 | const (
23 | RED = "\033[31m"
24 | RESET = "\033[0m"
25 | )
26 |
27 | func main() {
28 | fh, tty, _ := pty.Open()
29 | defer tty.Close()
30 | defer fh.Close()
31 | c := exec.Command("go", "run", "multiselect.go")
32 | c.Stdin = tty
33 | c.Stdout = tty
34 | c.Stderr = tty
35 | c.Start()
36 | buf := bufio.NewReaderSize(fh, 1024)
37 |
38 | expect("standard\r\n", buf)
39 | expect("\x1b[?25l\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
40 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
41 | expect(" \x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
42 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
43 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
44 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
45 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
46 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
47 | fh.Write([]byte("\x1b"))
48 | fh.Write([]byte("["))
49 | fh.Write([]byte("B"))
50 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
51 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
52 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
53 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
54 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
55 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
56 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
57 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
58 | fh.Write([]byte(" "))
59 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
60 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
61 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Monday\r\n", buf)
62 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
63 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
64 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
65 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
66 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
67 | fh.Write([]byte("\x1b"))
68 | fh.Write([]byte("["))
69 | fh.Write([]byte("B"))
70 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
71 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
72 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
73 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
74 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
75 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
76 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
77 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
78 | fh.Write([]byte("\x1b"))
79 | fh.Write([]byte("["))
80 | fh.Write([]byte("B"))
81 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
82 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
83 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
84 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
85 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
86 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
87 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
88 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
89 | fh.Write([]byte("\x1b"))
90 | fh.Write([]byte("["))
91 | fh.Write([]byte("B"))
92 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
93 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
94 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
95 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
96 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
97 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
98 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
99 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
100 | fh.Write([]byte("\x1b"))
101 | fh.Write([]byte("["))
102 | fh.Write([]byte("B"))
103 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
104 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
105 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
106 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
107 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
108 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
109 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
110 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
111 | fh.Write([]byte(" "))
112 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
113 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
114 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
115 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
116 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
117 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
118 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Friday\r\n", buf)
119 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
120 | fh.Write([]byte("\r"))
121 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\x1b[36m Monday, Friday\x1b[0m\r\n", buf)
122 | expect("Answered [Monday Friday].\r\n", buf)
123 | expect("---------------------\r\n", buf)
124 | expect("default (sunday, tuesday)\r\n", buf)
125 | expect("\x1b[?25l\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
126 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Sunday\r\n", buf)
127 | expect(" \x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
128 | expect(" \x1b[32m ◉ \x1b[0m Tuesday\r\n", buf)
129 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
130 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
131 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
132 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
133 | fh.Write([]byte(" "))
134 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
135 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
136 | expect(" \x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
137 | expect(" \x1b[32m ◉ \x1b[0m Tuesday\r\n", buf)
138 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
139 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
140 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
141 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
142 | fh.Write([]byte("\x1b"))
143 | fh.Write([]byte("["))
144 | fh.Write([]byte("B"))
145 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
146 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
147 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
148 | expect(" \x1b[32m ◉ \x1b[0m Tuesday\r\n", buf)
149 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
150 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
151 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
152 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
153 | fh.Write([]byte(" "))
154 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
155 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
156 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Monday\r\n", buf)
157 | expect(" \x1b[32m ◉ \x1b[0m Tuesday\r\n", buf)
158 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
159 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
160 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
161 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
162 | fh.Write([]byte("\x1b"))
163 | fh.Write([]byte("["))
164 | fh.Write([]byte("B"))
165 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
166 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
167 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
168 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Tuesday\r\n", buf)
169 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
170 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
171 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
172 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
173 | fh.Write([]byte(" "))
174 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
175 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
176 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
177 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
178 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
179 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
180 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
181 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
182 | fh.Write([]byte("\x1b"))
183 | fh.Write([]byte("["))
184 | fh.Write([]byte("B"))
185 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
186 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
187 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
188 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
189 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
190 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
191 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
192 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
193 | fh.Write([]byte("\x1b"))
194 | fh.Write([]byte("["))
195 | fh.Write([]byte("B"))
196 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
197 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
198 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
199 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
200 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
201 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
202 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
203 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
204 | fh.Write([]byte("\x1b"))
205 | fh.Write([]byte("["))
206 | fh.Write([]byte("B"))
207 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
208 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
209 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
210 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
211 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
212 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
213 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
214 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
215 | fh.Write([]byte(" "))
216 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
217 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
218 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
219 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
220 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
221 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
222 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Friday\r\n", buf)
223 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
224 | fh.Write([]byte("\r"))
225 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\x1b[36m Monday, Friday\x1b[0m\r\n", buf)
226 | expect("Answered [Monday Friday].\r\n", buf)
227 | expect("---------------------\r\n", buf)
228 | expect("default not found\r\n", buf)
229 | expect("\x1b[?25l\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
230 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
231 | expect(" \x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
232 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
233 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
234 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
235 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
236 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
237 | fh.Write([]byte("\x1b"))
238 | fh.Write([]byte("["))
239 | fh.Write([]byte("B"))
240 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
241 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
242 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
243 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
244 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
245 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
246 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
247 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
248 | fh.Write([]byte(" "))
249 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
250 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
251 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Monday\r\n", buf)
252 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
253 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
254 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
255 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
256 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
257 | fh.Write([]byte("\x1b"))
258 | fh.Write([]byte("["))
259 | fh.Write([]byte("B"))
260 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
261 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
262 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
263 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
264 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
265 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
266 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
267 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
268 | fh.Write([]byte("\x1b"))
269 | fh.Write([]byte("["))
270 | fh.Write([]byte("B"))
271 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
272 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
273 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
274 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
275 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
276 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
277 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
278 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
279 | fh.Write([]byte("\x1b"))
280 | fh.Write([]byte("["))
281 | fh.Write([]byte("B"))
282 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
283 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
284 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
285 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
286 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
287 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
288 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
289 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
290 | fh.Write([]byte("\x1b"))
291 | fh.Write([]byte("["))
292 | fh.Write([]byte("B"))
293 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
294 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
295 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
296 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
297 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
298 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
299 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
300 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
301 | fh.Write([]byte(" "))
302 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
303 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
304 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
305 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
306 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
307 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
308 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Friday\r\n", buf)
309 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
310 | fh.Write([]byte("\r"))
311 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\x1b[36m Monday, Friday\x1b[0m\r\n", buf)
312 | expect("Answered [Monday Friday].\r\n", buf)
313 | expect("---------------------\r\n", buf)
314 | expect("no help - type ?\r\n", buf)
315 | expect("\x1b[?25l\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
316 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
317 | expect(" \x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
318 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
319 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
320 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
321 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
322 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
323 | fh.Write([]byte("?"))
324 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
325 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
326 | expect(" \x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
327 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
328 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
329 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
330 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
331 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
332 | fh.Write([]byte("\x1b"))
333 | fh.Write([]byte("["))
334 | fh.Write([]byte("B"))
335 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
336 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
337 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Monday\r\n", buf)
338 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
339 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
340 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
341 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
342 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
343 | fh.Write([]byte(" "))
344 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
345 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
346 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Monday\r\n", buf)
347 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
348 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
349 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
350 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
351 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
352 | fh.Write([]byte("\x1b"))
353 | fh.Write([]byte("["))
354 | fh.Write([]byte("B"))
355 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
356 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
357 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
358 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
359 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
360 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
361 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
362 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
363 | fh.Write([]byte("\x1b"))
364 | fh.Write([]byte("["))
365 | fh.Write([]byte("B"))
366 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
367 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
368 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
369 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
370 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
371 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
372 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
373 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
374 | fh.Write([]byte("\x1b"))
375 | fh.Write([]byte("["))
376 | fh.Write([]byte("B"))
377 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
378 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
379 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
380 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
381 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
382 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
383 | expect(" \x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
384 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
385 | fh.Write([]byte("\x1b"))
386 | fh.Write([]byte("["))
387 | fh.Write([]byte("B"))
388 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
389 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
390 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
391 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
392 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
393 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
394 | expect("\x1b[36m❯\x1b[0m\x1b[1;99m ◯ \x1b[0m Friday\r\n", buf)
395 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
396 | fh.Write([]byte(" "))
397 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\r\n", buf)
398 | expect(" \x1b[1;99m ◯ \x1b[0m Sunday\r\n", buf)
399 | expect(" \x1b[32m ◉ \x1b[0m Monday\r\n", buf)
400 | expect(" \x1b[1;99m ◯ \x1b[0m Tuesday\r\n", buf)
401 | expect(" \x1b[1;99m ◯ \x1b[0m Wednesday\r\n", buf)
402 | expect(" \x1b[1;99m ◯ \x1b[0m Thursday\r\n", buf)
403 | expect("\x1b[36m❯\x1b[0m\x1b[32m ◉ \x1b[0m Friday\r\n", buf)
404 | expect(" \x1b[1;99m ◯ \x1b[0m Saturday\r\n", buf)
405 | fh.Write([]byte("\r"))
406 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat days do you prefer:\x1b[0m\x1b[36m Monday, Friday\x1b[0m\r\n", buf)
407 | expect("Answered [Monday Friday].\r\n", buf)
408 | expect("---------------------\r\n", buf)
409 |
410 | c.Wait()
411 | tty.Close()
412 | fh.Close()
413 | }
414 |
415 | func expect(expected string, buf *bufio.Reader) {
416 | sofar := []rune{}
417 | for _, r := range expected {
418 | got, _, _ := buf.ReadRune()
419 | sofar = append(sofar, got)
420 | if got != r {
421 | fmt.Fprintln(os.Stderr, RESET)
422 |
423 | // we want to quote the string but we also want to make the unexpected character RED
424 | // so we use the strconv.Quote function but trim off the quoted characters so we can
425 | // merge multiple quoted strings into one.
426 | expStart := strings.TrimSuffix(strconv.Quote(expected[:len(sofar)-1]), "\"")
427 | expMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(expected[len(sofar)-1])), "\""), "\"")
428 | expEnd := strings.TrimPrefix(strconv.Quote(expected[len(sofar):]), "\"")
429 |
430 | fmt.Fprintf(os.Stderr, "Expected: %s%s%s%s%s\n", expStart, RED, expMiss, RESET, expEnd)
431 |
432 | // read the rest of the buffer
433 | p := make([]byte, buf.Buffered())
434 | buf.Read(p)
435 |
436 | gotStart := strings.TrimSuffix(strconv.Quote(string(sofar[:len(sofar)-1])), "\"")
437 | gotMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(sofar[len(sofar)-1])), "\""), "\"")
438 | gotEnd := strings.TrimPrefix(strconv.Quote(string(p)), "\"")
439 |
440 | fmt.Fprintf(os.Stderr, "Got: %s%s%s%s%s\n", gotStart, RED, gotMiss, RESET, gotEnd)
441 | panic(fmt.Errorf("Unexpected Rune %q, Expected %q\n", got, r))
442 | } else {
443 | fmt.Printf("%c", r)
444 | }
445 | }
446 | }
447 |
--------------------------------------------------------------------------------
/tests/autoplay/password.go:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE!
3 | //
4 | // This file was automatically generated via the commands:
5 | //
6 | // go get github.com/coryb/autoplay
7 | // autoplay -n autoplay/password.go go run password.go
8 | //
9 | ////////////////////////////////////////////////////////////////////////////////
10 | package main
11 |
12 | import (
13 | "bufio"
14 | "fmt"
15 | "github.com/kr/pty"
16 | "os"
17 | "os/exec"
18 | "strconv"
19 | "strings"
20 | )
21 |
22 | const (
23 | RED = "\033[31m"
24 | RESET = "\033[0m"
25 | )
26 |
27 | func main() {
28 | fh, tty, _ := pty.Open()
29 | defer tty.Close()
30 | defer fh.Close()
31 | c := exec.Command("go", "run", "password.go")
32 | c.Stdin = tty
33 | c.Stdout = tty
34 | c.Stderr = tty
35 | c.Start()
36 | buf := bufio.NewReaderSize(fh, 1024)
37 |
38 | expect("standard\r\n", buf)
39 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mPlease type your password: \x1b[0m", buf)
40 | fh.Write([]byte("f"))
41 | expect("*", buf)
42 | fh.Write([]byte("o"))
43 | expect("*", buf)
44 | fh.Write([]byte("o"))
45 | expect("*", buf)
46 | fh.Write([]byte("b"))
47 | expect("*", buf)
48 | fh.Write([]byte("a"))
49 | expect("*", buf)
50 | fh.Write([]byte("r"))
51 | expect("*", buf)
52 | fh.Write([]byte("\r"))
53 | expect("\r\r\n", buf)
54 | expect("Answered foobar.\r\n", buf)
55 | expect("---------------------\r\n", buf)
56 | expect("please make sure paste works\r\n", buf)
57 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mPlease paste your password: \x1b[0m", buf)
58 | fh.Write([]byte("f"))
59 | fh.Write([]byte("o"))
60 | fh.Write([]byte("o"))
61 | fh.Write([]byte("b"))
62 | fh.Write([]byte("a"))
63 | fh.Write([]byte("r"))
64 | expect("******", buf)
65 | fh.Write([]byte("\r"))
66 | expect("\r\r\n", buf)
67 | expect("Answered foobar.\r\n", buf)
68 | expect("---------------------\r\n", buf)
69 | expect("no help, send '?'\r\n", buf)
70 | expect("\x1b[1;92m? \x1b[0m\x1b[1;99mPlease type your password: \x1b[0m", buf)
71 | fh.Write([]byte("?"))
72 | expect("*", buf)
73 | fh.Write([]byte("\r"))
74 | expect("\r\r\n", buf)
75 | expect("Answered ?.\r\n", buf)
76 | expect("---------------------\r\n", buf)
77 |
78 | c.Wait()
79 | tty.Close()
80 | fh.Close()
81 | }
82 |
83 | func expect(expected string, buf *bufio.Reader) {
84 | sofar := []rune{}
85 | for _, r := range expected {
86 | got, _, _ := buf.ReadRune()
87 | sofar = append(sofar, got)
88 | if got != r {
89 | fmt.Fprintln(os.Stderr, RESET)
90 |
91 | // we want to quote the string but we also want to make the unexpected character RED
92 | // so we use the strconv.Quote function but trim off the quoted characters so we can
93 | // merge multiple quoted strings into one.
94 | expStart := strings.TrimSuffix(strconv.Quote(expected[:len(sofar)-1]), "\"")
95 | expMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(expected[len(sofar)-1])), "\""), "\"")
96 | expEnd := strings.TrimPrefix(strconv.Quote(expected[len(sofar):]), "\"")
97 |
98 | fmt.Fprintf(os.Stderr, "Expected: %s%s%s%s%s\n", expStart, RED, expMiss, RESET, expEnd)
99 |
100 | // read the rest of the buffer
101 | p := make([]byte, buf.Buffered())
102 | buf.Read(p)
103 |
104 | gotStart := strings.TrimSuffix(strconv.Quote(string(sofar[:len(sofar)-1])), "\"")
105 | gotMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(sofar[len(sofar)-1])), "\""), "\"")
106 | gotEnd := strings.TrimPrefix(strconv.Quote(string(p)), "\"")
107 |
108 | fmt.Fprintf(os.Stderr, "Got: %s%s%s%s%s\n", gotStart, RED, gotMiss, RESET, gotEnd)
109 | panic(fmt.Errorf("Unexpected Rune %q, Expected %q\n", got, r))
110 | } else {
111 | fmt.Printf("%c", r)
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/tests/autoplay/select.go:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE!
3 | //
4 | // This file was automatically generated via the commands:
5 | //
6 | // go get github.com/coryb/autoplay
7 | // autoplay -n autoplay/select.go go run select.go
8 | //
9 | ////////////////////////////////////////////////////////////////////////////////
10 | package main
11 |
12 | import (
13 | "bufio"
14 | "fmt"
15 | "github.com/kr/pty"
16 | "os"
17 | "os/exec"
18 | "strconv"
19 | "strings"
20 | )
21 |
22 | const (
23 | RED = "\033[31m"
24 | RESET = "\033[0m"
25 | )
26 |
27 | func main() {
28 | fh, tty, _ := pty.Open()
29 | defer tty.Close()
30 | defer fh.Close()
31 | c := exec.Command("go", "run", "select.go")
32 | c.Stdin = tty
33 | c.Stdout = tty
34 | c.Stderr = tty
35 | c.Start()
36 | buf := bufio.NewReaderSize(fh, 1024)
37 |
38 | expect("standard\r\n", buf)
39 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\r\n", buf)
40 | expect("\x1b[1;36m❯ red\x1b[0m\r\n", buf)
41 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
42 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
43 | expect("\x1b[?25l", buf)
44 | fh.Write([]byte("\r"))
45 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\x1b[36m red\x1b[0m\r\n", buf)
46 | expect("Answered red.\r\n", buf)
47 | expect("---------------------\r\n", buf)
48 | expect("short\r\n", buf)
49 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\r\n", buf)
50 | expect("\x1b[1;36m❯ red\x1b[0m\r\n", buf)
51 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
52 | expect("\x1b[?25l", buf)
53 | fh.Write([]byte("\r"))
54 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\x1b[36m red\x1b[0m\r\n", buf)
55 | expect("Answered red.\r\n", buf)
56 | expect("---------------------\r\n", buf)
57 | expect("default\r\n", buf)
58 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color (should default blue):\x1b[0m\r\n", buf)
59 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
60 | expect("\x1b[1;36m❯ blue\x1b[0m\r\n", buf)
61 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
62 | expect("\x1b[?25l", buf)
63 | fh.Write([]byte("\r"))
64 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color (should default blue):\x1b[0m\x1b[36m blue\x1b[0m\r\n", buf)
65 | expect("Answered blue.\r\n", buf)
66 | expect("---------------------\r\n", buf)
67 | expect("one\r\n", buf)
68 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose one:\x1b[0m\r\n", buf)
69 | expect("\x1b[1;36m❯ hello\x1b[0m\r\n", buf)
70 | expect("\x1b[?25l", buf)
71 | fh.Write([]byte("\r"))
72 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose one:\x1b[0m\x1b[36m hello\x1b[0m\r\n", buf)
73 | expect("Answered hello.\r\n", buf)
74 | expect("---------------------\r\n", buf)
75 | expect("no help, type ?\r\n", buf)
76 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\r\n", buf)
77 | expect("\x1b[1;36m❯ red\x1b[0m\r\n", buf)
78 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
79 | expect("\x1b[?25l", buf)
80 | fh.Write([]byte("\r"))
81 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\x1b[36m red\x1b[0m\r\n", buf)
82 | expect("Answered red.\r\n", buf)
83 | expect("---------------------\r\n", buf)
84 | expect("passes through bottom\r\n", buf)
85 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose one:\x1b[0m\r\n", buf)
86 | expect("\x1b[1;36m❯ red\x1b[0m\r\n", buf)
87 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
88 | expect("\x1b[?25l", buf)
89 | fh.Write([]byte("\x1b"))
90 | fh.Write([]byte("["))
91 | fh.Write([]byte("B"))
92 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose one:\x1b[0m\r\n", buf)
93 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
94 | expect("\x1b[1;36m❯ blue\x1b[0m\r\n", buf)
95 | fh.Write([]byte("\x1b"))
96 | fh.Write([]byte("["))
97 | fh.Write([]byte("B"))
98 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose one:\x1b[0m\r\n", buf)
99 | expect("\x1b[1;36m❯ red\x1b[0m\r\n", buf)
100 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
101 | fh.Write([]byte("\r"))
102 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose one:\x1b[0m\x1b[36m red\x1b[0m\r\n", buf)
103 | expect("Answered red.\r\n", buf)
104 | expect("---------------------\r\n", buf)
105 | expect("passes through top\r\n", buf)
106 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose one:\x1b[0m\r\n", buf)
107 | expect("\x1b[1;36m❯ red\x1b[0m\r\n", buf)
108 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
109 | expect("\x1b[?25l", buf)
110 | fh.Write([]byte("\x1b"))
111 | fh.Write([]byte("["))
112 | fh.Write([]byte("A"))
113 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose one:\x1b[0m\r\n", buf)
114 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
115 | expect("\x1b[1;36m❯ blue\x1b[0m\r\n", buf)
116 | fh.Write([]byte("\r"))
117 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose one:\x1b[0m\x1b[36m blue\x1b[0m\r\n", buf)
118 | expect("Answered blue.\r\n", buf)
119 | expect("---------------------\r\n", buf)
120 | expect("no options\r\n", buf)
121 |
122 | c.Wait()
123 | tty.Close()
124 | fh.Close()
125 | }
126 |
127 | func expect(expected string, buf *bufio.Reader) {
128 | sofar := []rune{}
129 | for _, r := range expected {
130 | got, _, _ := buf.ReadRune()
131 | sofar = append(sofar, got)
132 | if got != r {
133 | fmt.Fprintln(os.Stderr, RESET)
134 |
135 | // we want to quote the string but we also want to make the unexpected character RED
136 | // so we use the strconv.Quote function but trim off the quoted characters so we can
137 | // merge multiple quoted strings into one.
138 | expStart := strings.TrimSuffix(strconv.Quote(expected[:len(sofar)-1]), "\"")
139 | expMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(expected[len(sofar)-1])), "\""), "\"")
140 | expEnd := strings.TrimPrefix(strconv.Quote(expected[len(sofar):]), "\"")
141 |
142 | fmt.Fprintf(os.Stderr, "Expected: %s%s%s%s%s\n", expStart, RED, expMiss, RESET, expEnd)
143 |
144 | // read the rest of the buffer
145 | p := make([]byte, buf.Buffered())
146 | buf.Read(p)
147 |
148 | gotStart := strings.TrimSuffix(strconv.Quote(string(sofar[:len(sofar)-1])), "\"")
149 | gotMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(sofar[len(sofar)-1])), "\""), "\"")
150 | gotEnd := strings.TrimPrefix(strconv.Quote(string(p)), "\"")
151 |
152 | fmt.Fprintf(os.Stderr, "Got: %s%s%s%s%s\n", gotStart, RED, gotMiss, RESET, gotEnd)
153 | panic(fmt.Errorf("Unexpected Rune %q, Expected %q\n", got, r))
154 | } else {
155 | fmt.Printf("%c", r)
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/tests/autoplay/selectThenInput.go:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////////////////
2 | // DO NOT MODIFY THIS FILE!
3 | //
4 | // This file was automatically generated via the commands:
5 | //
6 | // go get github.com/coryb/autoplay
7 | // autoplay -n autoplay/selectThenInput.go go run selectThenInput.go
8 | //
9 | ////////////////////////////////////////////////////////////////////////////////
10 | package main
11 |
12 | import (
13 | "bufio"
14 | "fmt"
15 | "github.com/kr/pty"
16 | "os"
17 | "os/exec"
18 | "strconv"
19 | "strings"
20 | )
21 |
22 | const (
23 | RED = "\033[31m"
24 | RESET = "\033[0m"
25 | )
26 |
27 | func main() {
28 | fh, tty, _ := pty.Open()
29 | defer tty.Close()
30 | defer fh.Close()
31 | c := exec.Command("go", "run", "selectThenInput.go")
32 | c.Stdin = tty
33 | c.Stdout = tty
34 | c.Stderr = tty
35 | c.Start()
36 | buf := bufio.NewReaderSize(fh, 1024)
37 |
38 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\r\n", buf)
39 | expect("\x1b[1;36m❯ red\x1b[0m\r\n", buf)
40 | expect("\x1b[1;99m blue\x1b[0m\r\n", buf)
41 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
42 | expect("\x1b[?25l", buf)
43 | fh.Write([]byte("\x1b"))
44 | fh.Write([]byte("["))
45 | fh.Write([]byte("B"))
46 | expect("\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\r\n", buf)
47 | expect("\x1b[1;99m red\x1b[0m\r\n", buf)
48 | expect("\x1b[1;36m❯ blue\x1b[0m\r\n", buf)
49 | expect("\x1b[1;99m green\x1b[0m\r\n", buf)
50 | fh.Write([]byte("\r"))
51 | expect("\x1b[?25h\x1b[0G\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1F\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mChoose a color:\x1b[0m\x1b[36m blue\x1b[0m\r\n", buf)
52 | expect("\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your name? \x1b[0m", buf)
53 | fh.Write([]byte("L"))
54 | expect("L", buf)
55 | fh.Write([]byte("a"))
56 | expect("a", buf)
57 | fh.Write([]byte("r"))
58 | expect("r", buf)
59 | fh.Write([]byte("r"))
60 | expect("r", buf)
61 | fh.Write([]byte("y"))
62 | expect("y", buf)
63 | fh.Write([]byte(" "))
64 | expect(" ", buf)
65 | fh.Write([]byte("W"))
66 | expect("W", buf)
67 | fh.Write([]byte("a"))
68 | expect("a", buf)
69 | fh.Write([]byte("l"))
70 | expect("l", buf)
71 | fh.Write([]byte("l"))
72 | expect("l", buf)
73 | fh.Write([]byte("\r"))
74 | expect("\r\r\n", buf)
75 | expect("\x1b[1F\x1b[0G\x1b[2K\x1b[1;92m? \x1b[0m\x1b[1;99mWhat is your name? \x1b[0m\x1b[36mLarry Wall\x1b[0m\r\n", buf)
76 | expect("Larry Wall chose blue.\r\n", buf)
77 |
78 | c.Wait()
79 | tty.Close()
80 | fh.Close()
81 | }
82 |
83 | func expect(expected string, buf *bufio.Reader) {
84 | sofar := []rune{}
85 | for _, r := range expected {
86 | got, _, _ := buf.ReadRune()
87 | sofar = append(sofar, got)
88 | if got != r {
89 | fmt.Fprintln(os.Stderr, RESET)
90 |
91 | // we want to quote the string but we also want to make the unexpected character RED
92 | // so we use the strconv.Quote function but trim off the quoted characters so we can
93 | // merge multiple quoted strings into one.
94 | expStart := strings.TrimSuffix(strconv.Quote(expected[:len(sofar)-1]), "\"")
95 | expMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(expected[len(sofar)-1])), "\""), "\"")
96 | expEnd := strings.TrimPrefix(strconv.Quote(expected[len(sofar):]), "\"")
97 |
98 | fmt.Fprintf(os.Stderr, "Expected: %s%s%s%s%s\n", expStart, RED, expMiss, RESET, expEnd)
99 |
100 | // read the rest of the buffer
101 | p := make([]byte, buf.Buffered())
102 | buf.Read(p)
103 |
104 | gotStart := strings.TrimSuffix(strconv.Quote(string(sofar[:len(sofar)-1])), "\"")
105 | gotMiss := strings.TrimSuffix(strings.TrimPrefix(strconv.Quote(string(sofar[len(sofar)-1])), "\""), "\"")
106 | gotEnd := strings.TrimPrefix(strconv.Quote(string(p)), "\"")
107 |
108 | fmt.Fprintf(os.Stderr, "Got: %s%s%s%s%s\n", gotStart, RED, gotMiss, RESET, gotEnd)
109 | panic(fmt.Errorf("Unexpected Rune %q, Expected %q\n", got, r))
110 | } else {
111 | fmt.Printf("%c", r)
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/tests/confirm.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/tj/survey"
5 | "github.com/tj/survey/tests/util"
6 | )
7 |
8 | var answer = false
9 |
10 | var goodTable = []TestUtil.TestTableEntry{
11 | {
12 | "Enter 'yes'", &survey.Confirm{
13 | Message: "yes:",
14 | }, &answer,
15 | },
16 | {
17 | "Enter 'no'", &survey.Confirm{
18 | Message: "yes:",
19 | }, &answer,
20 | },
21 | {
22 | "default", &survey.Confirm{
23 | Message: "yes:",
24 | Default: true,
25 | }, &answer,
26 | },
27 | {
28 | "not recognized (enter random letter)", &survey.Confirm{
29 | Message: "yes:",
30 | Default: true,
31 | }, &answer,
32 | },
33 | {
34 | "no help - type '?'", &survey.Confirm{
35 | Message: "yes:",
36 | Default: true,
37 | }, &answer,
38 | },
39 | }
40 |
41 | func main() {
42 | TestUtil.RunTable(goodTable)
43 | }
44 |
--------------------------------------------------------------------------------
/tests/doubleSelect.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/tj/survey"
7 | )
8 |
9 | var simpleQs = []*survey.Question{
10 | {
11 | Name: "color",
12 | Prompt: &survey.Select{
13 | Message: "select1:",
14 | Options: []string{"red", "blue", "green"},
15 | },
16 | Validate: survey.Required,
17 | },
18 | {
19 | Name: "color2",
20 | Prompt: &survey.Select{
21 | Message: "select2:",
22 | Options: []string{"red", "blue", "green"},
23 | },
24 | Validate: survey.Required,
25 | },
26 | }
27 |
28 | func main() {
29 | answers := struct {
30 | Color string
31 | Color2 string
32 | }{}
33 | // ask the question
34 | err := survey.Ask(simpleQs, &answers)
35 |
36 | if err != nil {
37 | fmt.Println(err.Error())
38 | return
39 | }
40 | // print the answers
41 | fmt.Printf("%s and %s.\n", answers.Color, answers.Color2)
42 | }
43 |
--------------------------------------------------------------------------------
/tests/editor.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/AlecAivazis/survey"
5 | "github.com/AlecAivazis/survey/tests/util"
6 | )
7 |
8 | var answer = ""
9 |
10 | var goodTable = []TestUtil.TestTableEntry{
11 | {
12 | "should open in editor", &survey.Editor{
13 | Message: "should open",
14 | }, &answer,
15 | },
16 | {
17 | "has help", &survey.Editor{
18 | Message: "press ? to see message",
19 | Help: "Does this work?",
20 | }, &answer,
21 | },
22 | }
23 |
24 | func main() {
25 | TestUtil.RunTable(goodTable)
26 | }
27 |
--------------------------------------------------------------------------------
/tests/help.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/tj/survey"
5 | "github.com/tj/survey/tests/util"
6 | )
7 |
8 | var (
9 | confirmAns = false
10 | inputAns = ""
11 | multiselectAns = []string{}
12 | selectAns = ""
13 | passwordAns = ""
14 | )
15 |
16 | var goodTable = []TestUtil.TestTableEntry{
17 | {
18 | "confirm", &survey.Confirm{
19 | Message: "Is it raining?",
20 | Help: "Go outside, if your head becomes wet the answer is probably 'yes'",
21 | }, &confirmAns,
22 | },
23 | {
24 | "input", &survey.Input{
25 | Message: "What is your phone number:",
26 | Help: "Phone number should include the area code, parentheses optional",
27 | }, &inputAns,
28 | },
29 | {
30 | "select", &survey.MultiSelect{
31 | Message: "What days are you available:",
32 | Help: "We are closed weekends and avaibility is limited on Wednesday",
33 | Options: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"},
34 | Default: []string{"Monday", "Tuesday", "Thursday", "Friday"},
35 | }, &multiselectAns,
36 | },
37 | {
38 | "select", &survey.Select{
39 | Message: "Choose a color:",
40 | Help: "Blue is the best color, but it is your choice",
41 | Options: []string{"red", "blue", "green"},
42 | Default: "blue",
43 | }, &selectAns,
44 | },
45 | {
46 | "password", &survey.Password{
47 | Message: "Enter a secret:",
48 | Help: "Don't really enter a secret, this is just for testing",
49 | }, &passwordAns,
50 | },
51 | }
52 |
53 | func main() {
54 | TestUtil.RunTable(goodTable)
55 | }
56 |
--------------------------------------------------------------------------------
/tests/input.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/tj/survey"
5 | "github.com/tj/survey/tests/util"
6 | )
7 |
8 | var val = ""
9 |
10 | var table = []TestUtil.TestTableEntry{
11 | {
12 | "no default", &survey.Input{Message: "Hello world"}, &val,
13 | },
14 | {
15 | "default", &survey.Input{Message: "Hello world", Default: "default"}, &val,
16 | },
17 | {
18 | "no help, send '?'", &survey.Input{Message: "Hello world"}, &val,
19 | },
20 | {
21 | "input text in random location", &survey.Input{Message: "Hello"}, &val,
22 | },
23 | }
24 |
25 | func main() {
26 | TestUtil.RunTable(table)
27 | }
28 |
--------------------------------------------------------------------------------
/tests/longSelect.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/tj/survey"
4 |
5 | func main() {
6 | color := ""
7 | prompt := &survey.Select{
8 | Message: "Choose a color:",
9 | Options: []string{
10 | "a",
11 | "b",
12 | "c",
13 | "d",
14 | "e",
15 | "f",
16 | "g",
17 | "h",
18 | "i",
19 | "j",
20 | },
21 | }
22 | survey.AskOne(prompt, &color, nil)
23 | }
24 |
--------------------------------------------------------------------------------
/tests/multiselect.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/tj/survey"
5 | "github.com/tj/survey/tests/util"
6 | )
7 |
8 | var answer = []string{}
9 |
10 | var table = []TestUtil.TestTableEntry{
11 | {
12 | "standard", &survey.MultiSelect{
13 | Message: "What days do you prefer:",
14 | Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
15 | }, &answer,
16 | },
17 | {
18 | "default (sunday, tuesday)", &survey.MultiSelect{
19 | Message: "What days do you prefer:",
20 | Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
21 | Default: []string{"Sunday", "Tuesday"},
22 | }, &answer,
23 | },
24 | {
25 | "default not found", &survey.MultiSelect{
26 | Message: "What days do you prefer:",
27 | Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
28 | Default: []string{"Sundayaa"},
29 | }, &answer,
30 | },
31 | {
32 | "no help - type ?", &survey.MultiSelect{
33 | Message: "What days do you prefer:",
34 | Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
35 | Default: []string{"Sundayaa"},
36 | }, &answer,
37 | },
38 | }
39 |
40 | func main() {
41 | TestUtil.RunTable(table)
42 | }
43 |
--------------------------------------------------------------------------------
/tests/password.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/tj/survey"
5 | "github.com/tj/survey/tests/util"
6 | )
7 |
8 | var value = ""
9 |
10 | var table = []TestUtil.TestTableEntry{
11 | {
12 | "standard", &survey.Password{Message: "Please type your password:"}, &value,
13 | },
14 | {
15 | "please make sure paste works", &survey.Password{Message: "Please paste your password:"}, &value,
16 | },
17 | {
18 | "no help, send '?'", &survey.Password{Message: "Please type your password:"}, &value,
19 | },
20 | }
21 |
22 | func main() {
23 | TestUtil.RunTable(table)
24 | }
25 |
--------------------------------------------------------------------------------
/tests/select.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/tj/survey"
5 | "github.com/tj/survey/tests/util"
6 | )
7 |
8 | var answer = ""
9 |
10 | var goodTable = []TestUtil.TestTableEntry{
11 | {
12 | "standard", &survey.Select{
13 | Message: "Choose a color:",
14 | Options: []string{"red", "blue", "green"},
15 | }, &answer,
16 | },
17 | {
18 | "short", &survey.Select{
19 | Message: "Choose a color:",
20 | Options: []string{"red", "blue"},
21 | }, &answer,
22 | },
23 | {
24 | "default", &survey.Select{
25 | Message: "Choose a color (should default blue):",
26 | Options: []string{"red", "blue", "green"},
27 | Default: "blue",
28 | }, &answer,
29 | },
30 | {
31 | "one", &survey.Select{
32 | Message: "Choose one:",
33 | Options: []string{"hello"},
34 | }, &answer,
35 | },
36 | {
37 | "no help, type ?", &survey.Select{
38 | Message: "Choose a color:",
39 | Options: []string{"red", "blue"},
40 | }, &answer,
41 | },
42 | {
43 | "passes through bottom", &survey.Select{
44 | Message: "Choose one:",
45 | Options: []string{"red", "blue"},
46 | }, &answer,
47 | },
48 | {
49 | "passes through top", &survey.Select{
50 | Message: "Choose one:",
51 | Options: []string{"red", "blue"},
52 | }, &answer,
53 | },
54 | }
55 |
56 | var badTable = []TestUtil.TestTableEntry{
57 | {
58 | "no options", &survey.Select{
59 | Message: "Choose one:",
60 | }, &answer,
61 | },
62 | }
63 |
64 | func main() {
65 | TestUtil.RunTable(goodTable)
66 | TestUtil.RunErrorTable(badTable)
67 | }
68 |
--------------------------------------------------------------------------------
/tests/selectThenInput.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/tj/survey"
7 | )
8 |
9 | // the questions to ask
10 | var simpleQs = []*survey.Question{
11 | {
12 | Name: "color",
13 | Prompt: &survey.Select{
14 | Message: "Choose a color:",
15 | Options: []string{"red", "blue", "green"},
16 | },
17 | Validate: survey.Required,
18 | },
19 | {
20 | Name: "name",
21 | Prompt: &survey.Input{
22 | Message: "What is your name?",
23 | },
24 | Validate: survey.Required,
25 | },
26 | }
27 |
28 | func main() {
29 | answers := struct {
30 | Color string
31 | Name string
32 | }{}
33 | // ask the question
34 | err := survey.Ask(simpleQs, &answers)
35 |
36 | if err != nil {
37 | fmt.Println(err.Error())
38 | return
39 | }
40 | // print the answers
41 | fmt.Printf("%s chose %s.\n", answers.Name, answers.Color)
42 | }
43 |
--------------------------------------------------------------------------------
/tests/util/test.go:
--------------------------------------------------------------------------------
1 | package TestUtil
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 |
7 | "github.com/tj/survey"
8 | )
9 |
10 | type TestTableEntry struct {
11 | Name string
12 | Prompt survey.Prompt
13 | Value interface{}
14 | }
15 |
16 | func formatAnswer(ans interface{}) {
17 | // show the answer to the user
18 | fmt.Printf("Answered %v.\n", reflect.ValueOf(ans).Elem())
19 | fmt.Println("---------------------")
20 | }
21 |
22 | func RunTable(table []TestTableEntry) {
23 | // go over every entry in the table
24 | for _, entry := range table {
25 | // tell the user what we are going to ask them
26 | fmt.Println(entry.Name)
27 | // perform the ask
28 | err := survey.AskOne(entry.Prompt, entry.Value, nil)
29 | if err != nil {
30 | fmt.Printf("AskOne on %v's prompt failed: %v.", entry.Name, err.Error())
31 | break
32 | }
33 | // show the answer to the user
34 | formatAnswer(entry.Value)
35 | }
36 | }
37 |
38 | func RunErrorTable(table []TestTableEntry) {
39 | // go over every entry in the table
40 | for _, entry := range table {
41 | // tell the user what we are going to ask them
42 | fmt.Println(entry.Name)
43 | // perform the ask
44 | err := survey.AskOne(entry.Prompt, entry.Value, nil)
45 | if err == nil {
46 | fmt.Printf("AskOne on %v's prompt didn't fail.", entry.Name)
47 | break
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/validate.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "reflect"
7 | )
8 |
9 | // Required does not allow an empty value
10 | func Required(val interface{}) error {
11 | // if the value passed in is the zero value of the appropriate type
12 | if isZero(reflect.ValueOf(val)) {
13 | return errors.New("Value is required")
14 | }
15 | return nil
16 | }
17 |
18 | // MaxLength requires that the string is no longer than the specified value
19 | func MaxLength(length int) Validator {
20 | // return a validator that checks the length of the string
21 | return func(val interface{}) error {
22 | if str, ok := val.(string); ok {
23 | // if the string is longer than the given value
24 | if len(str) > length {
25 | // yell loudly
26 | return fmt.Errorf("value is too long. Max length is %v", length)
27 | }
28 | } else {
29 | // otherwise we cannot convert the value into a string and cannot enforce length
30 | return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name())
31 | }
32 |
33 | // the input is fine
34 | return nil
35 | }
36 | }
37 |
38 | // MinLength requires that the string is longer or equal in length to the specified value
39 | func MinLength(length int) Validator {
40 | // return a validator that checks the length of the string
41 | return func(val interface{}) error {
42 | if str, ok := val.(string); ok {
43 | // if the string is shorter than the given value
44 | if len(str) < length {
45 | // yell loudly
46 | return fmt.Errorf("value is too short. Min length is %v", length)
47 | }
48 | } else {
49 | // otherwise we cannot convert the value into a string and cannot enforce length
50 | return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name())
51 | }
52 |
53 | // the input is fine
54 | return nil
55 | }
56 | }
57 |
58 | // ComposeValidators is a variadic function used to create one validator from many.
59 | func ComposeValidators(validators ...Validator) Validator {
60 | // return a validator that calls each one sequentially
61 | return func(val interface{}) error {
62 | // execute each validator
63 | for _, validator := range validators {
64 | // if the string is not valid
65 | if err := validator(val); err != nil {
66 | // return the error
67 | return err
68 | }
69 | }
70 | // we passed all validators, the string is valid
71 | return nil
72 | }
73 | }
74 |
75 | // isZero returns true if the passed value is the zero object
76 | func isZero(v reflect.Value) bool {
77 | switch v.Kind() {
78 | case reflect.Slice, reflect.Map:
79 | return v.Len() == 0
80 | }
81 |
82 | // compare the types directly with more general coverage
83 | return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
84 | }
85 |
--------------------------------------------------------------------------------
/validate_test.go:
--------------------------------------------------------------------------------
1 | package survey
2 |
3 | import (
4 | "math/rand"
5 | "testing"
6 | )
7 |
8 | func TestRequired_canSucceedOnPrimitiveTypes(t *testing.T) {
9 | // a string to test
10 | str := "hello"
11 | // if the string is not valid
12 | if valid := Required(str); valid != nil {
13 | //
14 | t.Error("Non null returned an error when one wasn't expected.")
15 | }
16 | }
17 |
18 | func TestRequired_canFailOnPrimitiveTypes(t *testing.T) {
19 | // a string to test
20 | str := ""
21 | // if the string is valid
22 | if notValid := Required(str); notValid == nil {
23 | //
24 | t.Error("Non null did not return an error when one was expected.")
25 | }
26 | }
27 |
28 | func TestRequired_canSucceedOnMap(t *testing.T) {
29 | // an non-empty map to test
30 | val := map[string]int{"hello": 1}
31 | // if the string is not valid
32 | if valid := Required(val); valid != nil {
33 | //
34 | t.Error("Non null returned an error when one wasn't expected.")
35 | }
36 | }
37 |
38 | func TestRequired_canFailOnMap(t *testing.T) {
39 | // an non-empty map to test
40 | val := map[string]int{}
41 | // if the string is valid
42 | if notValid := Required(val); notValid == nil {
43 | //
44 | t.Error("Non null did not return an error when one was expected.")
45 | }
46 | }
47 |
48 | func TestRequired_canSucceedOnLists(t *testing.T) {
49 | // a string to test
50 | str := []string{"hello"}
51 | // if the string is not valid
52 | if valid := Required(str); valid != nil {
53 | //
54 | t.Error("Non null returned an error when one wasn't expected.")
55 | }
56 | }
57 |
58 | func TestRequired_canFailOnLists(t *testing.T) {
59 | // a string to test
60 | str := []string{}
61 | // if the string is not valid
62 | if notValid := Required(str); notValid == nil {
63 | //
64 | t.Error("Non null did not return an error when one was expected.")
65 | }
66 | }
67 |
68 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
69 |
70 | func randString(n int) string {
71 | b := make([]byte, n)
72 | for i := range b {
73 | b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
74 | }
75 | return string(b)
76 | }
77 |
78 | func TestMaxLength(t *testing.T) {
79 | // the string to test
80 | testStr := randString(150)
81 | // validate the string
82 | if err := MaxLength(140)(testStr); err == nil {
83 | t.Error("No error returned with input greater than 150 characters.")
84 | }
85 | }
86 |
87 | func TestMinLength(t *testing.T) {
88 | // validate the string
89 | if err := MinLength(12)(randString(10)); err == nil {
90 | t.Error("No error returned with input less than 12 characters.")
91 | }
92 | }
93 |
94 | func TestMinLength_onInt(t *testing.T) {
95 | // validate the string
96 | if err := MinLength(12)(1); err == nil {
97 | t.Error("No error returned when enforcing length on int.")
98 | }
99 | }
100 |
101 | func TestMaxLength_onInt(t *testing.T) {
102 | // validate the string
103 | if err := MaxLength(12)(1); err == nil {
104 | t.Error("No error returned when enforcing length on int.")
105 | }
106 | }
107 |
108 | func TestComposeValidators_passes(t *testing.T) {
109 | // create a validator that requires a string of no more than 10 characters
110 | valid := ComposeValidators(
111 | Required,
112 | MaxLength(10),
113 | )
114 |
115 | str := randString(12)
116 | // if a valid string fails
117 | if err := valid(str); err == nil {
118 | // the test failed
119 | t.Error("Composed validator did not pass. Wanted string less than 10 chars, passed in", str)
120 | }
121 |
122 | }
123 |
124 | func TestComposeValidators_failsOnFirstError(t *testing.T) {
125 | // create a validator that requires a string of no more than 10 characters
126 | valid := ComposeValidators(
127 | Required,
128 | MaxLength(10),
129 | )
130 |
131 | // if an empty string passes
132 | if err := valid(""); err == nil {
133 | // the test failed
134 | t.Error("Composed validator did not fail on first test like expected.")
135 | }
136 | }
137 |
138 | func TestComposeValidators_failsOnSubsequentValidators(t *testing.T) {
139 | // create a validator that requires a string of no more than 10 characters
140 | valid := ComposeValidators(
141 | Required,
142 | MaxLength(10),
143 | )
144 |
145 | str := randString(12)
146 | // if a string longer than 10 passes
147 | if err := valid(str); err == nil {
148 | // the test failed
149 | t.Error("Composed validator did not fail on second first test like expected. Should fail max length > 10 :", str)
150 | }
151 | }
152 |
--------------------------------------------------------------------------------