├── .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 | [![Build Status](https://travis-ci.org/AlecAivazis/survey.svg?branch=feature%2Fpretty)](https://travis-ci.org/AlecAivazis/survey) 3 | [![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg)](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 | ![](https://zippy.gfycat.com/AmusingBossyArrowworm.gif) 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 | --------------------------------------------------------------------------------