pax_global_header00006660000000000000000000000064143444026220014513gustar00rootroot0000000000000052 comment=fa37277e6394c29db7bcc94062cb30cd7785a126 survey-2.3.7/000077500000000000000000000000001434440262200130615ustar00rootroot00000000000000survey-2.3.7/.github/000077500000000000000000000000001434440262200144215ustar00rootroot00000000000000survey-2.3.7/.github/ISSUE_TEMPLATE/000077500000000000000000000000001434440262200166045ustar00rootroot00000000000000survey-2.3.7/.github/ISSUE_TEMPLATE/bug.md000066400000000000000000000004111434440262200176770ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: 'Bug' assignees: '' --- **What operating system and terminal are you using?** **An example that showcases the bug.** **What did you expect to see?** **What did you see instead?** survey-2.3.7/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000002011434440262200207660ustar00rootroot00000000000000--- name: Ask for help about: Suggest an idea for this project or ask for help title: '' labels: 'Question' assignees: '' --- survey-2.3.7/.github/matchers.json000066400000000000000000000006301434440262200171210ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "go", "pattern": [ { "regexp": "^\\s*(.+\\.go):(\\d+):(?:(\\d+):)? (?:(warning): )?(.*)", "file": 1, "line": 2, "column": 3, "severity": 4, "message": 5 } ] } ] } survey-2.3.7/.github/workflows/000077500000000000000000000000001434440262200164565ustar00rootroot00000000000000survey-2.3.7/.github/workflows/test.yml000066400000000000000000000012101434440262200201520ustar00rootroot00000000000000name: Test on: [push, pull_request] jobs: test: strategy: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] go: ["1.19", "1.18", "1.17", "1.16"] runs-on: ${{ matrix.platform }} steps: - name: Check out code uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v3 with: go-version: "${{ matrix.go }}" cache: true - name: Add problem matcher run: echo "::add-matcher::$PWD/.github/matchers.json" - name: Run tests run: go test -timeout 30s -v $(go list -v ./... | grep -vE '(tests|examples)$') survey-2.3.7/CONTRIBUTING.md000066400000000000000000000056571434440262200153270ustar00rootroot00000000000000# Contributing to Survey 🎉🎉 First off, thanks for the interest in contributing to `survey`! 🎉🎉 The following is a set of guidelines to follow when contributing to this package. These are not hard rules, please use common sense and feel free to propose changes to this document in a pull request. ## Code of Conduct This project and its contibutors are expected to uphold the [Go Community Code of Conduct](https://golang.org/conduct). By participating, you are expected to follow these guidelines. ## Getting help * [Open an issue](https://github.com/AlecAivazis/survey/issues/new/choose) * Reach out to `@AlecAivazis` or `@mislav` in the Gophers slack (please use only when urgent) ## Submitting a contribution When submitting a contribution, - Try to make a series of smaller changes instead of one large change - Provide a description of each change that you are proposing - Reference the issue addressed by your pull request (if there is one) - Document all new exported Go APIs - Update the project's README when applicable - Include unit tests if possible - Contributions with visual ramifications or interaction changes should be accompanied with an integration test—see below for details. ## Writing and running tests When submitting features, please add as many units tests as necessary to test both positive and negative cases. Integration tests for survey uses [go-expect](https://github.com/Netflix/go-expect) to expect a match on stdout and respond on stdin. Since `os.Stdout` in a `go test` process is not a TTY, you need a way to interpret terminal / ANSI escape sequences for things like `CursorLocation`. The stdin/stdout handled by `go-expect` is also multiplexed to a [virtual terminal](https://github.com/hinshun/vt10x). For example, you can extend the tests for Input by specifying the following test case: ```go { "Test Input prompt interaction", // Name of the test. &Input{ // An implementation of the survey.Prompt interface. Message: "What is your name?", }, func(c *expect.Console) { // An expect procedure. You can expect strings / regexps and c.ExpectString("What is your name?") // write back strings / bytes to its psuedoterminal for survey. c.SendLine("Johnny Appleseed") c.ExpectEOF() // Nothing is read from the tty without an expect, and once an // expectation is met, no further bytes are read. End your // procedure with `c.ExpectEOF()` to read until survey finishes. }, "Johnny Appleseed", // The expected result. } ``` If you want to write your own `go-expect` test from scratch, you'll need to instantiate a virtual terminal, multiplex it into an `*expect.Console`, and hook up its tty with survey's optional stdio. Please see `go-expect` [documentation](https://godoc.org/github.com/Netflix/go-expect) for more detail. survey-2.3.7/LICENSE000066400000000000000000000020551434440262200140700ustar00rootroot00000000000000MIT License Copyright (c) 2018 Alec Aivazis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. survey-2.3.7/README.md000066400000000000000000000413731434440262200143500ustar00rootroot00000000000000# Survey [![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg)](https://pkg.go.dev/github.com/AlecAivazis/survey/v2) A library for building interactive and accessible prompts on terminals supporting ANSI escape sequences. ```go package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var qs = []*survey.Question{ { Name: "name", Prompt: &survey.Input{Message: "What is your name?"}, Validate: survey.Required, Transform: survey.Title, }, { Name: "color", Prompt: &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, Default: "red", }, }, { Name: "age", Prompt: &survey.Input{Message: "How old are you?"}, }, } func main() { // the answers will be written to this struct answers := struct { Name string // survey will match the question and field names FavoriteColor string `survey:"color"` // or you can tag fields to match a specific name Age int // if the types don't match, survey will convert it }{} // perform the questions err := survey.Ask(qs, &answers) if err != nil { fmt.Println(err.Error()) return } fmt.Printf("%s chose %s.", answers.Name, answers.FavoriteColor) } ``` ## Examples Examples can be found in the `examples/` directory. Run them to see basic behavior: ```bash go run examples/simple.go go run examples/validation.go ``` ## Running the Prompts There are two primary ways to execute prompts and start collecting information from your users: `Ask` and `AskOne`. The primary difference is whether you are interested in collecting a single piece of information or if you have a list of questions to ask whose answers should be collected in a single struct. For most basic usecases, `Ask` should be enough. However, for surveys with complicated branching logic, we recommend that you break out your questions into multiple calls to both of these functions to fit your needs. ### Configuring the Prompts Most prompts take fine-grained configuration through fields on the structs you instantiate. It is also possible to change survey's default behaviors by passing `AskOpts` to either `Ask` or `AskOne`. Examples in this document will do both interchangeably: ```golang prompt := &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, // can pass a validator directly Validate: survey.Required, } // or define a default for the single call to `AskOne` // the answer will get written to the color variable survey.AskOne(prompt, &color, survey.WithValidator(survey.Required)) // or define a default for every entry in a list of questions // the answer will get copied into the matching field of the struct as shown above survey.Ask(questions, &answers, survey.WithValidator(survey.Required)) ``` ## Prompts ### Input ```golang name := "" prompt := &survey.Input{ Message: "ping", } survey.AskOne(prompt, &name) ``` #### Suggestion Options ```golang file := "" prompt := &survey.Input{ Message: "inform a file to save:", Suggest: func (toComplete string) []string { files, _ := filepath.Glob(toComplete + "*") return files }, } } survey.AskOne(prompt, &file) ``` ### Multiline ```golang text := "" prompt := &survey.Multiline{ Message: "ping", } survey.AskOne(prompt, &text) ``` ### Password ```golang password := "" prompt := &survey.Password{ Message: "Please type your password", } survey.AskOne(prompt, &password) ``` ### Confirm ```golang name := false prompt := &survey.Confirm{ Message: "Do you like pie?", } survey.AskOne(prompt, &name) ``` ### Select ```golang color := "" prompt := &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, } survey.AskOne(prompt, &color) ``` Fields and values that come from a `Select` prompt can be one of two different things. If you pass an `int` the field will have the value of the selected index. If you instead pass a string, the string value selected will be written to the field. The user can also press `esc` to toggle the ability cycle through the options with the j and k keys to do down and up respectively. By default, the select prompt is limited to showing 7 options at a time and will paginate lists of options longer than that. This can be changed a number of ways: ```golang // as a field on a single select prompt := &survey.MultiSelect{..., PageSize: 10} // or as an option to Ask or AskOne survey.AskOne(prompt, &days, survey.WithPageSize(10)) ``` #### Select options description The optional description text can be used to add extra information to each option listed in the select prompt: ```golang color := "" prompt := &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, Description: func(value string, index int) string { if value == "red" { return "My favorite color" } return "" }, } survey.AskOne(prompt, &color) // Assuming that the user chose "red - My favorite color": fmt.Println(color) //=> "red" ``` ### MultiSelect ![Example](img/multi-select-all-none.gif) ```golang days := []string{} prompt := &survey.MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, } survey.AskOne(prompt, &days) ``` Fields and values that come from a `MultiSelect` prompt can be one of two different things. If you pass an `int` the field will have a slice of the selected indices. If you instead pass a string, a slice of the string values selected will be written to the field. The user can also press `esc` to toggle the ability cycle through the options with the j and k keys to do down and up respectively. By default, the MultiSelect prompt is limited to showing 7 options at a time and will paginate lists of options longer than that. This can be changed a number of ways: ```golang // as a field on a single select prompt := &survey.MultiSelect{..., PageSize: 10} // or as an option to Ask or AskOne survey.AskOne(prompt, &days, survey.WithPageSize(10)) ``` ### Editor Launches the user's preferred editor (defined by the \$VISUAL or \$EDITOR environment variables) on a temporary file. Once the user exits their editor, the contents of the temporary file are read in as the result. If neither of those are present, notepad (on Windows) or vim (Linux or Mac) is used. You can also specify a [pattern](https://golang.org/pkg/io/ioutil/#TempFile) for the name of the temporary file. This can be useful for ensuring syntax highlighting matches your usecase. ```golang prompt := &survey.Editor{ Message: "Shell code snippet", FileName: "*.sh", } survey.AskOne(prompt, &content) ``` ## Filtering Options By default, the user can filter for options in Select and MultiSelects by typing while the prompt is active. This will filter out all options that don't contain the typed string anywhere in their name, ignoring case. A custom filter function can also be provided to change this behavior: ```golang func myFilter(filterValue string, optValue string, optIndex int) bool { // only include the option if it includes the filter and has length greater than 5 return strings.Contains(optValue, filterValue) && len(optValue) >= 5 } // configure it for a specific prompt &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, Filter: myFilter, } // or define a default for all of the questions survey.AskOne(prompt, &color, survey.WithFilter(myFilter)) ``` ## Keeping the filter active By default the filter will disappear if the user selects one of the filtered elements. Once the user selects one element the filter setting is gone. However the user can prevent this from happening and keep the filter active for multiple selections in a e.g. MultiSelect: ```golang // configure it for a specific prompt &Select{ Message: "Choose a color:", Options: []string{"light-green", "green", "dark-green", "red"}, KeepFilter: true, } // or define a default for all of the questions survey.AskOne(prompt, &color, survey.WithKeepFilter(true)) ``` ## Validation Validating individual responses for a particular question can be done by defining a `Validate` field on the `survey.Question` to be validated. This function takes an `interface{}` type and returns an error to show to the user, prompting them for another response. Like usual, validators can be provided directly to the prompt or with `survey.WithValidator`: ```golang q := &survey.Question{ Prompt: &survey.Input{Message: "Hello world validation"}, Validate: func (val interface{}) error { // since we are validating an Input, the assertion will always succeed if str, ok := val.(string) ; !ok || len(str) > 10 { return errors.New("This response cannot be longer than 10 characters.") } return nil }, } color := "" prompt := &survey.Input{ Message: "Whats your name?" } // you can pass multiple validators here and survey will make sure each one passes survey.AskOne(prompt, &color, survey.WithValidator(survey.Required)) ``` ### Built-in Validators `survey` comes prepackaged with a few validators to fit common situations. Currently these validators include: | name | valid types | description | notes | | ------------ | -------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Required | any | Rejects zero values of the response type | Boolean values pass straight through since the zero value (false) is a valid response | | MinLength(n) | string | Enforces that a response is at least the given length | | | MaxLength(n) | string | Enforces that a response is no longer than the given length | | | MaxItems(n) | []OptionAnswer | Enforces that a response has no more selections of the indicated | | | MinItems(n) | []OptionAnswer | Enforces that a response has no less selections of the indicated | | ## Help Text All of the prompts have a `Help` field which can be defined to provide more information to your users: ```golang &survey.Input{ Message: "What is your phone number:", Help: "Phone number should include the area code", } ``` ## Removing the "Select All" and "Select None" options By default, users can select all of the multi-select options using the right arrow key. To prevent users from being able to do this (and remove the ` to all` message from the prompt), use the option `WithRemoveSelectAll`: ```golang import ( "github.com/AlecAivazis/survey/v2" ) number := "" prompt := &survey.Input{ Message: "This question has the select all option removed", } survey.AskOne(prompt, &number, survey.WithRemoveSelectAll()) ``` Also by default, users can use the left arrow key to unselect all of the options. To prevent users from being able to do this (and remove the ` to none` message from the prompt), use the option `WithRemoveSelectNone`: ```golang import ( "github.com/AlecAivazis/survey/v2" ) number := "" prompt := &survey.Input{ Message: "This question has the select all option removed", } survey.AskOne(prompt, &number, survey.WithRemoveSelectNone()) ``` ### Changing the input rune In some situations, `?` is a perfectly valid response. To handle this, you can change the rune that survey looks for with `WithHelpInput`: ```golang import ( "github.com/AlecAivazis/survey/v2" ) number := "" prompt := &survey.Input{ Message: "If you have this need, please give me a reasonable message.", Help: "I couldn't come up with one.", } survey.AskOne(prompt, &number, survey.WithHelpInput('^')) ``` ## Changing the Icons Changing the icons and their color/format can be done by passing the `WithIcons` option. The format follows the patterns outlined [here](https://github.com/mgutz/ansi#style-format). For example: ```golang import ( "github.com/AlecAivazis/survey/v2" ) number := "" prompt := &survey.Input{ Message: "If you have this need, please give me a reasonable message.", Help: "I couldn't come up with one.", } survey.AskOne(prompt, &number, survey.WithIcons(func(icons *survey.IconSet) { // you can set any icons icons.Question.Text = "⁇" // for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format icons.Question.Format = "yellow+hb" })) ``` The icons and their default text and format are summarized below: | name | text | format | description | | -------------- | ---- | ---------- | ------------------------------------------------------------- | | Error | X | red | Before an error | | Help | i | cyan | Before help text | | Question | ? | green+hb | Before the message of a prompt | | SelectFocus | > | green | Marks the current focus in `Select` and `MultiSelect` prompts | | UnmarkedOption | [ ] | default+hb | Marks an unselected option in a `MultiSelect` prompt | | MarkedOption | [x] | cyan+b | Marks a chosen selection in a `MultiSelect` prompt | ## Custom Types survey will assign prompt answers to your custom types if they implement this interface: ```golang type Settable interface { WriteAnswer(field string, value interface{}) error } ``` Here is an example how to use them: ```golang type MyValue struct { value string } func (my *MyValue) WriteAnswer(name string, value interface{}) error { my.value = value.(string) } myval := MyValue{} survey.AskOne( &survey.Input{ Message: "Enter something:", }, &myval ) ``` ## Testing You can test your program's interactive prompts using [go-expect](https://github.com/Netflix/go-expect). The library can be used to expect a match on stdout and respond on stdin. Since `os.Stdout` in a `go test` process is not a TTY, if you are manipulating the cursor or using `survey`, you will need a way to interpret terminal / ANSI escape sequences for things like `CursorLocation`. `vt10x.NewVT10XConsole` will create a `go-expect` console that also multiplexes stdio to an in-memory [virtual terminal](https://github.com/hinshun/vt10x). For some examples, you can see any of the tests in this repo. ## FAQ ### What kinds of IO are supported by `survey`? survey aims to support most terminal emulators; it expects support for ANSI escape sequences. This means that reading from piped stdin or writing to piped stdout is **not supported**, and likely to break your application in these situations. See [#337](https://github.com/AlecAivazis/survey/pull/337#issue-581351617) ### Why isn't Ctrl-C working? Ordinarily, when you type Ctrl-C, the terminal recognizes this as the QUIT button and delivers a SIGINT signal to the process, which terminates it. However, Survey temporarily configures the terminal to deliver control codes as ordinary input bytes. When Survey reads a ^C byte (ASCII \x03, "end of text"), it interrupts the current survey and returns a `github.com/AlecAivazis/survey/v2/terminal.InterruptErr` from `Ask` or `AskOne`. If you want to stop the process, handle the returned error in your code: ```go err := survey.AskOne(prompt, &myVar) if err != nil { if err == terminal.InterruptErr { log.Fatal("interrupted") } ... } ``` survey-2.3.7/confirm.go000066400000000000000000000071701434440262200150520ustar00rootroot00000000000000package survey import ( "fmt" "regexp" ) // Confirm is a regular text input that accept yes/no answers. Response type is a bool. type Confirm struct { Renderer Message string Default bool Help string } // data available to the templates when processing type ConfirmTemplateData struct { Confirm Answer string ShowHelp bool Config *PromptConfig } // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format var ConfirmQuestionTemplate = ` {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }} {{color "reset"}} {{- if .Answer}} {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} {{- else }} {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}} {{- color "white"}}{{if .Default}}(Y/n) {{else}}(y/N) {{end}}{{color "reset"}} {{- end}}` // the regex for answers var ( yesRx = regexp.MustCompile("^(?i:y(?:es)?)$") noRx = regexp.MustCompile("^(?i:n(?:o)?)$") ) func yesNo(t bool) string { if t { return "Yes" } return "No" } func (c *Confirm) getBool(showHelp bool, config *PromptConfig) (bool, error) { cursor := c.NewCursor() rr := c.NewRuneReader() _ = rr.SetTermMode() defer func() { _ = rr.RestoreTermMode() }() // start waiting for input for { line, err := rr.ReadLine(0) if err != nil { return false, err } // move back up a line to compensate for the \n echoed from terminal cursor.PreviousLine(1) val := string(line) // get the answer that matches the var answer bool switch { case yesRx.Match([]byte(val)): answer = true case noRx.Match([]byte(val)): answer = false case val == "": answer = c.Default case val == config.HelpInput && c.Help != "": err := c.Render( ConfirmQuestionTemplate, ConfirmTemplateData{ Confirm: *c, ShowHelp: true, Config: config, }, ) if err != nil { // use the default value and bubble up return c.Default, err } showHelp = true continue default: // we didnt get a valid answer, so print error and prompt again //lint:ignore ST1005 it should be fine for this error message to have punctuation if err := c.Error(config, fmt.Errorf("%q is not a valid answer, please try again.", val)); err != nil { return c.Default, err } err := c.Render( ConfirmQuestionTemplate, ConfirmTemplateData{ Confirm: *c, ShowHelp: showHelp, Config: config, }, ) if err != nil { // use the default value and bubble up return c.Default, err } continue } return answer, nil } } /* Prompt prompts the user with a simple text field and expects a reply followed by a carriage return. likesPie := false prompt := &survey.Confirm{ Message: "What is your name?" } survey.AskOne(prompt, &likesPie) */ func (c *Confirm) Prompt(config *PromptConfig) (interface{}, error) { // render the question template err := c.Render( ConfirmQuestionTemplate, ConfirmTemplateData{ Confirm: *c, Config: config, }, ) if err != nil { return "", err } // get input and return return c.getBool(false, config) } // Cleanup overwrite the line with the finalized formatted version func (c *Confirm) Cleanup(config *PromptConfig, val interface{}) error { // if the value was previously true ans := yesNo(val.(bool)) // render the template return c.Render( ConfirmQuestionTemplate, ConfirmTemplateData{ Confirm: *c, Answer: ans, Config: config, }, ) } survey-2.3.7/confirm_test.go000066400000000000000000000073241434440262200161120ustar00rootroot00000000000000package survey import ( "bytes" "fmt" "io" "os" "testing" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" "github.com/stretchr/testify/assert" ) func init() { // disable color output for all prompts to simplify testing core.DisableColor = true } func TestConfirmRender(t *testing.T) { tests := []struct { title string prompt Confirm data ConfirmTemplateData expected string }{ { "Test Confirm question output with default true", Confirm{Message: "Is pizza your favorite food?", Default: true}, ConfirmTemplateData{}, fmt.Sprintf("%s Is pizza your favorite food? (Y/n) ", defaultIcons().Question.Text), }, { "Test Confirm question output with default false", Confirm{Message: "Is pizza your favorite food?", Default: false}, ConfirmTemplateData{}, fmt.Sprintf("%s Is pizza your favorite food? (y/N) ", defaultIcons().Question.Text), }, { "Test Confirm answer output", Confirm{Message: "Is pizza your favorite food?"}, ConfirmTemplateData{Answer: "Yes"}, fmt.Sprintf("%s Is pizza your favorite food? Yes\n", defaultIcons().Question.Text), }, { "Test Confirm with help but help message is hidden", Confirm{Message: "Is pizza your favorite food?", Help: "This is helpful"}, ConfirmTemplateData{}, fmt.Sprintf("%s Is pizza your favorite food? [%s for help] (y/N) ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput)), }, { "Test Confirm help output with help message shown", Confirm{Message: "Is pizza your favorite food?", Help: "This is helpful"}, ConfirmTemplateData{ShowHelp: true}, fmt.Sprintf("%s This is helpful\n%s Is pizza your favorite food? (y/N) ", defaultIcons().Help.Text, defaultIcons().Question.Text), }, } for _, test := range tests { t.Run(test.title, func(t *testing.T) { r, w, err := os.Pipe() assert.NoError(t, err) test.prompt.WithStdio(terminal.Stdio{Out: w}) test.data.Confirm = test.prompt // set the runtime config test.data.Config = defaultPromptConfig() err = test.prompt.Render( ConfirmQuestionTemplate, test.data, ) assert.NoError(t, err) assert.NoError(t, w.Close()) var buf bytes.Buffer _, err = io.Copy(&buf, r) assert.NoError(t, err) assert.Contains(t, buf.String(), test.expected) }) } } func TestConfirmPrompt(t *testing.T) { tests := []PromptTest{ { "Test Confirm prompt interaction", &Confirm{ Message: "Is pizza your favorite food?", }, func(c expectConsole) { c.ExpectString("Is pizza your favorite food? (y/N)") c.SendLine("n") c.ExpectEOF() }, false, }, { "Test Confirm prompt interaction with default", &Confirm{ Message: "Is pizza your favorite food?", Default: true, }, func(c expectConsole) { c.ExpectString("Is pizza your favorite food? (Y/n)") c.SendLine("") c.ExpectEOF() }, true, }, { "Test Confirm prompt interaction overriding default", &Confirm{ Message: "Is pizza your favorite food?", Default: true, }, func(c expectConsole) { c.ExpectString("Is pizza your favorite food? (Y/n)") c.SendLine("n") c.ExpectEOF() }, false, }, { "Test Confirm prompt interaction and prompt for help", &Confirm{ Message: "Is pizza your favorite food?", Help: "It probably is", }, func(c expectConsole) { c.ExpectString( fmt.Sprintf( "Is pizza your favorite food? [%s for help] (y/N)", string(defaultPromptConfig().HelpInput), ), ) c.SendLine("?") c.ExpectString("It probably is") c.SendLine("Y") c.ExpectEOF() }, true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { RunPromptTest(t, test) }) } } survey-2.3.7/core/000077500000000000000000000000001434440262200140115ustar00rootroot00000000000000survey-2.3.7/core/template.go000066400000000000000000000056061434440262200161620ustar00rootroot00000000000000package core import ( "bytes" "os" "sync" "text/template" "github.com/mgutz/ansi" ) // DisableColor can be used to make testing reliable var DisableColor = false var TemplateFuncsWithColor = map[string]interface{}{ // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format "color": ansi.ColorCode, } var TemplateFuncsNoColor = map[string]interface{}{ // Templates without Color formatting. For layout/ testing. "color": func(color string) string { return "" }, } // envColorDisabled returns if output colors are forbid by environment variables func envColorDisabled() bool { return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0" } // envColorForced returns if output colors are forced from environment variables func envColorForced() bool { val, ok := os.LookupEnv("CLICOLOR_FORCE") return ok && val != "0" } // RunTemplate returns two formatted strings given a template and // the data it requires. The first string returned is generated for // user-facing output and may or may not contain ANSI escape codes // for colored output. The second string does not contain escape codes // and can be used by the renderer for layout purposes. func RunTemplate(tmpl string, data interface{}) (string, string, error) { tPair, err := GetTemplatePair(tmpl) if err != nil { return "", "", err } userBuf := bytes.NewBufferString("") err = tPair[0].Execute(userBuf, data) if err != nil { return "", "", err } layoutBuf := bytes.NewBufferString("") err = tPair[1].Execute(layoutBuf, data) if err != nil { return userBuf.String(), "", err } return userBuf.String(), layoutBuf.String(), err } var ( memoizedGetTemplate = map[string][2]*template.Template{} memoMutex = &sync.RWMutex{} ) // GetTemplatePair returns a pair of compiled templates where the // first template is generated for user-facing output and the // second is generated for use by the renderer. The second // template does not contain any color escape codes, whereas // the first template may or may not depending on DisableColor. func GetTemplatePair(tmpl string) ([2]*template.Template, error) { memoMutex.RLock() if t, ok := memoizedGetTemplate[tmpl]; ok { memoMutex.RUnlock() return t, nil } memoMutex.RUnlock() templatePair := [2]*template.Template{nil, nil} templateNoColor, err := template.New("prompt").Funcs(TemplateFuncsNoColor).Parse(tmpl) if err != nil { return [2]*template.Template{}, err } templatePair[1] = templateNoColor envColorHide := envColorDisabled() && !envColorForced() if DisableColor || envColorHide { templatePair[0] = templatePair[1] } else { templateWithColor, err := template.New("prompt").Funcs(TemplateFuncsWithColor).Parse(tmpl) templatePair[0] = templateWithColor if err != nil { return [2]*template.Template{}, err } } memoMutex.Lock() memoizedGetTemplate[tmpl] = templatePair memoMutex.Unlock() return templatePair, nil } survey-2.3.7/core/write.go000066400000000000000000000237171434440262200155040ustar00rootroot00000000000000package core import ( "errors" "fmt" "reflect" "strconv" "strings" "time" ) // the tag used to denote the name of the question const tagName = "survey" // Settable allow for configuration when assigning answers type Settable interface { WriteAnswer(field string, value interface{}) error } // OptionAnswer is the return type of Selects/MultiSelects that lets the appropriate information // get copied to the user's struct type OptionAnswer struct { Value string Index int } type reflectField struct { value reflect.Value fieldType reflect.StructField } func OptionAnswerList(incoming []string) []OptionAnswer { list := []OptionAnswer{} for i, opt := range incoming { list = append(list, OptionAnswer{Value: opt, Index: i}) } return list } func WriteAnswer(t interface{}, name string, v interface{}) (err error) { // if the field is a custom type if s, ok := t.(Settable); ok { // use the interface method return s.WriteAnswer(name, v) } // the target to write to target := reflect.ValueOf(t) // the value to write from value := reflect.ValueOf(v) // make sure we are writing to a pointer if target.Kind() != reflect.Ptr { return errors.New("you must pass a pointer as the target of a Write operation") } // the object "inside" of the target pointer elem := target.Elem() // handle the special types switch elem.Kind() { // if we are writing to a struct case reflect.Struct: // if we are writing to an option answer than we want to treat // it like a single thing and not a place to deposit answers if elem.Type().Name() == "OptionAnswer" { // copy the value over to the normal struct return copy(elem, value) } // get the name of the field that matches the string we were given field, _, err := findField(elem, name) // if something went wrong if err != nil { // bubble up return err } // handle references to the Settable interface aswell if s, ok := field.Interface().(Settable); ok { // use the interface method return s.WriteAnswer(name, v) } if field.CanAddr() { if s, ok := field.Addr().Interface().(Settable); ok { // use the interface method return s.WriteAnswer(name, v) } } // copy the value over to the normal struct return copy(field, value) case reflect.Map: mapType := reflect.TypeOf(t).Elem() if mapType.Key().Kind() != reflect.String { return errors.New("answer maps key must be of type string") } // copy only string value/index value to map if, // map is not of type interface and is 'OptionAnswer' if value.Type().Name() == "OptionAnswer" { if kval := mapType.Elem().Kind(); kval == reflect.String { mt := *t.(*map[string]string) mt[name] = value.FieldByName("Value").String() return nil } else if kval == reflect.Int { mt := *t.(*map[string]int) mt[name] = int(value.FieldByName("Index").Int()) return nil } } if mapType.Elem().Kind() != reflect.Interface { return errors.New("answer maps must be of type map[string]interface") } mt := *t.(*map[string]interface{}) mt[name] = value.Interface() return nil } // otherwise just copy the value to the target return copy(elem, value) } type errFieldNotMatch struct { questionName string } func (err errFieldNotMatch) Error() string { return fmt.Sprintf("could not find field matching %v", err.questionName) } func (err errFieldNotMatch) Is(target error) bool { // implements the dynamic errors.Is interface. if target != nil { if name, ok := IsFieldNotMatch(target); ok { // if have a filled questionName then perform "deeper" comparison. return name == "" || err.questionName == "" || name == err.questionName } } return false } // IsFieldNotMatch reports whether an "err" is caused by a non matching field. // It returns the Question.Name that couldn't be matched with a destination field. // // Usage: // // if err := survey.Ask(qs, &v); err != nil { // if name, ok := core.IsFieldNotMatch(err); ok { // // name is the question name that did not match a field // } // } func IsFieldNotMatch(err error) (string, bool) { if err != nil { if v, ok := err.(errFieldNotMatch); ok { return v.questionName, true } } return "", false } // BUG(AlecAivazis): the current implementation might cause weird conflicts if there are // two fields with same name that only differ by casing. func findField(s reflect.Value, name string) (reflect.Value, reflect.StructField, error) { fields := flattenFields(s) // first look for matching tags so we can overwrite matching field names for _, f := range fields { // the value of the survey tag tag := f.fieldType.Tag.Get(tagName) // if the tag matches the name we are looking for if tag != "" && tag == name { // then we found our index return f.value, f.fieldType, nil } } // then look for matching names for _, f := range fields { // if the name of the field matches what we're looking for if strings.EqualFold(f.fieldType.Name, name) { return f.value, f.fieldType, nil } } // we didn't find the field return reflect.Value{}, reflect.StructField{}, errFieldNotMatch{name} } func flattenFields(s reflect.Value) []reflectField { sType := s.Type() numField := sType.NumField() fields := make([]reflectField, 0, numField) for i := 0; i < numField; i++ { fieldType := sType.Field(i) field := s.Field(i) if field.Kind() == reflect.Struct && fieldType.Anonymous { // field is a promoted structure fields = append(fields, flattenFields(field)...) continue } fields = append(fields, reflectField{field, fieldType}) } return fields } // isList returns true if the element is something we can Len() func isList(v reflect.Value) bool { switch v.Type().Kind() { case reflect.Array, reflect.Slice: return true default: return false } } // Write takes a value and copies it to the target func copy(t reflect.Value, v reflect.Value) (err error) { // if something ends up panicing we need to catch it in a deferred func defer func() { if r := recover(); r != nil { // if we paniced with an error if _, ok := r.(error); ok { // cast the result to an error object err = r.(error) } else if _, ok := r.(string); ok { // otherwise we could have paniced with a string so wrap it in an error err = errors.New(r.(string)) } } }() // if we are copying from a string result to something else if v.Kind() == reflect.String && v.Type() != t.Type() { var castVal interface{} var casterr error vString := v.Interface().(string) switch t.Kind() { case reflect.Bool: castVal, casterr = strconv.ParseBool(vString) case reflect.Int: castVal, casterr = strconv.Atoi(vString) case reflect.Int8: var val64 int64 val64, casterr = strconv.ParseInt(vString, 10, 8) if casterr == nil { castVal = int8(val64) } case reflect.Int16: var val64 int64 val64, casterr = strconv.ParseInt(vString, 10, 16) if casterr == nil { castVal = int16(val64) } case reflect.Int32: var val64 int64 val64, casterr = strconv.ParseInt(vString, 10, 32) if casterr == nil { castVal = int32(val64) } case reflect.Int64: if t.Type() == reflect.TypeOf(time.Duration(0)) { castVal, casterr = time.ParseDuration(vString) } else { castVal, casterr = strconv.ParseInt(vString, 10, 64) } case reflect.Uint: var val64 uint64 val64, casterr = strconv.ParseUint(vString, 10, 8) if casterr == nil { castVal = uint(val64) } case reflect.Uint8: var val64 uint64 val64, casterr = strconv.ParseUint(vString, 10, 8) if casterr == nil { castVal = uint8(val64) } case reflect.Uint16: var val64 uint64 val64, casterr = strconv.ParseUint(vString, 10, 16) if casterr == nil { castVal = uint16(val64) } case reflect.Uint32: var val64 uint64 val64, casterr = strconv.ParseUint(vString, 10, 32) if casterr == nil { castVal = uint32(val64) } case reflect.Uint64: castVal, casterr = strconv.ParseUint(vString, 10, 64) case reflect.Float32: var val64 float64 val64, casterr = strconv.ParseFloat(vString, 32) if casterr == nil { castVal = float32(val64) } case reflect.Float64: castVal, casterr = strconv.ParseFloat(vString, 64) default: //lint:ignore ST1005 allow this error message to be capitalized return fmt.Errorf("Unable to convert from string to type %s", t.Kind()) } if casterr != nil { return casterr } t.Set(reflect.ValueOf(castVal)) return } // if we are copying from an OptionAnswer to something if v.Type().Name() == "OptionAnswer" { // copying an option answer to a string if t.Kind() == reflect.String { // copies the Value field of the struct t.Set(reflect.ValueOf(v.FieldByName("Value").Interface())) return } // copying an option answer to an int if t.Kind() == reflect.Int { // copies the Index field of the struct t.Set(reflect.ValueOf(v.FieldByName("Index").Interface())) return } // copying an OptionAnswer to an OptionAnswer if t.Type().Name() == "OptionAnswer" { t.Set(v) return } // we're copying an option answer to an incorrect type //lint:ignore ST1005 allow this error message to be capitalized return fmt.Errorf("Unable to convert from OptionAnswer to type %s", t.Kind()) } // if we are copying from one slice or array to another if isList(v) && isList(t) { // loop over every item in the desired value for i := 0; i < v.Len(); i++ { // write to the target given its kind switch t.Kind() { // if its a slice case reflect.Slice: // an object of the correct type obj := reflect.Indirect(reflect.New(t.Type().Elem())) // write the appropriate value to the obj and catch any errors if err := copy(obj, v.Index(i)); err != nil { return err } // just append the value to the end t.Set(reflect.Append(t, obj)) // otherwise it could be an array case reflect.Array: // set the index to the appropriate value if err := copy(t.Slice(i, i+1).Index(0), v.Index(i)); err != nil { return err } } } } else { // set the value to the target t.Set(v) } // we're done return } survey-2.3.7/core/write_test.go000066400000000000000000000503301434440262200165320ustar00rootroot00000000000000package core import ( "fmt" "reflect" "testing" "time" "github.com/stretchr/testify/assert" ) func TestWrite_returnsErrorIfTargetNotPtr(t *testing.T) { // try to copy a value to a non-pointer err := WriteAnswer(true, "hello", true) // make sure there was an error if err == nil { t.Error("Did not encounter error when writing to non-pointer.") } } func TestWrite_canWriteToBool(t *testing.T) { // a pointer to hold the boolean value ptr := true // try to copy a false value to the pointer err := WriteAnswer(&ptr, "", false) if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr { // the test failed t.Error("Could not write a false bool to a pointer") } } func TestWrite_canWriteString(t *testing.T) { // a pointer to hold the boolean value ptr := "" // try to copy a false value to the pointer err := WriteAnswer(&ptr, "", "hello") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is not what we wrote if ptr != "hello" { t.Error("Could not write a string value to a pointer") } } func TestWrite_canWriteSlice(t *testing.T) { // a pointer to hold the value ptr := []string{} // copy in a value err := WriteAnswer(&ptr, "", []string{"hello", "world"}) if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // make sure there are two entries assert.Equal(t, []string{"hello", "world"}, ptr) } func TestWrite_canWriteMapString(t *testing.T) { // a map to hold the values ptr := map[string]string{} // copy in a value err := WriteAnswer(&ptr, "test", OptionAnswer{Value: "hello", Index: 5}) if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // make sure only string value is written assert.Equal(t, map[string]string{"test": "hello"}, ptr) } func TestWrite_canWriteMapInt(t *testing.T) { // a map to hold the values ptr := map[string]int{} // copy in a value err := WriteAnswer(&ptr, "test", OptionAnswer{Value: "hello", Index: 5}) if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // make sure only string value is written assert.Equal(t, map[string]int{"test": 5}, ptr) } func TestWrite_recoversInvalidReflection(t *testing.T) { // a variable to mutate ptr := false // write a boolean value to the string err := WriteAnswer(&ptr, "", "hello") // if there was no error if err == nil { // the test failed t.Error("Did not encounter error when forced invalid write.") } } func TestWriteAnswer_handlesNonStructValues(t *testing.T) { // the value to write to ptr := "" // write a value to the pointer err := WriteAnswer(&ptr, "", "world") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if we didn't change the value appropriate if ptr != "world" { // the test failed t.Error("Did not write value to primitive pointer") } } func TestWriteAnswer_canMutateStruct(t *testing.T) { // the struct to hold the answer ptr := struct{ Name string }{} // write a value to an existing field err := WriteAnswer(&ptr, "name", "world") if err != nil { // the test failed t.Errorf("Encountered error while writing answer: %v", err.Error()) // we're done here return } // make sure we changed the field if ptr.Name != "world" { // the test failed t.Error("Did not mutate struct field when writing answer.") } } func TestWriteAnswer_optionAnswer(t *testing.T) { t.Run("writes index for ints", func(t *testing.T) { val := 0 // write a value to an existing field err := WriteAnswer(&val, "", OptionAnswer{ Index: 10, Value: "string value", }) if err != nil { t.Errorf("Encountered error while writing answer: %v", err.Error()) return } if val != 10 { t.Errorf("Wrong value written. Wanted 10, got %v", val) return } }) t.Run("writes OptionAnswer for OptionAnswer", func(t *testing.T) { val := OptionAnswer{} // write a value to an existing field err := WriteAnswer(&val, "", OptionAnswer{ Index: 10, Value: "string value", }) if err != nil { t.Errorf("Encountered error while writing answer: %v", err.Error()) return } if val.Index != 10 || val.Value != "string value" { t.Errorf("Wrong internal values: %v", val) return } }) t.Run("writes value for strings", func(t *testing.T) { val := "" // write a value to an existing field err := WriteAnswer(&val, "", OptionAnswer{ Index: 10, Value: "string value", }) if err != nil { t.Errorf("Encountered error while writing answer: %v", err.Error()) return } if val != "string value" { t.Errorf("Wrong value written. Wanted \"100\", got %v", val) return } }) t.Run("writes slice of indices for slice of ints", func(t *testing.T) { val := []int{} // write a value to an existing field err := WriteAnswer(&val, "", []OptionAnswer{ { Index: 10, Value: "string value", }, }) if err != nil { t.Errorf("Encountered error while writing answer: %v", err.Error()) return } if len(val) != 1 || val[0] != 10 { t.Errorf("Wrong value written. Wanted [10], got %v", val) return } }) t.Run("writes slice of values for slice of strings", func(t *testing.T) { val := []string{} // write a value to an existing field err := WriteAnswer(&val, "", []OptionAnswer{ { Index: 10, Value: "string value", }, }) if err != nil { t.Errorf("Encountered error while writing answer: %v", err.Error()) return } if len(val) != 1 || val[0] != "string value" { t.Errorf("Wrong value written. Wanted [string value], got %v", val) return } }) } func TestWriteAnswer_canMutateMap(t *testing.T) { // the map to hold the answer ptr := make(map[string]interface{}) // write a value to an existing field err := WriteAnswer(&ptr, "name", "world") if err != nil { // the test failed t.Errorf("Encountered error while writing answer: %v", err.Error()) // we're done here return } // make sure we changed the field if ptr["name"] != "world" { // the test failed t.Error("Did not mutate map when writing answer.") } } func TestWrite_returnsErrorIfInvalidMapType(t *testing.T) { // try to copy a value to a non map[string]interface{} ptr := make(map[int]string) err := WriteAnswer(&ptr, "name", "world") // make sure there was an error if err == nil { t.Error("Did not encounter error when writing to invalid map.") } } func TestWrite_writesStringSliceToIntSlice(t *testing.T) { // make a slice of int to write to target := []int{} // write the answer err := WriteAnswer(&target, "name", []string{"1", "2", "3"}) // make sure there was no error assert.Nil(t, err, "WriteSlice to Int Slice") // and we got what we wanted assert.Equal(t, []int{1, 2, 3}, target) } func TestWrite_writesStringArrayToIntArray(t *testing.T) { // make an array of int to write to target := [3]int{} // write the answer err := WriteAnswer(&target, "name", [3]string{"1", "2", "3"}) // make sure there was no error assert.Nil(t, err, "WriteArray to Int Array") // and we got what we wanted assert.Equal(t, [3]int{1, 2, 3}, target) } func TestWriteAnswer_returnsErrWhenFieldNotFound(t *testing.T) { // the struct to hold the answer ptr := struct{ Name string }{} // write a value to an existing field err := WriteAnswer(&ptr, "", "world") if err == nil { // the test failed t.Error("Did not encountered error while writing answer to non-existing field.") } } func TestFindField_canFindExportedField(t *testing.T) { // create a reflective wrapper over the struct to look through val := reflect.ValueOf(struct{ Name string }{Name: "Jack"}) // find the field matching "name" field, fieldType, err := findField(val, "name") // if something went wrong if err != nil { // the test failed t.Error(err.Error()) return } // make sure we got the right value if field.Interface() != "Jack" { // the test failed t.Errorf("Did not find the correct field value. Expected 'Jack' found %v.", field.Interface()) } // make sure we got the right field type if fieldType.Name != "Name" { // the test failed t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", fieldType.Name) } } func TestFindField_canFindTaggedField(t *testing.T) { // the struct to look through val := reflect.ValueOf(struct { Username string `survey:"name"` }{ Username: "Jack", }) // find the field matching "name" field, fieldType, err := findField(val, "name") // if something went wrong if err != nil { // the test failed t.Error(err.Error()) return } // make sure we got the right value if field.Interface() != "Jack" { // the test failed t.Errorf("Did not find the correct field value. Expected 'Jack' found %v.", field.Interface()) } // make sure we got the right fieldType if fieldType.Name != "Username" { // the test failed t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", fieldType.Name) } } func TestFindField_canHandleCapitalAnswerNames(t *testing.T) { // create a reflective wrapper over the struct to look through val := reflect.ValueOf(struct{ Name string }{Name: "Jack"}) // find the field matching "name" field, fieldType, err := findField(val, "Name") // if something went wrong if err != nil { // the test failed t.Error(err.Error()) return } // make sure we got the right value if field.Interface() != "Jack" { // the test failed t.Errorf("Did not find the correct field value. Expected 'Jack' found %v.", field.Interface()) } // make sure we got the right fieldType if fieldType.Name != "Name" { // the test failed t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", fieldType.Name) } } func TestFindField_tagOverwriteFieldName(t *testing.T) { // the struct to look through val := reflect.ValueOf(struct { Name string Username string `survey:"name"` }{ Name: "Ralf", Username: "Jack", }) // find the field matching "name" field, fieldType, err := findField(val, "name") // if something went wrong if err != nil { // the test failed t.Error(err.Error()) return } // make sure we got the right value if field.Interface() != "Jack" { // the test failed t.Errorf("Did not find the correct field value. Expected 'Jack' found %v.", field.Interface()) } // make sure we got the right fieldType if fieldType.Name != "Username" { // the test failed t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", fieldType.Name) } } func TestFindField_supportsPromotedFields(t *testing.T) { // create a reflective wrapper over the struct to look through type Common struct { Name string } type Strct struct { Common // Name field added by composition Username string } val := reflect.ValueOf(Strct{Common: Common{Name: "Jack"}}) // find the field matching "name" field, fieldType, err := findField(val, "Name") // if something went wrong if err != nil { // the test failed t.Error(err.Error()) return } // make sure we got the right value if field.Interface() != "Jack" { // the test failed t.Errorf("Did not find the correct field value. Expected 'Jack' found %v.", field.Interface()) } // make sure we got the right fieldType if fieldType.Name != "Name" { // the test failed t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", fieldType.Name) } } func TestFindField_promotedFieldsWithTag(t *testing.T) { // create a reflective wrapper over the struct to look through type Common struct { Username string `survey:"name"` } type Strct struct { Common // Name field added by composition Name string } val := reflect.ValueOf(Strct{ Common: Common{Username: "Jack"}, Name: "Ralf", }) // find the field matching "name" field, fieldType, err := findField(val, "name") // if something went wrong if err != nil { // the test failed t.Error(err.Error()) return } // make sure we got the right value if field.Interface() != "Jack" { // the test failed t.Errorf("Did not find the correct field value. Expected 'Jack' found %v.", field.Interface()) } // make sure we got the right fieldType if fieldType.Name != "Username" { // the test failed t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", fieldType.Name) } } func TestFindField_promotedFieldsDontHavePriorityOverTags(t *testing.T) { // create a reflective wrapper over the struct to look through type Common struct { Name string } type Strct struct { Common // Name field added by composition Username string `survey:"name"` } val := reflect.ValueOf(Strct{ Common: Common{Name: "Ralf"}, Username: "Jack", }) // find the field matching "name" field, fieldType, err := findField(val, "name") // if something went wrong if err != nil { // the test failed t.Error(err.Error()) return } // make sure we got the right value if field.Interface() != "Jack" { // the test failed t.Errorf("Did not find the correct field value. Expected 'Jack' found %v.", field.Interface()) } // make sure we got the right fieldType if fieldType.Name != "Username" { // the test failed t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", fieldType.Name) } } type testFieldSettable struct { Values map[string]string } type testStringSettable struct { Value string `survey:"string"` } type testTaggedStruct struct { TaggedValue testStringSettable `survey:"tagged"` } type testPtrTaggedStruct struct { TaggedValue *testStringSettable `survey:"tagged"` } func (t *testFieldSettable) WriteAnswer(name string, value interface{}) error { if t.Values == nil { t.Values = map[string]string{} } if v, ok := value.(string); ok { t.Values[name] = v return nil } return fmt.Errorf("Incompatible type %T", value) } func (t *testStringSettable) WriteAnswer(_ string, value interface{}) error { t.Value = value.(string) return nil } func TestWriteWithFieldSettable(t *testing.T) { testSet1 := testFieldSettable{} err := WriteAnswer(&testSet1, "values", "stringVal") assert.Nil(t, err) assert.Equal(t, map[string]string{"values": "stringVal"}, testSet1.Values) testSet2 := testFieldSettable{} err = WriteAnswer(&testSet2, "values", 123) assert.Error(t, fmt.Errorf("Incompatible type int64"), err) assert.Equal(t, map[string]string{}, testSet2.Values) testString1 := testStringSettable{} err = WriteAnswer(&testString1, "", "value1") assert.Nil(t, err) assert.Equal(t, testStringSettable{"value1"}, testString1) testSetStruct := testTaggedStruct{} err = WriteAnswer(&testSetStruct, "tagged", "stringVal1") assert.Nil(t, err) assert.Equal(t, testTaggedStruct{TaggedValue: testStringSettable{"stringVal1"}}, testSetStruct) testPtrSetStruct := testPtrTaggedStruct{&testStringSettable{}} err = WriteAnswer(&testPtrSetStruct, "tagged", "stringVal1") assert.Nil(t, err) assert.Equal(t, testPtrTaggedStruct{TaggedValue: &testStringSettable{"stringVal1"}}, testPtrSetStruct) } // CONVERSION TESTS func TestWrite_canStringToBool(t *testing.T) { // a pointer to hold the boolean value ptr := true // try to copy a false value to the pointer err := WriteAnswer(&ptr, "", "false") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToInt(t *testing.T) { // a pointer to hold the value var ptr int = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToInt8(t *testing.T) { // a pointer to hold the value var ptr int8 = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToInt16(t *testing.T) { // a pointer to hold the value var ptr int16 = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToInt32(t *testing.T) { // a pointer to hold the value var ptr int32 = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToInt64(t *testing.T) { // a pointer to hold the value var ptr int64 = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToUint(t *testing.T) { // a pointer to hold the value var ptr uint = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToUint8(t *testing.T) { // a pointer to hold the value var ptr uint8 = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToUint16(t *testing.T) { // a pointer to hold the value var ptr uint16 = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToUint32(t *testing.T) { // a pointer to hold the value var ptr uint32 = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToUint64(t *testing.T) { // a pointer to hold the value var ptr uint64 = 1 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToFloat32(t *testing.T) { // a pointer to hold the value var ptr float32 = 1.0 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2.5") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2.5 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canStringToFloat64(t *testing.T) { // a pointer to hold the value var ptr float64 = 1.0 // try to copy a value to the pointer err := WriteAnswer(&ptr, "", "2.5") if err != nil { t.Fatalf("WriteAnswer() = %v", err) } // if the value is true if ptr != 2.5 { // the test failed t.Error("Could not convert string to pointer type") } } func TestWrite_canConvertStructFieldTypes(t *testing.T) { // the struct to hold the answer ptr := struct { Name string Age uint Male bool Height float64 Timeout time.Duration }{} // write the values as strings check(t, WriteAnswer(&ptr, "name", "Bob")) check(t, WriteAnswer(&ptr, "age", "22")) check(t, WriteAnswer(&ptr, "male", "true")) check(t, WriteAnswer(&ptr, "height", "6.2")) check(t, WriteAnswer(&ptr, "timeout", "30s")) // make sure we changed the fields if ptr.Name != "Bob" { t.Error("Did not mutate Name when writing answer.") } if ptr.Age != 22 { t.Error("Did not mutate Age when writing answer.") } if !ptr.Male { t.Error("Did not mutate Male when writing answer.") } if ptr.Height != 6.2 { t.Error("Did not mutate Height when writing answer.") } if ptr.Timeout != time.Duration(30*time.Second) { t.Error("Did not mutate Timeout when writing answer.") } } func check(t *testing.T, err error) { if err != nil { t.Fatalf("Encountered error while writing answer: %v", err.Error()) } } survey-2.3.7/editor.go000066400000000000000000000123661434440262200147060ustar00rootroot00000000000000package survey import ( "bytes" "io/ioutil" "os" "os/exec" "runtime" "github.com/AlecAivazis/survey/v2/terminal" shellquote "github.com/kballard/go-shellquote" ) /* Editor launches an instance of the users preferred editor on a temporary file. The editor to use is determined by reading the $VISUAL or $EDITOR environment variables. If neither of those are present, notepad (on Windows) or vim (others) is used. The launch of the editor is triggered by the enter key. Since the response may be long, it will not be echoed as Input does, instead, it print . Response type is a string. message := "" prompt := &survey.Editor{ Message: "What is your commit message?" } survey.AskOne(prompt, &message) */ type Editor struct { Renderer Message string Default string Help string Editor string HideDefault bool AppendDefault bool FileName string } // data available to the templates when processing type EditorTemplateData struct { Editor Answer string ShowAnswer bool ShowHelp bool Config *PromptConfig } // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format var EditorQuestionTemplate = ` {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }} {{color "reset"}} {{- if .ShowAnswer}} {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} {{- else }} {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}} {{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} {{- color "cyan"}}[Enter to launch editor] {{color "reset"}} {{- end}}` var ( bom = []byte{0xef, 0xbb, 0xbf} editor = "vim" ) func init() { if runtime.GOOS == "windows" { editor = "notepad" } if v := os.Getenv("VISUAL"); v != "" { editor = v } else if e := os.Getenv("EDITOR"); e != "" { editor = e } } func (e *Editor) PromptAgain(config *PromptConfig, invalid interface{}, err error) (interface{}, error) { initialValue := invalid.(string) return e.prompt(initialValue, config) } func (e *Editor) Prompt(config *PromptConfig) (interface{}, error) { initialValue := "" if e.Default != "" && e.AppendDefault { initialValue = e.Default } return e.prompt(initialValue, config) } func (e *Editor) prompt(initialValue string, config *PromptConfig) (interface{}, error) { // render the template err := e.Render( EditorQuestionTemplate, EditorTemplateData{ Editor: *e, Config: config, }, ) if err != nil { return "", err } // start reading runes from the standard in rr := e.NewRuneReader() _ = rr.SetTermMode() defer func() { _ = rr.RestoreTermMode() }() cursor := e.NewCursor() cursor.Hide() defer cursor.Show() for { r, _, err := rr.ReadRune() if err != nil { return "", err } if r == '\r' || r == '\n' { break } if r == terminal.KeyInterrupt { return "", terminal.InterruptErr } if r == terminal.KeyEndTransmission { break } if string(r) == config.HelpInput && e.Help != "" { err = e.Render( EditorQuestionTemplate, EditorTemplateData{ Editor: *e, ShowHelp: true, Config: config, }, ) if err != nil { return "", err } } continue } // prepare the temp file pattern := e.FileName if pattern == "" { pattern = "survey*.txt" } f, err := ioutil.TempFile("", pattern) if err != nil { return "", err } defer func() { _ = os.Remove(f.Name()) }() // write utf8 BOM header // The reason why we do this is because notepad.exe on Windows determines the // encoding of an "empty" text file by the locale, for example, GBK in China, // while golang string only handles utf8 well. However, a text file with utf8 // BOM header is not considered "empty" on Windows, and the encoding will then // be determined utf8 by notepad.exe, instead of GBK or other encodings. if _, err := f.Write(bom); err != nil { return "", err } // write initial value if _, err := f.WriteString(initialValue); err != nil { return "", err } // close the fd to prevent the editor unable to save file if err := f.Close(); err != nil { return "", err } // check is input editor exist if e.Editor != "" { editor = e.Editor } stdio := e.Stdio() args, err := shellquote.Split(editor) if err != nil { return "", err } args = append(args, f.Name()) // open the editor cmd := exec.Command(args[0], args[1:]...) cmd.Stdin = stdio.In cmd.Stdout = stdio.Out cmd.Stderr = stdio.Err cursor.Show() if err := cmd.Run(); err != nil { return "", err } // raw is a BOM-unstripped UTF8 byte slice raw, err := ioutil.ReadFile(f.Name()) if err != nil { return "", err } // strip BOM header text := string(bytes.TrimPrefix(raw, bom)) // check length, return default value on empty if len(text) == 0 && !e.AppendDefault { return e.Default, nil } return text, nil } func (e *Editor) Cleanup(config *PromptConfig, val interface{}) error { return e.Render( EditorQuestionTemplate, EditorTemplateData{ Editor: *e, Answer: "", ShowAnswer: true, Config: config, }, ) } survey-2.3.7/editor_test.go000066400000000000000000000153721434440262200157450ustar00rootroot00000000000000package survey import ( "bytes" "fmt" "io" "os" "os/exec" "testing" "time" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" "github.com/stretchr/testify/assert" ) func init() { // disable color output for all prompts to simplify testing core.DisableColor = true } func TestEditorRender(t *testing.T) { tests := []struct { title string prompt Editor data EditorTemplateData expected string }{ { "Test Editor question output without default", Editor{Message: "What is your favorite month:"}, EditorTemplateData{}, fmt.Sprintf("%s What is your favorite month: [Enter to launch editor] ", defaultIcons().Question.Text), }, { "Test Editor question output with default", Editor{Message: "What is your favorite month:", Default: "April"}, EditorTemplateData{}, fmt.Sprintf("%s What is your favorite month: (April) [Enter to launch editor] ", defaultIcons().Question.Text), }, { "Test Editor question output with HideDefault", Editor{Message: "What is your favorite month:", Default: "April", HideDefault: true}, EditorTemplateData{}, fmt.Sprintf("%s What is your favorite month: [Enter to launch editor] ", defaultIcons().Question.Text), }, { "Test Editor answer output", Editor{Message: "What is your favorite month:"}, EditorTemplateData{Answer: "October", ShowAnswer: true}, fmt.Sprintf("%s What is your favorite month: October\n", defaultIcons().Question.Text), }, { "Test Editor question output without default but with help hidden", Editor{Message: "What is your favorite month:", Help: "This is helpful"}, EditorTemplateData{}, fmt.Sprintf("%s What is your favorite month: [%s for help] [Enter to launch editor] ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput)), }, { "Test Editor question output with default and with help hidden", Editor{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"}, EditorTemplateData{}, fmt.Sprintf("%s What is your favorite month: [%s for help] (April) [Enter to launch editor] ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput)), }, { "Test Editor question output without default but with help shown", Editor{Message: "What is your favorite month:", Help: "This is helpful"}, EditorTemplateData{ShowHelp: true}, fmt.Sprintf("%s This is helpful\n%s What is your favorite month: [Enter to launch editor] ", defaultIcons().Help.Text, defaultIcons().Question.Text), }, { "Test Editor question output with default and with help shown", Editor{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"}, EditorTemplateData{ShowHelp: true}, fmt.Sprintf("%s This is helpful\n%s What is your favorite month: (April) [Enter to launch editor] ", defaultIcons().Help.Text, defaultIcons().Question.Text), }, } for _, test := range tests { t.Run(test.title, func(t *testing.T) { r, w, err := os.Pipe() assert.NoError(t, err) test.prompt.WithStdio(terminal.Stdio{Out: w}) test.data.Editor = test.prompt // set the icon set test.data.Config = defaultPromptConfig() err = test.prompt.Render( EditorQuestionTemplate, test.data, ) assert.NoError(t, err) assert.NoError(t, w.Close()) var buf bytes.Buffer _, err = io.Copy(&buf, r) assert.NoError(t, err) assert.Contains(t, buf.String(), test.expected) }) } } func TestEditorPrompt(t *testing.T) { if _, err := exec.LookPath("vi"); err != nil { t.Skip("warning: vi not found in PATH") } tests := []PromptTest{ { "Test Editor prompt interaction", &Editor{ Editor: "vi", Message: "Edit git commit message", }, func(c expectConsole) { c.ExpectString("Edit git commit message [Enter to launch editor]") c.SendLine("") time.Sleep(time.Millisecond) c.Send("ccAdd editor prompt tests\x1b") c.SendLine(":wq!") c.ExpectEOF() }, "Add editor prompt tests\n", }, { "Test Editor prompt interaction with default", &Editor{ Editor: "vi", Message: "Edit git commit message", Default: "No comment", }, func(c expectConsole) { c.ExpectString("Edit git commit message (No comment) [Enter to launch editor]") c.SendLine("") time.Sleep(time.Millisecond) c.SendLine(":q!") c.ExpectEOF() }, "No comment", }, { "Test Editor prompt interaction overriding default", &Editor{ Editor: "vi", Message: "Edit git commit message", Default: "No comment", }, func(c expectConsole) { c.ExpectString("Edit git commit message (No comment) [Enter to launch editor]") c.SendLine("") time.Sleep(time.Millisecond) c.Send("ccAdd editor prompt tests\x1b") c.SendLine(":wq!") c.ExpectEOF() }, "Add editor prompt tests\n", }, { "Test Editor prompt interaction hiding default", &Editor{ Editor: "vi", Message: "Edit git commit message", Default: "No comment", HideDefault: true, }, func(c expectConsole) { c.ExpectString("Edit git commit message [Enter to launch editor]") c.SendLine("") time.Sleep(time.Millisecond) c.SendLine(":q!") c.ExpectEOF() }, "No comment", }, { "Test Editor prompt interaction and prompt for help", &Editor{ Editor: "vi", Message: "Edit git commit message", Help: "Describe your git commit", }, func(c expectConsole) { c.ExpectString( fmt.Sprintf( "Edit git commit message [%s for help] [Enter to launch editor]", string(defaultPromptConfig().HelpInput), ), ) c.SendLine("?") c.ExpectString("Describe your git commit") c.SendLine("") time.Sleep(time.Millisecond) c.Send("ccAdd editor prompt tests\x1b") c.SendLine(":wq!") c.ExpectEOF() }, "Add editor prompt tests\n", }, { "Test Editor prompt interaction with default and append default", &Editor{ Editor: "vi", Message: "Edit git commit message", Default: "No comment", AppendDefault: true, }, func(c expectConsole) { c.ExpectString("Edit git commit message (No comment) [Enter to launch editor]") c.SendLine("") c.ExpectString("No comment") c.SendLine("dd") c.SendLine(":wq!") c.ExpectEOF() }, "", }, { "Test Editor prompt interaction with editor args", &Editor{ Editor: "vi --", Message: "Edit git commit message", }, func(c expectConsole) { c.ExpectString("Edit git commit message [Enter to launch editor]") c.SendLine("") time.Sleep(time.Millisecond) c.Send("ccAdd editor prompt tests\x1b") c.SendLine(":wq!") c.ExpectEOF() }, "Add editor prompt tests\n", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { RunPromptTest(t, test) }) } } survey-2.3.7/examples/000077500000000000000000000000001434440262200146775ustar00rootroot00000000000000survey-2.3.7/examples/countrylist.go000066400000000000000000000120701434440262200176250ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var countryQs = []*survey.Question{ { Name: "country", Prompt: &survey.Select{ Message: "Choose a country:", Options: []string{ "Afghanistan", "Åland Islands", "Albania", "Algeria", "American Samoa", "AndorrA", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", "Congo, The Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote D'Ivoire", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard Island and Mcdonald Islands", "Holy See (Vatican City State)", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic Of", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Democratic People'S Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", "Lao People'S Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic of", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", "RWANDA", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia and Montenegro", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", "Yemen", "Zambia", "Zimbabwe", }, }, Validate: survey.Required, }, } func main() { answers := struct { Country string }{} // ask the question err := survey.Ask(countryQs, &answers) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("you chose %s.\n", answers.Country) } survey-2.3.7/examples/cursor.go000066400000000000000000000010351434440262200165420ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var simpleQs = []*survey.Question{ { Name: "name", Prompt: &survey.Input{ Message: "What is your name?", }, Validate: survey.Required, }, } func main() { ansmap := make(map[string]interface{}) // ask the question err := survey.Ask(simpleQs, &ansmap, survey.WithShowCursor(true)) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("Your name is %s.\n", ansmap["name"]) } survey-2.3.7/examples/inputfilesuggestion.go000066400000000000000000000013031434440262200213320ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "path/filepath" "github.com/AlecAivazis/survey/v2" ) func suggestFiles(toComplete string) []string { files, _ := filepath.Glob(toComplete + "*") return files } // the questions to ask var q = []*survey.Question{ { Name: "file", Prompt: &survey.Input{ Message: "Which file should be read?", Suggest: suggestFiles, Help: "Any file; do not need to exist yet", }, Validate: survey.Required, }, } func main() { answers := struct { File string }{} // ask the question err := survey.Ask(q, &answers) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("File chosen %s.\n", answers.File) } survey-2.3.7/examples/longlist.go000066400000000000000000000011671434440262200170660ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var simpleQs = []*survey.Question{ { Name: "letter", Prompt: &survey.Select{ Message: "Choose a letter:", Options: []string{ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", }, }, Validate: survey.Required, }, } func main() { answers := struct { Letter string }{} // ask the question err := survey.Ask(simpleQs, &answers) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("you chose %s.\n", answers.Letter) } survey-2.3.7/examples/longmulti.go000066400000000000000000000120561434440262200172440ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "strings" survey "github.com/AlecAivazis/survey/v2" ) // the questions to ask var multiQs = []*survey.Question{ { Name: "letter", Prompt: &survey.MultiSelect{ Message: "Choose one or more words :", Options: []string{ "Afghanistan", "Åland Islands", "Albania", "Algeria", "American Samoa", "AndorrA", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", "Congo, The Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote D'Ivoire", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard Island and Mcdonald Islands", "Holy See (Vatican City State)", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic Of", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Democratic People'S Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", "Lao People'S Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic of", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", "RWANDA", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia and Montenegro", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", "Yemen", "Zambia", "Zimbabwe", }, }, }, } func main() { answers := []string{} // ask the question err := survey.Ask(multiQs, &answers) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("you chose: %s\n", strings.Join(answers, ", ")) } survey-2.3.7/examples/longmultikeepfilter.go000066400000000000000000000121131434440262200213110ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "strings" survey "github.com/AlecAivazis/survey/v2" ) // the questions to ask var multiQs = []*survey.Question{ { Name: "letter", Prompt: &survey.MultiSelect{ Message: "Choose one or more words :", Options: []string{ "Afghanistan", "Åland Islands", "Albania", "Algeria", "American Samoa", "AndorrA", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", "Congo, The Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote D'Ivoire", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guernsey", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard Island and Mcdonald Islands", "Holy See (Vatican City State)", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic Of", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy", "Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Democratic People'S Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", "Lao People'S Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", "Liechtenstein", "Lithuania", "Luxembourg", "Macao", "Macedonia, The Former Yugoslav Republic of", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Palestinian Territory, Occupied", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", "RWANDA", "Saint Helena", "Saint Kitts and Nevis", "Saint Lucia", "Saint Pierre and Miquelon", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia and Montenegro", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Timor-Leste", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", "Yemen", "Zambia", "Zimbabwe", }, }, }, } func main() { answers := []string{} // ask the question err := survey.Ask(multiQs, &answers, survey.WithKeepFilter(true)) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("you chose: %s\n", strings.Join(answers, ", ")) } survey-2.3.7/examples/map.go000066400000000000000000000012561434440262200160070ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var simpleQs = []*survey.Question{ { Name: "name", Prompt: &survey.Input{ Message: "What is your name?", }, Validate: survey.Required, }, { Name: "color", Prompt: &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, }, Validate: survey.Required, }, } func main() { ansmap := make(map[string]interface{}) // ask the question err := survey.Ask(simpleQs, &ansmap) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("%s chose %s.\n", ansmap["name"], ansmap["color"]) } survey-2.3.7/examples/password.go000066400000000000000000000014661434440262200170770ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var defaultPasswordCharacterPrompt = &survey.Password{ Message: "What is your password? (Default hide character)", } var customPasswordCharacterPrompt = &survey.Password{ Message: "What is your password? (Custom hide character)", } func main() { var defaultPass string var customPass string // ask the question err := survey.AskOne(defaultPasswordCharacterPrompt, &defaultPass) if err != nil { fmt.Println(err.Error()) return } fmt.Println() err = survey.AskOne(customPasswordCharacterPrompt, &customPass, survey.WithHideCharacter('-')) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("Password 1: %s.\n Password 2: %s\n", defaultPass, customPass) } survey-2.3.7/examples/select_description.go000066400000000000000000000015441434440262200211140ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) type Meal struct { Title string Comment string } func main() { var meals = []Meal{ {Title: "Bread", Comment: "Contains gluten"}, {Title: "Eggs", Comment: "Free-range"}, {Title: "Apple", Comment: ""}, {Title: "Burger", Comment: "Veggie patties available"}, } titles := make([]string, len(meals)) for i, m := range meals { titles[i] = m.Title } var qs = &survey.Select{ Message: "Choose a meal:", Options: titles, Description: func(value string, index int) string { return meals[index].Comment }, } answerIndex := 0 // ask the question err := survey.AskOne(qs, &answerIndex) if err != nil { fmt.Println(err.Error()) return } meal := meals[answerIndex] // print the answers fmt.Printf("you picked %s, nice choice.\n", meal.Title) } survey-2.3.7/examples/simple.go000066400000000000000000000013271434440262200165220ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var simpleQs = []*survey.Question{ { Name: "name", Prompt: &survey.Input{ Message: "What is your name?", }, Validate: survey.Required, Transform: survey.Title, }, { Name: "color", Prompt: &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, }, Validate: survey.Required, }, } func main() { answers := struct { Name string Color string }{} // ask the question err := survey.Ask(simpleQs, &answers) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("%s chose %s.\n", answers.Name, answers.Color) } survey-2.3.7/examples/validation.go000066400000000000000000000014401434440262200173570ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var validationQs = []*survey.Question{ { Name: "name", Prompt: &survey.Input{Message: "What is your name?"}, Validate: survey.Required, }, { Name: "valid", Prompt: &survey.Input{Message: "Enter 'foo':", Default: "not foo"}, Validate: func(val interface{}) error { // if the input matches the expectation if str := val.(string); str != "foo" { return fmt.Errorf("You entered %s, not 'foo'.", str) } // nothing was wrong return nil }, }, } func main() { // the place to hold the answers answers := struct { Name string Valid string }{} err := survey.Ask(validationQs, &answers) if err != nil { fmt.Println("\n", err.Error()) } } survey-2.3.7/filter.go000066400000000000000000000000171434440262200146730ustar00rootroot00000000000000package survey survey-2.3.7/go.mod000066400000000000000000000011231434440262200141640ustar00rootroot00000000000000module github.com/AlecAivazis/survey/v2 require ( github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/creack/pty v1.1.17 github.com/davecgh/go-spew v1.1.1 // indirect github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-isatty v0.0.8 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b github.com/stretchr/testify v1.6.1 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/text v0.4.0 ) go 1.13 survey-2.3.7/go.sum000066400000000000000000000123551434440262200142220ustar00rootroot00000000000000github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= survey-2.3.7/img/000077500000000000000000000000001434440262200136355ustar00rootroot00000000000000survey-2.3.7/img/multi-select-all-none.gif000066400000000000000000002102421434440262200204370ustar00rootroot00000000000000GIF89a1 "%1 0 ?B$J2$D%S*]4^?r, ?: &97OCvEbGgPW~!Q*,,,5i8=Qqbzcet$Z: <A-sE(R%W5aEcNlQrxr,d-D]^-g6l<vK|oh 7`.b1q@qBrQuPvKQ`o~~svU B 9p/$.E"2'QDbG~5]^_ M^Ti w%!'%\((-5)+ ,`, -.A/4R0C`34124Qp55X67[V7n88;8Uz9 ;;(>,.> A_vBBbCCzD?NI#I;@J KK:8K{LLMQQRmT V[.\]A%_(`amaO:a1dwde}ef;hU`kC*kI9kk#m5pr^s}xwsx3yyzvXz{z|E}~|u|Ld@S!jQyX/Uzbf5s^(zSck3vw?zD~FLTgo[\pzk|! NETSCAPE2.0! ,HA*\0#t(ŋ3j.lIɓ(S\ɲe( *Uϔ͕r{eԩU @9{dH c]˶mIPᶢb.,J]pxnƒ9FQ #K`q}X̲gQ(uZ0Kh"CJϔ~1\0?ZO>yl!gG-2xBD2 @ Sgw.x@ߪg zU4`)x0Dix MHyU &$}e~Eu\sLi@ ,x ?dsmJB" QWl >ӜBjN7f+ǒM O9h(9 N{la9lXAԊKd̎F$o>Xc/h >;- tM$> @oC L- r)jt3PFuXp K8   a@1" FDpz:s D=3$,+qECEt{OD0C]## '.,3 IC *0@eieyCMq& 7%-Kƾ5$|ydG;j \f2%2] `(R`ȅ<ь}+pq<p6PfE} @ A٠&  }026&>I*d#8HE.zTlt# ~X{ # % C> r>(IJZ Xd#HJ"Ǣ4%X)"C R( 1D+Al@ _AdLd*A"hB <+FgLKiҖ Ӭ&РJH Nj.sZ蒗,k7|! `S:P @ 'r\ YsF3rB@yˋ׳?1*NTN$9#8NL2`vP೭ه jua@( U9' k?zޅiNsЄBͬ,FeQiu,i͚z -r,/yLَnqV9?z1dLPC/A~8% 4@FhMo35d5ȅ=hzd҈*bk } Qnu3!^P׼W cdL؇6 }Vx@6 ) >Q [8dS^A2dm+v.p)w10٥Bs p@a/*@1xZlʁ'L$Ai@EӴ=`M֩.+nq LGb?>UAc Y,yKM2=;$*󘽚9,qȝIJ`fSsb(E2&Sm,(jbRYaN2'7τ{ȣ#=a2z`Ռ>uTj,:OB7d3{ ]њmX ]z rG 'GVry7 1"Qtp P J0Qt@v_nh pH1 6gء0-LN}[ eHAb8wd00#P7@%Ј4Pwr4ege7x7 \"h @mrȉ0Cx8i8S8)7ݸو@M$"/0Ho\03MgaLMp8H^7hAWk1APz옎n!)i' ӷx(:鍚(XFgHPg# A&ki荢x!4Pypg8_",\,thlZGqЕ?(/ ;0l- d fI 2Iu^: mrI}y9H  Cq) Ay(a7٘IKQudb{!.M¸j+Tch k=vKaGP>cɌld9kxW̹g S""2Ij"1d٤m@J 7bj˹i)yY*2IFEiz[mviAQ!\pcjYͩ9HQ؟#:~rÑlJm> : d+8՝ʢy)9;:zCi@k":J)Q@ ǡ_E°s UE$` M 0 NTE!d^h]Dd`pHVxmFmp$vz0zpHsen 0 ! D 򩡊 Jʩ06LXJ%M TQc< ApNsIUd Wzq}UMq_ x oZIQ8N 6ie !`K9:zA~ujxǚɊY,jJ~QzŠ3UT;j *#3Y_!K])pƠ2*Ek":۰+JKJvFGq  Ȱ<$+T8 㺒۱&  fxWaA j f; 0)L;Aź Ŭ'Dp @sUѰ 10 ,kt, sQd- B(G h!+r a5 0I B10t b0k YA8)k37d7s4<@;# 4\'E_3@ 2qjdP4Npe`PA{[5s'W\xL6!{Nf53,™3%<A+8Ü"B(̶6KgD:x7pqя+y96dajE3uBMzy6Yy\lPl~;pn,l5h$ÀU,QfJ1|C<}a8NlK%@PYW!OvujP%Üu ;h0!b YL s!"b,&u^qx ޙ1?xu p`,.0~о ` ާ",)^  p@``Xt^Y\^`4i Щ31Ѣ⧲/ P+*  #0ya釗5 >j@-r^'RGԲ.LtRd߰!,~븞&@閐1:n F DXĘ~j ր-z(@ mʘ̮ B >&eM$>E ~.@I'E~梨+@F *,3>l A췱0 0 ͐B t R\C/`` VX{Û _XwӅ0+ EOE> {0(0 x"^ĘQF=~RH%MDRJ-]SL5m4+%(ě5a a %l@@O +rtѫ2spـW\uśW^}sOMH\QvzռB(Efnl9SۿmƝ[n޽}1pϟ>wIn!V <^I<dҵ+uq'I@(vq_|_pygptrLF4ajoJ;psY=!СEaʯDOD1EWd4"̸`{G]@ T%B*Q!M`C|F(ZJ+2K-M|1ĝBTyGPցλ8`&8ЅAX .B}KKFނ/8c7M_|;8dOF9AÓ"U9fgfo9gwg>YhtIݢAK( :jz^8"خҫ:l&{^׍&ra]KIBIʦn&ֈ(7HezH!EGoggot㌭KGAH:l9g꫰*묶zqg=# `KBv{|!h 4H3 5X5d;v~ }{7ӅyCZtg;AwKo[A§w>|bphF<1 <B=0!yD""D8B'`F x@p/E)$&9 JR*a8DۜP ) G'I,2MG PiNu*+Fы_c\}ᔧ?LF eb2}":)&CT2TŪ0ҐTxȈ6>mNXHdEYri5j] "UJVjd!:.wu-WP/E^ /VӘ>ưl8̖,vLjVO|12kvӛ߬ \=pӜDg:չNvӝ!ь&K(mLXOxӟ0K2 (&_[6ԡ;9QXƍ  a M%}hHE*.ER4#: pbu:)ZԦ*3#2va09ltsZW"X+@':ҙ[&]_rjе B(2hA!xS$df3!OySt޳մ5MX?s" ^ Ai4*q} )Y4$i @:D[:̺Ѹ.Ү:?DDa˗9 ; R$J34#C Cc<.>SũA|ɼ<B?_+*@2AJZTFǑ:d6ˆQS& %63%TZp33hDxEw|ICǺ|4 DG$Ȃ4ȃDȄl%K>s{ @ѧ$??y,Y@ۈɵxI)H L JF룃6̙\ xD[Wxة9S04AI(iʟ?|+*2 x3;|" !$48 IA ȅsi*0\ @!88{ªž I)K·t C1sÜ۹C}ʉL{7wغdʦ󻨣E D\MkMlF[zY(DyЄ:0LL3! E2;Ԙ\A hhP "Ka"LdCpE Kx@%>#se%>$zR޸G̞RrG}(ɘ\:E Lҧ:B5?;C@%B5CEDUEeFuGHIJKLMNOPQ%R5SETUUeVuWXYU ! ,-! ,-! ,-! ,-! ,-! ,-! ,z-*\ȰÇ#JHŋ3j\HƏ CIɈ=\ɲ˗0Q,͛8s6Tϟ@3 JѠ3 ]ʴʤ4JJbʪXjMsׯ`ÊKٳhӪ]˶۷pʝKݻx B ӈ%+ ‰]LYj AS3S-yMT@̀ڐ^3{4fMF̮=ht2ē+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8< $PAJTQiI*9W9)TR^UK^Tnɥ^fYd_I&DZPj䑙m"d 1IBP! ,-! ,-! ,-! ,' r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-H*\ȰÇ#JH"p% CIɓ(SlB:0@+sɳϜ-,9fĠB (?JJa:ٛg@ˡE\Zٳh-] 33ʮEx 1cqEWs-}W` #<<lBlS ]{d5ɘ!R4Mdw8s3uQ M&(Lat# @ 3+ dTԝC;y%2s|o=!vڹ{!ԟw%%A`C@xWxhEXЃYHqE*`0gԀPqء Tt0XUsN妙fAp30@qaCAQPK@4 @w7:PXA9 {e1W _UyeavbI_POv$`ıbRoPiA-,4%@7 $jƜd_p]V۝P,XF8uQ| fke[oU,,-˅ M1@ Yp{,N@^-zBM0PMM n_  fD1/F%?cd"_[,w*qPA~V1`@ exD{aQ'\".s smv\F)|hA6}430=`og-؀@˛@/{<5H'+1+1Ě qJ%T[A]kA5u-L6|CЃ1c i\"QC !wA +PBA_F@$D ׄv8+5hr5EPB `` rru9諁ط|ρarkb(`a `ҷ!e) Bz7$p )4 RЂ1F PMw`PP-y BZtK 0A0)$bٝ0yAC T'FB(`:h"!iFE6I;@-@P]vc أD4A& ZXBEi ~ڒ*&@)F8~B&-UƩ* `f `8QW(o#Oao qphFXժJ֦.(cXSuu1olT @!H{ӆ{%%%>XT}'Z'ؕjt.)'c(%""@+>0@އ@p[~ PBq_U lTWHnf3F<Ձ(rZ/6WT\EWr~sv[pI5zwPQ!\]Ou&8Yf~xGx7H%;؃|,17$(LPX}JXB[th*ǕXkw8WЀE\"$H 8*|W"' #w!|h$*$1M 0Hb< ArU@UA` PK=UX2A5 e_zw dHfj ARHB\2f-sWV X0; @)4$A+Ŏ+xd3^ŕʚk1ﵨ [z! %P 3JWzi9iKL*Sö!D' aq5o)P1[zF  Y6زd; ;N[[R,g+9a cKbKqX{xvDZRKΓje[v{'ѴmxH}#z"ꫂ۸;[{n0 ;| ۺB@ԳЀyۻR>Z `;+Pi A Pp n;+؛i`2 o ` +p Z m` U~AXZF8>P=SWlnL*[Ɲɢ~PƆ|Ȉ*h@ǗA?50!Țɜ Y~)\ʦ|ʨʪʬʮʰ˲<˴\˶|˸˺˼˾qC |l7cЬLŬP{ќbK!<|,Ο @vPD dl\ Yr=}΍Qx =Ѱ,LѤkf"!&}ғ <,](0m*iY1:].=j ϊpYF} #ӌNԊь*P]VMM}Z\^`b=ԏcjJ=[krMY{4]֓ss3miIzׄmm/c7[8` FjW-)qޭM >\MCLm$=ny%ڊ4 8']㥭9䚼<>|B0L ]㍝TN^^2[d_~{F:4mhk8mOy^i".~S^w߆>t.p^.#^>^naܝ~j$n]뾮jnn^I>~1L[6Оү.on ,ѾI=,=^쮍܁nΙ^#=n+^E{髎z&>%*̣>? !!~!(_^ύ){0Q=z8ȡXH#BPK?Qqu-5MXU1F_b?Al̾jyr=fm/ 4Mפ&LN_$$/2ӦX4HY~/^:һŎoon\_jzyRU_?ǟTI,?@@  HB >Q'^ĘQF=~RH%MDRJ-]Sfˊ3mN=}TPEETɜK>UTU^z0 ! ,-H@ K  JHŋ3jȱǏ C2!:0I. I͛8sI$*Yӂ)^li'"I.0IիXA: 33bj 1R-($XN5lGt$3@eИ aD-,xh[{w+P#65yM쨡 >qK~&aD t J!$p}.$ P  Km\pqA6_8WDtj+.4 @ "&Rjc@6C 0ehʩLNZxzFڬr)ʨFELP֢غz5ohH J*xD )BPDma'\(ܫ,@a-&%̫zSk%-Ġ |i0\) ټpⰳO(ʜ2l_oSIսVd-J{2YSL1d4l;t:)8@dӏ5 E zahYx"B:5t A +PFA_Fz"4QA\AL\ _:l=p #4B 6/<>>gMP)`?Ykf _*94]n(`a X:(Գ Hb8?MO4`%\$'Rv$ wpcc,9A v4]~X ; fp- v cn`M`cϑbNǗ98 TA=XC &151JyH0`MlnyɛXR3 SB¹(PMbӜ};1AINszg;K` cR 3QΉ&`f H B۩(Kig3bA (;o23b]ҘIZdBz-i$O=&!P  ,ַCJ*ы8tDo1Y( ߥ(՟e!reQ2!.[ΐ.O7 /XE /x $Ldȁ% Ry/+r,'nࣿ5"x01h6Py&faXnVT$x*Fpp=a ɱc7o:~I%|G( i[fFeSA('P we(+^---",d@" 8+t2"~b#lQrPOV(jN؁40s9e[p9_{i0YA n@ծr `'|)<׺յT<%ctY.@Nr !,'<ڙ"%~ !$.g WkO_As: y$W3@ӴD%.>tCW8f@  pA4M*d's?v}W~U}ujgg||wWA:L0W1BT~e`tTjzW{BY'xTdMv iHӦ瀶/k9QA9 QuwQ/;H4+6[0Hg`D,P&e3I4?m^c_` 1f+eg r0|(cG;@<\kd!AThhH5&H`H`xr FV8Te#8o1|_`IaHگ;۶yχ_>@h!m,'@YhLg  k4@AB$![1 @pfG5&JWxp\WDC3֜_2(އ"%N˜Y8LD*ip)Td19~bz)rYn]%ϝ̬Th.faD0 J@!$ Șq})$ P  Kp1]pqA&_8XD4j*.4 @ 2@>gQj@FC 0eXihM: IzJjι+BP蘹`Te 10X)~V14@ e@9^pPXppźd0{Q+FmrW-G/KV4853C+Eǃ%clZsmخ+`Z˒1*-Fӊ6UӋ5QG'-0L+EGi>!#4OsIi"B:5t1@5Bg J|BD qESm"l5|ދ0B(t+ E( FPϻ0]6zWT"<}C0:^XX*ҙu' zJ,0RC,x@L Q- Ð̮v4P`&\@A,萁+A.004Y! 2"2pz$¿-O#=SKTXi`tn9TaȘ36P|bC`p$g44&3kܨUbAȅ?}M$xVA 7)Ј QB. 1DYlP^+-F065~0ZfB*+ IK[Z1IQdej/cRj1#`@1R|~PMBc!}8{v)8l@f:9ZF@y4D!(' TD v 03! Tb- :YgE>JgTeM Pn󛳊>0,j IU/JopmXԡF<ݦBy{DS(1V !}SZSWA pWE]k3vzajQd Dc!(wBI *M6 & `+33hV`x&!P  ,ER5$K" *s؍v5d*>6nhd"Xlzi Nu[\9ƽ ~+`2dB Y9:iBUh2KK @/pHuE!` WNh,\ o %$gći~e_ S͊Q3f ݤس1)fP[)Ya) B;2X -CpD5xk%&0R,eYgDQO|`-8@WTJ ea 0CtU%76~r> t01Ӟw Ӛt= Xԧ& Fh:Խ1,]{0E8y.3UINnQuG[ BA\#dvM8W {77 Pc=)AlN{m.K\ƾfH/>+g~-Ph-1I ԭ@|r{Br@O6O.rӄѤNضh \0(a#.m`Kp8+ЄVA0f!F(IHG;;[Q~!A!T{ȄNhkr1c;IX} fTA}HrW(8f(Q gSU?X(@)'OҡSj(E8cC̸uxRpQ녉a+r+A(RIh=}Z؆f|Ȏo7maOJh8u-,>hj(J鄻xap'@ :8).8 \r-0`.#d +B&d#SB5ZW;ٓPs>0Oo8y4S `AI7 o<(E;6 DFf5Pr[(^Py3Zy7DAe@>ٕ!xǸ)qM@aag07pap٧$+Q1#IyƱusY('0~ə}|c989Vșɘb٧vmq9oٕJ}ީyz3].g q)K he-RZad!!4$[{ AO1$`,%%Z"ebwDm'D3磽G xG/ ӣZr"KFd::HT-1!h#!>GXzeZZ 1Vz PXA&zJjNZ#lJmj:+hSDrz0 g1&Pq)P0QdrD'@Aqj>;P fڪ*Z!⩤*4Rz:SZ6iJs!ګ1y:5z}EqAڬIڭL!ڮ9"z) 芯 ! ,-! ,-! ,-! ,-! ,-H*\ȰÇ#J(1 .)\#=RIɓ(S\a( PB!?&ٲϟ@ -2L9 (ŖCJJdΎ$BH_K6 (PHTb&,öyY{;Ѯ5 6pQ88>l t y@ Xp2l (PձCi 7%` W~#ImiN;>CąNб):VDLcG/N(}$! G #}| t%] 7>Wc&D"#f$;jRa(ơW2.ueQ{A8`aO8YiyP-/`DsP/J9n "qLhĤl\X=$|7O}cD2qsn;K ,pX*b$3a:]whĊAR\I, IX *%S`fAMiRsO -ڹ)aM?72v6.`eMBrt<'ʘLt,L*㔗9)BTz<(%UQYcQ}jՖuov{c@qW cW@w:n5,u>-hխˁ\oˊ#|HIOSlcܠDҖqk:l(8 2CTh{#.hL@&l / z*)aS!YN2ɽ&B6cS&w ~k)֤eSsj#E.KUd[D`ȇ'<pu꽡-,ԕ ҵqJk_7XR`*6-F#C ;zOaa.qhd@" Weľ.TB^P@vaEBrQ!6@P\䖡 dX`z%={l@! l;ZcB&ۚZFxѣg6XA 8a@uo t4K)<` jpY ֳhe߷DU= 7`f[Zl:io} laGskРU{z^n[RiRIop])vw.2-C\T/kVx[Sl+Hb!B:q%=<"r|(wZAy>=O"L]x]oIݎ3%MixiΦN'YI=S^8mkfY4p-չM 2lM";MGQKLUo6N۟2"-噪X"qoz=T_UqUEO>Ǥt*5|{+zW %7. (BNb"P\Ar['SXU{oOF&'$w17VF}/6iKG$TXۧwearЂu=t|'vf.p;M7zq} }/{QwANA}!l!j16>wx`!xM Eb`,Cb7P9K`cf T)ٙYE BX!Q pg)wXX*HЉwaEhjɖOBp`ə"<'ciلA9yGy8Z787f(1x zP3 z:2)xᄮӝȟ٢١y)q9$[Tybh!d1(fWeu}Wa7:OZ!eeU SG^(Q!EDK]j>GuQ]!)sצ"D!d| Rbvk*j8aaR!GV"|y|fy Aq*$P΁oʠ$(g(x1lU$w!FaJ_n RKj%`'͚egqXZxPwV&;!!$A0lq* a`m&$b±HLʭk&SA6Xʯ*JgP)9P5eA6 2G N qz2;4K"[>KR`%±9HJL۴NPR;T[V{X@qZ۵^`b.;f{hj-@ Zl{xzZkp |{2 h.Q  P ` <˸[{+  {T[ Pۻ p рk K[{1n ;[{[Ak@ ̛[{*$B@ |I0 0 ͐ @@ L  0 Q4 ,2<4\q#_z6>@BNp\rL\?LM< M\NmQ P@ ]p_^a2ȹ;|m` P P ps<̕Pǜ˜Μq>K>Q P0@٤}@ H <|, 攞n F镰0B]  ҂#]'ҡҺ~- ֽDmϮHLSNש< upu `ғ=mq=u}"޾,k:QX#݁ܛ]ACN }o(>_^ ;k(./47c,<?ōQ/I'_Q\qP\oW?b^P0?ӗp] N¼BGUdO❛WW)dM3dAF6fDTRJ-]SL5męSN=}TPEETRMuM@z{&i RD٥RH$Y|7)2i.w.^,Hv)daBL"F:D$QB_%8©%?UE^װE36&SKL2 uCV͂- M4 @–J(+SX ;wCqK H*7fx[]^ _[TK!ߵ d4Q{E>- Oc/^WNCpM + BbW5䒈'Jъ"ZhF rYu] jXCFc$5+iYDIoI_ "vXUu;Ynhoh|NRRE(1* C^3ig'$|f4?uVɭnk\(׹ hR:>wLbHTv HVQa9%<)!~`g: ̊LM+ڲUZEiJJKfwۥ!AYzQ{ԧqپnvoRh^pTwUԋx>p.ڹe|=r5xnR.ciz+qwemܑt@yَ Dc{Bx!,;~"8q˅m%eeE4J4r(z[t8G,`ڛu,[G)|#Co>]0 Ya/q:N+( oQ<#EYg|&*L :Flz4GQS2>?X?}[zHd `F䡉:,7|_+7 ˜20ew߰ķDdB#Js졯2ʰ@[g=W5Wht0 MOQєeH&&N&P ˿"F@rQxa+6|zl8'y .'!63d,*R8(1=y#K 7>*)S9$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-H*\ȰÇ#JH"%p% CIɓ(S,Œt@1`ɳϟ@I0Q&M (D[JՉCIrAկ`R*Q(gf${fA7tMXƕ-EhZ}Pi DQ b#TJ4^ ; WJ`+HɨSJ[h  ^}a \p\=p#1A Of@ \x cNܺ:{gt`Ia| U@F@~SwY}FEDE܇ HelH6Co.QT$vXcR&TDW8>$ VK8 u]1^LTȐ5A?WёE &b[Z+NYnUE:41NDd]f)'yg& 8Q#Ȑ6#dVE&q.@ K8  a@BJ<\/@aD7Dǫ Ku1pR j[8qZD<֦EB=D : Hة @x= Z qXc9$"gtk!,kZKzџqpf_YkjP`@MĀ.C9ԅ^pP$ P'A'sk0؆<Пiiz˟z}ȳbn!t'uS ѻ1Y8wj3'p,VgWkKlsKf[k桢nRlbM. 5@0 ZJb5B:p J|BCX}'B5x%+Aw% %R<&} 4(R#(}\`.Xf^1/r pw@ +, ROWiY yBIcKX ą0aLx) C|@ X0: (mCi8AEd:ؾP*ܞƦЀO $zMax%c2  -̬R % aG&:-c',Љa` -ȝS&n`1J<'kݡdnJ؇4 <&PC6 ׄ-'D]b `9+P098$tBzD+[ &O-5F~H@1 QLyS)O@ ,ρGJPCP3€E@ !Mщn;PGy~Z9BF* `f (ԣQA(to#Q l`Cp'],(r;Uui 4:)\Ofٖ5s T=9(^Cc)\rTP]~0Я)eJFZ* oڄ!},U[f3}Bj=" *#π"Ԯ "$~>졌L Ȑ/>)&Tin, X+' ʠ-3/h*ЏD&M C6+=yВRCh"J$x ,U儷, #V4@R Rrn ƉdAᜰ;zl `ohس` |me#PL$eJsW4S|jzT]:q3G.f< (~.p)Ŧ œ!$e(cu_P{q) HF;0M0z1R*e0_JC7f:иҨ) g/8gY@%8+Wp1ys?)HP2h,R$~31/(F,()'`YQa$bX )ÏA9Q6+PA2I `@)&($y 93ّ aXbz!k1ՕdTh񓠶2~WMps0I~9dna|JEKH(U)2 y `Ѱ A8 q-3633HgA2Sbgq,."j\ 68a'5c8U3,7437̩wcKwEa`؉f5Y[!aKdi!z*9\+ P2Q9,h1R7pgwYOu)+!CQי9-:]ڢgɣBEsB,OJ8z)79aVz%ȡW) 6~k$#2Q'D^;$*"mL䞻N>TR^XZ\^`;䪑a~hH1ip>W^=qzۼ^x>!f_>" 2NL3 阞n>>n蝞ݕݯznao~\%4>㬎.~Mn~cx%>.knen^"^>^Nծ"aL{_sɽ} _-닄ݵ^~?o(N,_ޒ~X6?&/+@9QD RĘQF=~RH%MDRJ-]SLYfN=}TPEETҎ>UTU^Ś)A\~VXe͞E õiݾW\uFxa[X`… ξ~*FYdʕ-_N#dy5u3fҥMF迟7]쐵jڵmƝ[gϻ]wy!c7֝\r͵"k9~wV?vݽoԱ=_zݿas?ܺ0ҦO@o60B Rl<ʳ:K&1DG|JlMdD_1ƚ[3>G@g[Ўz4U ZXf30gYU0 f\rYh4ơjZk`yD'ua!"iE^GD"aEb%N4ؒAX?!i[UG!RJ KFaL@>NQffPAhA-,4$@7 IZ^_p FDpm$aYG \\bITN\-y IB=D *yP&9ij\- z餕W2%(*ÑꠅZ cм^>hH di\D )BPDfa'\K,@A>;k늊2@*Edf$ݳcfIak+l2횤ފ CO*,RSݱ,-Mf|I Vĵ\+Kn]bt?Um-p . @l)>A-sN-K&v~f}F ED7GP !9E 1MAWL\ ^:-o #4B _7"gҩMP)`>Wi.zD(`94CA x]]5Oxh0g|I@([q.Έ X  pB%ӗbtMǺ"B" t 9(8ꔧ>5JӛܹgKD;raj@*eKRu+\˱tg]&CGsլnA*$%" )PCl*JAd(8 2>$ zG :Vmz( pARڙ2J5᪱s89]TW1vNkK(ePamo`)2!-dkԃ Qu^ֵmT177da)/%oiY nWFF0=$ pK{ݟ7 UeEy)*f)&=[cb@3ebOO@,Zؖ d-dW $22LV(25,d\OLV(jN؁4inE[pP'XAzV0YA nРLҥsc TϘu9Uvǩ & bZӜl[S2=α zeLMwZ׀E][cig[kKpJ 7sQ%]k5\fW=Z0 Z~ߗo-m"y! !b_Yܻ钙 }=$$mg /տ w~hI@c fI%g2 hצٹ&Xa`*%SlWvm*;{[>)+I UNڟp>ePxqnq[~cJ1 0`P%.O-H ?qGR [M`pu8ͮy')w>rj S/>B?7{V2w#Pyoơ 5y~?85lw7) -zxXXQ7|kcayM0P0vhQ/;:*6[GgC,P5h%C-bIW_B8G1E7C;v=~l]b_ 1fu*MO |dssU Y =ŇNhJel؅XPf?59M(Yh"?\B,()'OQSik(E.c&1؉vHPCd `8fƓJ8gEFKȉ(m=ɗ}\e}l&x33lȄX&(q͸TP8m ` F]Y0%A0h,Cd*IBO#&5VW^B>pSYp(y4 P40 pƕ> ơ6GHJɔf5qyt)z ",@.[2I(XDAti>!|h)q1"4*-,!" 9#RQf棨NlaH]:>%S?a 1y~j}Z~6( %-%:!tjZ~yk!zDK%9qJ!Jg6v a jlź*2$VV`;̪+ʪ>g0(9̚w[Bʃ;ZjQTZ:$a*JCR k! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-H*\ȰÇ#J(1 .)\hǏCIɓ(S ŀ \G Uɳϟ"YY&Jx(ЧPJMH NGʵW*@93֓n+i2ز풝(be:oB7*oҮBGxYϠCSu@OxPrMs4$&(A }m '$rv#p-':q)%!go^:迫\;^EGq f%QUM17oVXE  k ugAJZ(R'K<`G,JbBHCn7"V@IבQGz_O"xM9cDFD8S,>H`Q&twXb=5[nFN d%qegI⟀Fu/eB JЀT7 4U`_p`\:c,,F8uD}>}dXcQ$:.4 @RiZz"+4@]-@ziE3a[b+ghʨf'ΙZn`+VDƠG`rXM0-EJzq@a  ˯)LgM\ޖ|1r΋f^9C4\ASǃ. f;bzX Z&1ø*q.R׋XbMT}L| PH+M8xqWDssc^ҺK 5– Zl$b5B: J|BCX{'B5x"lakR#4B ҪμDjSMP)`??@Z7nNzr pxw(`a g>= 3c`XG.hBH3H'< @%`AB.T¢x1wCt% A6 ݖ<42zIM5D:ŢQQ0W 8>/b:a ? $?1-jp @A*v&ddzXH?"z$YgF$HSi!j5 @Ⱦ)Z:OqpӬ&P| dJ9n KXTl+nw+ !cF72M/c;f2țSp!RT`0_cnF1`Giv5iM@Q 8p GI`&x"Ybs:D ( sASSndDܾp #3>IKgBVkP Y)F@EZUYG8uߌABa"驕ͫn+˯*h+՝BUa le8B(K)MV36Iɭ\Vt+GNX AL'Wf( ЂJɑq/^|tݙK@&l / \fJ38)A=ˉ"Cۦjk\Oy#5 dsDžBrIZྈpp 񰄇M>H4jZ WTHV| ܀Hu@` da:w s3'擄LGF'W 2kсA IwNI {@gznOmoxeq[2A1v#2Ywdl4TIuV؋%=K(EhC-&,K @aV pZn76ނ+i07g Sx<]=+OVƶ/Ah+}fpL@ dkAnY";׻5nP*˚ֶxll{ 7ni8qofY6l `[ҐB5Oo]^#ٶo^[!6MfXɍ-!5k(JX9FtqQ6Zw<DD@G{adg Zo7{9h. QEV"WeӨO]f(N`z*# XCn{y` #TKhkڎf@Kzz;TUxK !d(O5]~zj`Sk)ߌFNS>/}GV'=iGP7-RG9Z  pA$KQYU{^#oL%2'$5t~GeqhQu}lZhgar{Dvl~CL^zxW{cG6xD[с$cwZ}yY9q FxXKM Db<AsLU [A\5Hk\Ebth_Gp_vj @f(eD;>Ƈ"_c_?`K/imqA0a#2e0,PaЗ^)`K`)ٚS)ə"2E :$Q pg $ӓJHЉtDegHBpR`ҹhy CWtўupc3Rr bꉐQ9cFJ 4فӝǟ9)ʈ))ÖnV)-zF!@&}8aG<GRIWhAcsyT&Y~1O4DĀW)ovRPi v}"xC0GqQR #D!|CHha:#8RBeq !|zqk OxUQhAn!ez̪G%0Q:`gAvoʭv KJ jWX amYSX"IaT{[#l4%`;Ky""۬Ur w2;2|Q4>Rpݚ?{HJL۴NPR;T[V{X(Q^`b;>Kl۶np{#f i=+z|۷~;Q!{k! ۸ @  p@ C`0 2 P$ x;Ѹ`0 @i   𵮴ڻܫ иfKPh KA{h+MۿߋpЀkj `qBÐ l#,۾# l    ڲ!:<|v[• (,'ܸ/ p7=hjlnp|7ܼ |砱 ,Ѡ1 犾O\n8"P&Q蚾(A> P@ pj>^~C B,m` P  0 {Ӝ;@Ө.N^DT< O| ]M d[>>~.߰P-؏{.~~<ɰ 0 xm ޮ*]6o⫾PD_x  @{+47n;_ Ē #v.Y LXk!mY]~Ȥ  p V@x@fjn ?r/ . 4Q'~)ȎL.N YN6QD-^ĘQF=~RH%MDR0!`@MUTU^eD6% J]O AhkW\uśW/Cּ1Jgʏ;=aCW RA ȝERXp̥ck'[8)[sX|9st7͐٘lpōG\r}z*HbhQR'O_v1&#l$+yl]OC|m0%pG.dQ(0B 'B rl|F^BZH1/v惱Eth zF XtQHP؉B)J+2 g"adCa5'|Gޠ Tr-'M6 ff<x3N,Pkg(Qa̲RK/4SM75hK6PopfmNn`<{dVmXru-qG6XqI/N6ZiV*Ou"!f|;Qªs(e1&bOs% ְ%C6`&` l%B 6eƦgR:&B:ьZݝ3NeNac+ӝM.!gC\k)`&hKx#J{h | ki-6%p\ֵ Y6$k$9xbL)ŧo\fDh\!2!ye|r $;/0'tOGa] 2!|q9).1OSwݫ/=G>yg^)!槧z>{{?|d_Hqٍr$ ~秿~Wobgv`H2Ѐ^_=D(c!VP$G) 5A.i W@ Q &D&@wuЅ/>:$1qR{dw\D'p@>մ5fjs&8M E.Rh!DrH5!"X.3DF/'?Oy@ ZP8HBR/_(qR9y=A'B!gE+ Nvqnծw=X/T<_l`O OE.D!mlb%;YaC2D\6lhE ; mКa-0&,"ֶ׻*Y$~` 0Enr[Ae}@ ,x{ *׻E]n7"ҍ#k-dq%x;_1 \+ 8qINfX&Q[y, 0[pBK2# '(K!3=H@2PK7?2;ܡ@E 2H%, &(ㆤQ.ɔpuok.RmӷZgŸ_G;YCD{IfiS ;D(\C`w0ŭ+▼7Ư;aX jԕ-kZ #Qql_qv|EhXerqX5H|q.PYBW 8g7cAݻolXOG4$-isHAi)g2$KyJ1_]{fCnw8?8k$zb ;3=>C&@[&B ī[F{4 ;x5 fyЄ: A?=ȧ 'a!O;'``QUk5뾙.h\A{4(35_6§:d[6 5@f;MpQ o6,DvZvwy(7ybd A*;x(<)"}*[TT΂*ȩP˜8!S_d'c ĻX9el9˩99cj9FkF ! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! , r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,-! ,-! ,' r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,9 r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,-! ,-! ,-! ,9 q-iǒP0QVHİ LpQ@DPqO6JA{@M@ Jѣ(tӤNtTTRe֪" ! ,-! ,' q-iǒP0QVHİ LpQ@DPqO6JA{@M@ Jѣ(tӤNtTTRe֪" ! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,"-\# *H0J;"q0DSűN6m6VFwPʼnH Hg@G-_*`G͔hLM.j`ў5*UK;D;qjBJA STC8tlTl/ dgħeU* NcsHAE8Ǝb$@r6h@Cq! ,-! ,-! ,' r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,-! ,-! ,4-\# *H0J;"q0DSűN6m6VFwPʼnH Hg@G-_*`G͔hLM.j`ў5*UK;D;qjBJA STC8tlTl/ dgħeU* NcsHAE8Ǝb$@r6h@Cq! ,-! ,-! ,-! ,-! ,9 r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,-! ,-! ,F-\# *H0J;"q0DSűN6m6VFwPʼnH Hg@G-_*`G͔hLM.j`ў5*UK;D;qjBJA STC8tlTl/ dgħeU* NcsHAE8Ǝb$@r6h@Cq! ,-! ,-! ,K r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,-! ,X-\# *H0J;"q0DSűN6m6VFwPʼnH Hg@G-_*`G͔hLM.j`ў5*UK;D;qjBJA STC8tlTl/ dgħeU* NcsHAE8Ǝb$@r6h@Cq! ,-! ,-! ,K q-iǒP0QVHİ LpQ@DPqO6JA{@M@ Jѣ(tӤNtTTRe֪" ! ,9 q-iǒP0QVHİ LpQ@DPqO6JA{@M@ Jѣ(tӤNtTTRe֪" ! ,-! ,' q-iǒP0QVHİ LpQ@DPqO6JA{@M@ Jѣ(tӤNtTTRe֪" ! ,-! , q-iǒP0QVHİ LpQ@DPqO6JA{@M@ Jѣ(tӤNtTTRe֪" ! ,-! ,-! ,-! ,-\# *H0J;"q0DSűN6m6VFwPʼnH Hg@G-_*`G͔hLM.j`ў5*UK;D;qjBJA STC8tlTl/ dgħeU* NcsHAE8Ǝb$@r6h@Cq! ,-! ,-! ,-! ,-! ,-! , r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,-! ,' r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,9 r-L@6\Ȑ"Ă#>$pƎ CIɓЂ>X0`j`&Ja bADPK M :ĹiehTM֎h5! ,-! ,-! ,-! ,-! ,}-8Ђ#2}%&Lo__R=!+p)Eu{n1ߒ4Skqՠi6ѲI}jkʫ>mJ jIt;nȹueB̋ko_$/>H0(882匒]Wzf<Zww[W\X6o X[W(k7Upm 'srU^wIxȕX4z^MיHh_mib *_`f="6ei!t*Du (v(eS5Fk8QE|!h壃)ᄂ$jHnLOY:XZ6=ub!Lfyet΋xRxjҞգjhCZ$Viel?B>{%oiNiQf>j=)&Dǧ~ZE\rJ/Joʚ+pk\Fz)dʩV|cx7U@! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,9 q-iǒP0QVHİ LpQ@DPqO6JA{@M@ Jѣ(tӤNtTTRe֪" ! ,-! ,-! ,-! ,}-\# *H0J;"q0DSűN6m6VFwPʼnH Hg@G-_*`G͔hLM.j`ў5*UK;D;qjBJA STC8tlTl/ dgħeU* NcsHAE8Ǝb$@r6h@CqԮc'XKڭ!Jl]?j4Y|sf89]ն[*0u^jba+~6ZoƝ[{{]}pÉqVJ9 dHCD˩fF&ZM֑$Zj2&[[ Ѓ-p^o%r}\tCawy (W,Wx(Wa(sef%]5[~%Y%BzTRE6,!i@~Qh. ㆯXj!6k&"[QȢp/#r3XQs4e(s>򘣏]*NVUeu^ԤzOZR|XzI*e\j >fkVnovfuiH hCЉ"r(JFM:R^Br*+x*yLjサy:@V&x)ȹلr@hR:9T jkoǢԭE#sJ*. Iiʔ{kޔZIߨ<Ype"E f>gs^hxvlo|r1-$Gp('JQu/n7nM=/K4FZKۚx W}ț 1{]1y~X! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,}-8Ђ#2}%&Lo__R=!+p)Eu{n1ߒ4Skqՠi6ѲI}jkʫ>mJ jIt;nȹueB̋ko_$/>H0(882匒]Wzf<Zww[W\X6o X[W(k7Upm 'srU^wIxȕX4z^MיHh_mib *_`f="6ei!t*Du (v(eS5Fk8QE|!h壃)ᄂ$jHnLOY:XZ6=ub!Lfyet΋xRxjҞգjhCZ$Viel?B>{%oiNiQf>j=)&Dǧ~ZE\rJ/Joʚ+pk\Fz)dʩV|cx7U@! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,}-\# *H0J;"q0DSűN6m6VFwPʼnH Hg@G-_*`G͔hLM.j`ў5*UK;D;qjBJA STC8tlTl/ dgħeU* NcsHAE8Ǝb$@r6h@CqԮc'XKڭ!Jl]?j4Y|sf89]ն[*0u^jba+~6ZoƝ[{{]}pÉqVJ9 dHCD˩fF&ZM֑$Zj2&[[ Ѓ-p^o%r}\tCawy (W,Wx(Wa(sef%]5[~%Y%BzTRE6,!i@~Qh. ㆯXj!6k&"[QȢp/#r3XQs4e(s>򘣏]*NVUeu^ԤzOZR|XzI*e\j >fkVnovfuiH hCЉ"r(JFM:R^Br*+x*yLjサy:@V&x)ȹلr@hR:9T jkoǢԭE#sJ*. Iiʔ{kޔZIߨ<Ype"E f>gs^hxvlo|r1-$Gp('JQu/n7nM=/K4FZKۚx W}ț 1{]1y~X! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,}-8Ђ#2}%&Lo__R=!+p)Eu{n1ߒ4Skqՠi6ѲI}jkʫ>mJ jIt;nȹueB̋ko_$/>H0(882匒]Wzf<Zww[W\X6o X[W(k7Upm 'srU^wIxȕX4z^MיHh_mib *_`f="6ei!t*Du (v(eS5Fk8QE|!h壃)ᄂ$jHnLOY:XZ6=ub!Lfyet΋xRxjҞգjhCZ$Viel?B>{%oiNiQf>j=)&Dǧ~ZE\rJ/Joʚ+pk\Fz)dʩV|cx7U@! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,b [-D o΀HЂDŽ3zPȑ $ǒ+Y0"ŊfThQbG'n!C'Sr ! ,-! ,-! ,-! ,-! ,-! ,-! ,-! ,-H*\ȰÇ#JH1"%NȱǏ CI$I(`ɗ0cʜIBTtP1ɤWLYphҥ6_Y8:i8hԯ`Ê})%'00A,bZ ˞MR/Z]B %?f˸JÈ[p w`uࡓ`Zi;mt~r W uW^A}y;n7PUVW17й!+˔Ï)RNkp|E!{/P_h)p`|adD@AW Nr hҞSb7Br] /Q8t.bB8Xpf5mS\  _@8$cJ2G*М(.eu@C< ޒ\  m9CU^R?`ɑYYgK ]Fnfm AhD~v5fto.K8`AU|RLkBI5()#T9!3<# AT/<V5kr<4Q`2(3KY N B@.Q$*$Gu3 z[ bpÆ+n@p@Wk0,MK|R }J4'{ZY='w?쀃BCP a4fd#+WKs a 6(ad<+pRˈTWP*2BJ34^_0=Ps.J=@M?])B#|5[@AJ0Pky-BF#ƥ-9R /xAsT*^Ye/%㲯.+D3jI?ga=1]j}iOAKsB7ck@pV3\H L"n =8j *Y)pYbl8ag&0F-2 4`,sT*0B8g؆!gĂa QBgP)=43A< ;a,D0Q89o]xEQ %>D11-kxF8vF6.C  j8fNP~J`d @%6R+A O2dRf정p F>2!,|%B(DT \4IP"d# D-Na0ɍ4)0)k$KBLa"$d;"'Ӛ:)xҗPRRӶjA  T*LZQ_%3M TBRsYl`oi"O3!T ѓsS\j :IN ?}& y$OUiɩ vS65X9ɇr 8q b!.q! e` f9A\`!B$L%7|r e3Q5 ~ljWDF^]Ōtcb3+ZeaB 2ыSDAՠu/%>7. jht iL -.%Z`BLP AÂ>s%r(l) Fr:ܹDJfsZ .d.*a˗bw.K6.r KcY2d) !l& <`,G4VK7spe!ZܤwL'#}i"EHmK ':o9]pǀS^5 99рwh ypUt]re89}A4. \HM -@>5B6 <[7֕ q?(9jV7 [ Ev+ 8q${ypwAxq" E1!:h7ѕr-L5.U˗!.'E @sz(,g9(QԀ@"u9X$`'(\m@t6%\@ǺuQ0q+u H!@&C:`" fd-j9/(R$Ύ{A+f7H=%Zb+ +h=m?'y`m|!P ʀ E @np Cm #p&R~$ H,b Ր "@@p61C҂vp , Eq\=$qհ pV,+'WW#'H1!cK a<`&Wz?h6!4Cm;'%Vb;(F4Rrtp(Wg4thxrq A)H?.(F(hv6[{@3tJqjv?H p 5TÈwhOlQ?83(~9x&X='g~ȋ1as ?FRuro5è|!mK{" k +e/0 We aoY"PE  Ps`X%#6C8[:[ B "[eT1/\TF[\J! ParQ.ee9 .]!!DH ^6xCLw# ua\VIxfdJ9J:@<u_<`wr)639d9g)a`Z'fbcњxÈiU☨iYY AI(9%jWSi˜tlчR@h{<<@SalrA#ęjI:11$9@ٙɞ fYx&9 .! ya))ƙ22 JG@ 5 <0^,UqU / 54"q gmݠUDF?V,G= VdB pD6'qd "R L7 \XZ Dyt}~ q`aJPxĆd; ;G0> :uYM3oP2TP,J5I@7349(Z/JЩZxP\Q%p) w$:êʬGr}f:Ab<0*c 4ʪEL' f(AT ua٨`p#NPA6DFKl"+jg9M@8ࠀaV&̴4R1H![ ,'V%L<;d#ĵs$@ \ILdJgta5H[2C>;Y;0I=V|3k3q#Chhp-4d.;rSjq#@V"R(,$ܛY[YPd{&ܛ,qɣ(6ԣ]@U\F AԬa*ͫlg@($Nqw80Gu[-^6~8:/Z` X0 p ʀpPR>T. gu 1@*onҀ[ b xOD]Uz|5j aH L;KAfPn,/;op @F,w9mۀ ~>^~/M =0` #PgW. pc+4^!b~ȞʮA ]]0hgD~:[e>l @-[8 ` ~Y%]~ ,9JB1 醪.?B>~^_bnՀXJ,.A6 Q0 #e"Kk@a"0R?TnAo@ 0 0 !t.z E\U?t_vZ ^ 9psEmə 4AsJ q XJw?_}( FJ?҅r3$d m_/L~c0??"?R?_Q>j?$N`ᜏ a1cq_BkD>d ?%B^>E!, 42C>QD-^ĘQF=~RH%MDRJ-]n W5H%836je`Y4< |hX8tzSpp=sf xP6LmݾW\uśW \IQ%(sʹQt;EHW8OͪUK44ͩ@A*raڵmƝ[ܩABSNPHښ_p%gR\j PdV|ݿ_|ᬊ_E鄗Oa,$x&s͐h`c&yE'#DOD1EW7\)"9ac$(3g`!C0peA2co&y02K-+p)fFh@BC"-$4 :9)/%PCE4Q|Ƈ"?Vs.̏mNl/&!m7/hH/,qfyC5شYl.P3մ5C`H| P0*8cOF>1 <Nv 1S\} !Yy,І7!o> A#"89SCk;πhq|E` *t mBЌgD]B > 0e3 "#{iD36 b$ PNJb)UɈid$%Y |O0HG'Eq]Hg 5DYGje.3Һ>qe4gّw J9<`D-Љg~O2 qTUzUt9Nr hA]*' q8>uBx`W2glc)T"iUR-2˜($_Ư=$5j׻z=r"1#VY<H6ude#”Q6Yc YEm:TôHlEjTZY SjVmի_kX:V^-k[jA֋խojֶ qJ&կ}JΝJ 0Heg|L"[\x;Yʒ˓c*RYs4,DjHDL)Eh7bR-+h$V xi"H•,*^r@pl8mÉ_"ۨhE,@㢆8!H Fr_ډ1A,(hCQA`$1dH{ڣ4 HAzHBddRJJs"O8)SR |>SP%W==O4T-;(gJWpl18V4 ;RSv@3)iV$6o Vuks\jD¤]TR)lVPk5x52>OYx (ݐn[Cy=nrgrU=ˤ*x¯[AvR^jg_*v`c%QPՍpRuVRUrR!ϐFt)C,NϹjA{J19#ģBAI)· }SS@WdlpOF`AV:!Vvȳt* QJ@{4.MN<2"SU{D;B5Ir6A y)g>$C(5R3#%iЃIUZ4XY I )ջ.泈% Q&а) p5X&SAZkp5 A5_ :y:g {ʧd)l3(lі(|s˨c:a ɸ@ 7P'u7f6%bq8887I`8<ģCT9CEdFtD9B1xMGD9ک 2S%# j:C@,`"97?3C]$#@bB6BW44eLֲ"cA˻Ti<“cr|F a:c3~l LGRq`@= {!!>bȻAG)1xY:Ky "0##ȗ,ga=#$8z::4S$6󿚄ɢ4t(@бXF@H%[2ʭ|| JI!46\y\(Y\ʺ\|Pi ¦YnWpmC&߃Bṭtۨ+ K} 0C8#Ck)A{8ԩ ژI+tMCC {͚*MRΗDLJTN STUWĈ[ÊE4Ct:MqNC;,LJ<Ca.X1 kԻlԽeH->٤P! "##ڤ:U8z&Is 8p0<,>%QEj3GH +=4%!  xIzJ|@;q4 4IU+ϋbGقVJGXY=J&m*l*|\f `, P@(\[pBু B,g@npsv͋Ԩx̌RRD ?/T)Cw4 M6t׃ lԤCط͈M͈ء͌؝ٌ͎;survey-2.3.7/input.go000066400000000000000000000127151434440262200145550ustar00rootroot00000000000000package survey import ( "errors" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" ) /* Input is a regular text input that prints each character the user types on the screen and accepts the input with the enter key. Response type is a string. name := "" prompt := &survey.Input{ Message: "What is your name?" } survey.AskOne(prompt, &name) */ type Input struct { Renderer Message string Default string Help string Suggest func(toComplete string) []string answer string typedAnswer string options []core.OptionAnswer selectedIndex int showingHelp bool } // data available to the templates when processing type InputTemplateData struct { Input ShowAnswer bool ShowHelp bool Answer string PageEntries []core.OptionAnswer SelectedIndex int Config *PromptConfig } // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format var InputQuestionTemplate = ` {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }} {{color "reset"}} {{- if .ShowAnswer}} {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} {{- else if .PageEntries -}} {{- .Answer}} [Use arrows to move, enter to select, type to continue] {{- "\n"}} {{- range $ix, $choice := .PageEntries}} {{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}} {{- $choice.Value}} {{- color "reset"}}{{"\n"}} {{- end}} {{- else }} {{- if or (and .Help (not .ShowHelp)) .Suggest }}{{color "cyan"}}[ {{- if and .Help (not .ShowHelp)}}{{ print .Config.HelpInput }} for help {{- if and .Suggest}}, {{end}}{{end -}} {{- if and .Suggest }}{{color "cyan"}}{{ print .Config.SuggestInput }} for suggestions{{end -}} ]{{color "reset"}} {{end}} {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} {{- end}}` func (i *Input) onRune(config *PromptConfig) terminal.OnRuneFn { return terminal.OnRuneFn(func(key rune, line []rune) ([]rune, bool, error) { if i.options != nil && (key == terminal.KeyEnter || key == '\n') { return []rune(i.answer), true, nil } else if i.options != nil && key == terminal.KeyEscape { i.answer = i.typedAnswer i.options = nil } else if key == terminal.KeyArrowUp && len(i.options) > 0 { if i.selectedIndex == 0 { i.selectedIndex = len(i.options) - 1 } else { i.selectedIndex-- } i.answer = i.options[i.selectedIndex].Value } else if (key == terminal.KeyArrowDown || key == terminal.KeyTab) && len(i.options) > 0 { if i.selectedIndex == len(i.options)-1 { i.selectedIndex = 0 } else { i.selectedIndex++ } i.answer = i.options[i.selectedIndex].Value } else if key == terminal.KeyTab && i.Suggest != nil { i.answer = string(line) i.typedAnswer = i.answer options := i.Suggest(i.answer) i.selectedIndex = 0 if len(options) == 0 { return line, false, nil } i.answer = options[0] if len(options) == 1 { i.typedAnswer = i.answer i.options = nil } else { i.options = core.OptionAnswerList(options) } } else { if i.options == nil { return line, false, nil } if key >= terminal.KeySpace { i.answer += string(key) } i.typedAnswer = i.answer i.options = nil } pageSize := config.PageSize opts, idx := paginate(pageSize, i.options, i.selectedIndex) err := i.Render( InputQuestionTemplate, InputTemplateData{ Input: *i, Answer: i.answer, ShowHelp: i.showingHelp, SelectedIndex: idx, PageEntries: opts, Config: config, }, ) if err == nil { err = errReadLineAgain } return []rune(i.typedAnswer), true, err }) } var errReadLineAgain = errors.New("read line again") func (i *Input) Prompt(config *PromptConfig) (interface{}, error) { // render the template err := i.Render( InputQuestionTemplate, InputTemplateData{ Input: *i, Config: config, ShowHelp: i.showingHelp, }, ) if err != nil { return "", err } // start reading runes from the standard in rr := i.NewRuneReader() _ = rr.SetTermMode() defer func() { _ = rr.RestoreTermMode() }() cursor := i.NewCursor() if !config.ShowCursor { cursor.Hide() // hide the cursor defer cursor.Show() // show the cursor when we're done } var line []rune for { if i.options != nil { line = []rune{} } line, err = rr.ReadLineWithDefault(0, line, i.onRune(config)) if err == errReadLineAgain { continue } if err != nil { return "", err } break } i.answer = string(line) // readline print an empty line, go up before we render the follow up cursor.Up(1) // if we ran into the help string if i.answer == config.HelpInput && i.Help != "" { // show the help and prompt again i.showingHelp = true return i.Prompt(config) } // if the line is empty if len(i.answer) == 0 { // use the default value return i.Default, err } lineStr := i.answer i.AppendRenderedText(lineStr) // we're done return lineStr, err } func (i *Input) Cleanup(config *PromptConfig, val interface{}) error { return i.Render( InputQuestionTemplate, InputTemplateData{ Input: *i, ShowAnswer: true, Config: config, Answer: val.(string), }, ) } survey-2.3.7/input_test.go000066400000000000000000000311731434440262200156130ustar00rootroot00000000000000package survey import ( "bytes" "fmt" "io" "os" "strings" "testing" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" "github.com/stretchr/testify/assert" ) func init() { // disable color output for all prompts to simplify testing core.DisableColor = true } func TestInputRender(t *testing.T) { suggestFn := func(string) (s []string) { return s } tests := []struct { title string prompt Input data InputTemplateData expected string }{ { "Test Input question output without default", Input{Message: "What is your favorite month:"}, InputTemplateData{}, fmt.Sprintf("%s What is your favorite month: ", defaultIcons().Question.Text), }, { "Test Input question output with default", Input{Message: "What is your favorite month:", Default: "April"}, InputTemplateData{}, fmt.Sprintf("%s What is your favorite month: (April) ", defaultIcons().Question.Text), }, { "Test Input answer output", Input{Message: "What is your favorite month:"}, InputTemplateData{ShowAnswer: true, Answer: "October"}, fmt.Sprintf("%s What is your favorite month: October\n", defaultIcons().Question.Text), }, { "Test Input question output without default but with help hidden", Input{Message: "What is your favorite month:", Help: "This is helpful"}, InputTemplateData{}, fmt.Sprintf("%s What is your favorite month: [%s for help] ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput)), }, { "Test Input question output with default and with help hidden", Input{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"}, InputTemplateData{}, fmt.Sprintf("%s What is your favorite month: [%s for help] (April) ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput)), }, { "Test Input question output without default but with help shown", Input{Message: "What is your favorite month:", Help: "This is helpful"}, InputTemplateData{ShowHelp: true}, fmt.Sprintf("%s This is helpful\n%s What is your favorite month: ", defaultIcons().Help.Text, defaultIcons().Question.Text), }, { "Test Input question output with default and with help shown", Input{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"}, InputTemplateData{ShowHelp: true}, fmt.Sprintf("%s This is helpful\n%s What is your favorite month: (April) ", defaultIcons().Help.Text, defaultIcons().Question.Text), }, { "Test Input question output with completion", Input{Message: "What is your favorite month:", Suggest: suggestFn}, InputTemplateData{}, fmt.Sprintf("%s What is your favorite month: [%s for suggestions] ", defaultIcons().Question.Text, string(defaultPromptConfig().SuggestInput)), }, { "Test Input question output with suggestions and help hidden", Input{Message: "What is your favorite month:", Suggest: suggestFn, Help: "This is helpful"}, InputTemplateData{}, fmt.Sprintf("%s What is your favorite month: [%s for help, %s for suggestions] ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput), string(defaultPromptConfig().SuggestInput)), }, { "Test Input question output with suggestions and default and help hidden", Input{Message: "What is your favorite month:", Suggest: suggestFn, Help: "This is helpful", Default: "April"}, InputTemplateData{}, fmt.Sprintf("%s What is your favorite month: [%s for help, %s for suggestions] (April) ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput), string(defaultPromptConfig().SuggestInput)), }, { "Test Input question output with suggestions shown", Input{Message: "What is your favorite month:", Suggest: suggestFn}, InputTemplateData{ Answer: "February", PageEntries: core.OptionAnswerList([]string{"January", "February", "March", "etc..."}), SelectedIndex: 1, }, fmt.Sprintf( "%s What is your favorite month: February [Use arrows to move, enter to select, type to continue]\n"+ " January\n%s February\n March\n etc...\n", defaultIcons().Question.Text, defaultPromptConfig().Icons.SelectFocus.Text, ), }, } for _, test := range tests { t.Run(test.title, func(t *testing.T) { r, w, err := os.Pipe() assert.NoError(t, err) test.prompt.WithStdio(terminal.Stdio{Out: w}) test.data.Input = test.prompt // set the runtime config test.data.Config = defaultPromptConfig() err = test.prompt.Render( InputQuestionTemplate, test.data, ) assert.NoError(t, err) assert.NoError(t, w.Close()) var buf bytes.Buffer _, err = io.Copy(&buf, r) assert.NoError(t, err) assert.Contains(t, buf.String(), test.expected) }) } } func TestInputPrompt(t *testing.T) { tests := []PromptTest{ { "Test Input prompt interaction", &Input{ Message: "What is your name?", }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("Larry Bird") c.ExpectEOF() }, "Larry Bird", }, { "Test Input prompt interaction with default", &Input{ Message: "What is your name?", Default: "Johnny Appleseed", }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("") c.ExpectEOF() }, "Johnny Appleseed", }, { "Test Input prompt interaction overriding default", &Input{ Message: "What is your name?", Default: "Johnny Appleseed", }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("Larry Bird") c.ExpectEOF() }, "Larry Bird", }, { "Test Input prompt interaction and prompt for help", &Input{ Message: "What is your name?", Help: "It might be Satoshi Nakamoto", }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("?") c.ExpectString("It might be Satoshi Nakamoto") c.SendLine("Satoshi Nakamoto") c.ExpectEOF() }, "Satoshi Nakamoto", }, { // https://en.wikipedia.org/wiki/ANSI_escape_code // Device Status Report - Reports the cursor position (CPR) to the // application as (as though typed at the keyboard) ESC[n;mR, where n is the // row and m is the column. "SKIP: Test Input prompt with R matching DSR", &Input{ Message: "What is your name?", }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("R") c.ExpectEOF() }, "R", }, { "Test Input prompt interaction when delete", &Input{ Message: "What is your name?", }, func(c expectConsole) { c.ExpectString("What is your name?") c.Send("Johnny ") c.Send(string(terminal.KeyDelete)) c.SendLine("") c.ExpectEOF() }, "Johnny", }, { "Test Input prompt interaction when delete rune", &Input{ Message: "What is your name?", }, func(c expectConsole) { c.ExpectString("What is your name?") c.Send("小明") c.Send(string(terminal.KeyBackspace)) c.SendLine("") c.ExpectEOF() }, "小", }, { "Test Input prompt interaction when ask for suggestion with empty value", &Input{ Message: "What is your favorite month?", Suggest: func(string) []string { return []string{"January", "February"} }, }, func(c expectConsole) { c.ExpectString("What is your favorite month?") c.Send(string(terminal.KeyTab)) c.ExpectString("January") c.ExpectString("February") c.SendLine("") c.ExpectEOF() }, "January", }, { "Test Input prompt interaction when ask for suggestion with some value", &Input{ Message: "What is your favorite month?", Suggest: func(string) []string { return []string{"February"} }, }, func(c expectConsole) { c.ExpectString("What is your favorite month?") c.Send("feb") c.Send(string(terminal.KeyTab)) c.SendLine("") c.ExpectEOF() }, "February", }, { "Test Input prompt interaction when ask for suggestion with some value, choosing the second one", &Input{ Message: "What is your favorite month?", Suggest: func(string) []string { return []string{"January", "February", "March"} }, }, func(c expectConsole) { c.ExpectString("What is your favorite month?") c.Send(string(terminal.KeyTab)) c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowDown)) c.SendLine("") c.ExpectEOF() }, "March", }, { "Test Input prompt interaction when ask for suggestion with some value, choosing the second one", &Input{ Message: "What is your favorite month?", Suggest: func(string) []string { return []string{"January", "February", "March"} }, }, func(c expectConsole) { c.ExpectString("What is your favorite month?") c.Send(string(terminal.KeyTab)) c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowUp)) c.SendLine("") c.ExpectEOF() }, "February", }, { "Test Input prompt interaction when ask for suggestion, complementing it and get new suggestions", &Input{ Message: "Where to save it?", Suggest: func(complete string) []string { if complete == "" { return []string{"folder1/", "folder2/", "folder3/"} } return []string{"folder3/file1.txt", "folder3/file2.txt"} }, }, func(c expectConsole) { c.ExpectString("Where to save it?") c.Send(string(terminal.KeyTab)) c.ExpectString("folder1/") c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowDown)) c.Send("f") c.Send(string(terminal.KeyTab)) c.ExpectString("folder3/file2.txt") c.Send(string(terminal.KeyArrowDown)) c.SendLine("") c.ExpectEOF() }, "folder3/file2.txt", }, { "Test Input prompt interaction when asked suggestions, but abort suggestions", &Input{ Message: "Wanna a suggestion?", Suggest: func(string) []string { return []string{"suggest1", "suggest2"} }, }, func(c expectConsole) { c.ExpectString("Wanna a suggestion?") c.Send("typed answer") c.Send(string(terminal.KeyTab)) c.ExpectString("suggest1") c.Send(string(terminal.KeyEscape)) c.ExpectString("typed answer") c.SendLine("") c.ExpectEOF() }, "typed answer", }, { "Test Input prompt interaction with suggestions, when tabbed with list being shown, should select next suggestion", &Input{ Message: "Choose the special one:", Suggest: func(string) []string { return []string{"suggest1", "suggest2", "special answer"} }, }, func(c expectConsole) { c.ExpectString("Choose the special one:") c.Send("s") c.Send(string(terminal.KeyTab)) c.ExpectString("suggest1") c.ExpectString("suggest2") c.ExpectString("special answer") c.Send(string(terminal.KeyTab)) c.Send(string(terminal.KeyTab)) c.SendLine("") c.ExpectEOF() }, "special answer", }, { "Test Input prompt must allow moving cursor using right and left arrows", &Input{Message: "Filename to save:"}, func(c expectConsole) { c.ExpectString("Filename to save:") c.Send("essay.txt") c.Send(string(terminal.KeyArrowLeft)) c.Send(string(terminal.KeyArrowLeft)) c.Send(string(terminal.KeyArrowLeft)) c.Send(string(terminal.KeyArrowLeft)) c.Send("_final") c.Send(string(terminal.KeyArrowRight)) c.Send(string(terminal.KeyArrowRight)) c.Send(string(terminal.KeyArrowRight)) c.Send(string(terminal.KeyArrowRight)) c.Send(string(terminal.KeyBackspace)) c.Send(string(terminal.KeyBackspace)) c.Send(string(terminal.KeyBackspace)) c.Send("md") c.Send(string(terminal.KeyArrowLeft)) c.Send(string(terminal.KeyArrowLeft)) c.Send(string(terminal.KeyArrowLeft)) c.SendLine("2") c.ExpectEOF() }, "essay_final2.md", }, { "Test Input prompt must allow moving cursor using right and left arrows, even after suggestions", &Input{Message: "Filename to save:", Suggest: func(string) []string { return []string{".txt", ".csv", ".go"} }}, func(c expectConsole) { c.ExpectString("Filename to save:") c.Send(string(terminal.KeyTab)) c.ExpectString(".txt") c.ExpectString(".csv") c.ExpectString(".go") c.Send(string(terminal.KeyTab)) c.Send(string(terminal.KeyArrowLeft)) c.Send(string(terminal.KeyArrowLeft)) c.Send(string(terminal.KeyArrowLeft)) c.Send(string(terminal.KeyArrowLeft)) c.Send(string(terminal.KeyArrowLeft)) c.Send("newtable") c.SendLine("") c.ExpectEOF() }, "newtable.csv", }, } for _, test := range tests { testName := strings.TrimPrefix(test.name, "SKIP: ") t.Run(testName, func(t *testing.T) { if testName != test.name { t.Skipf("warning: flakey test %q", testName) } RunPromptTest(t, test) }) } } survey-2.3.7/multiline.go000066400000000000000000000047221434440262200154170ustar00rootroot00000000000000package survey import ( "strings" "github.com/AlecAivazis/survey/v2/terminal" ) type Multiline struct { Renderer Message string Default string Help string } // data available to the templates when processing type MultilineTemplateData struct { Multiline Answer string ShowAnswer bool ShowHelp bool Config *PromptConfig } // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format var MultilineQuestionTemplate = ` {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }} {{color "reset"}} {{- if .ShowAnswer}} {{- "\n"}}{{color "cyan"}}{{.Answer}}{{color "reset"}} {{- if .Answer }}{{ "\n" }}{{ end }} {{- else }} {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} {{- color "cyan"}}[Enter 2 empty lines to finish]{{color "reset"}} {{- end}}` func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) { // render the template err := i.Render( MultilineQuestionTemplate, MultilineTemplateData{ Multiline: *i, Config: config, }, ) if err != nil { return "", err } // start reading runes from the standard in rr := i.NewRuneReader() _ = rr.SetTermMode() defer func() { _ = rr.RestoreTermMode() }() cursor := i.NewCursor() multiline := make([]string, 0) emptyOnce := false // get the next line for { var line []rune line, err = rr.ReadLine(0) if err != nil { return string(line), err } if string(line) == "" { if emptyOnce { numLines := len(multiline) + 2 cursor.PreviousLine(numLines) for j := 0; j < numLines; j++ { terminal.EraseLine(i.Stdio().Out, terminal.ERASE_LINE_ALL) cursor.NextLine(1) } cursor.PreviousLine(numLines) break } emptyOnce = true } else { emptyOnce = false } multiline = append(multiline, string(line)) } val := strings.Join(multiline, "\n") val = strings.TrimSpace(val) // if the line is empty if len(val) == 0 { // use the default value return i.Default, err } i.AppendRenderedText(val) return val, err } func (i *Multiline) Cleanup(config *PromptConfig, val interface{}) error { return i.Render( MultilineQuestionTemplate, MultilineTemplateData{ Multiline: *i, Answer: val.(string), ShowAnswer: true, Config: config, }, ) } survey-2.3.7/multiline_test.go000066400000000000000000000110141434440262200164460ustar00rootroot00000000000000package survey import ( "bytes" "fmt" "io" "os" "testing" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" "github.com/stretchr/testify/assert" ) func init() { // disable color output for all prompts to simplify testing core.DisableColor = true } func TestMultilineRender(t *testing.T) { tests := []struct { title string prompt Multiline data MultilineTemplateData expected string }{ { "Test Multiline question output without default", Multiline{Message: "What is your favorite month:"}, MultilineTemplateData{}, fmt.Sprintf("%s What is your favorite month: [Enter 2 empty lines to finish]", defaultIcons().Question.Text), }, { "Test Multiline question output with default", Multiline{Message: "What is your favorite month:", Default: "April"}, MultilineTemplateData{}, fmt.Sprintf("%s What is your favorite month: (April) [Enter 2 empty lines to finish]", defaultIcons().Question.Text), }, { "Test Multiline answer output", Multiline{Message: "What is your favorite month:"}, MultilineTemplateData{Answer: "October", ShowAnswer: true}, fmt.Sprintf("%s What is your favorite month: \nOctober", defaultIcons().Question.Text), }, { "Test Multiline question output without default but with help hidden", Multiline{Message: "What is your favorite month:", Help: "This is helpful"}, MultilineTemplateData{}, fmt.Sprintf("%s What is your favorite month: [Enter 2 empty lines to finish]", string(defaultPromptConfig().HelpInput)), }, { "Test Multiline question output with default and with help hidden", Multiline{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"}, MultilineTemplateData{}, fmt.Sprintf("%s What is your favorite month: (April) [Enter 2 empty lines to finish]", string(defaultPromptConfig().HelpInput)), }, { "Test Multiline question output without default but with help shown", Multiline{Message: "What is your favorite month:", Help: "This is helpful"}, MultilineTemplateData{ShowHelp: true}, fmt.Sprintf("%s This is helpful\n%s What is your favorite month: [Enter 2 empty lines to finish]", defaultIcons().Help.Text, defaultIcons().Question.Text), }, { "Test Multiline question output with default and with help shown", Multiline{Message: "What is your favorite month:", Default: "April", Help: "This is helpful"}, MultilineTemplateData{ShowHelp: true}, fmt.Sprintf("%s This is helpful\n%s What is your favorite month: (April) [Enter 2 empty lines to finish]", defaultIcons().Help.Text, defaultIcons().Question.Text), }, } for _, test := range tests { t.Run(test.title, func(t *testing.T) { r, w, err := os.Pipe() assert.NoError(t, err) test.prompt.WithStdio(terminal.Stdio{Out: w}) test.data.Multiline = test.prompt // set the icon set test.data.Config = defaultPromptConfig() err = test.prompt.Render( MultilineQuestionTemplate, test.data, ) assert.NoError(t, err) assert.NoError(t, w.Close()) var buf bytes.Buffer _, err = io.Copy(&buf, r) assert.NoError(t, err) assert.Contains(t, buf.String(), test.expected, test.title) }) } } func TestMultilinePrompt(t *testing.T) { tests := []PromptTest{ { "Test Multiline prompt interaction", &Multiline{ Message: "What is your name?", }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("Larry Bird\nI guess...\nnot sure\n\n") c.ExpectEOF() }, "Larry Bird\nI guess...\nnot sure", }, { "Test Multiline prompt interaction with default", &Multiline{ Message: "What is your name?", Default: "Johnny Appleseed", }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("\n\n") c.ExpectEOF() }, "Johnny Appleseed", }, { "Test Multiline prompt interaction overriding default", &Multiline{ Message: "What is your name?", Default: "Johnny Appleseed", }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("Larry Bird\n\n") c.ExpectEOF() }, "Larry Bird", }, { "Test Multiline does not implement help interaction", &Multiline{ Message: "What is your name?", Help: "It might be Satoshi Nakamoto", }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("?") c.SendLine("Satoshi Nakamoto\n\n") c.ExpectEOF() }, "?\nSatoshi Nakamoto", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { RunPromptTest(t, test) }) } } survey-2.3.7/multiselect.go000066400000000000000000000242051434440262200157450ustar00rootroot00000000000000package survey import ( "errors" "fmt" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" ) /* MultiSelect is a prompt that presents a list of various options to the user for them to select using the arrow keys and enter. Response type is a slice of strings. days := []string{} prompt := &survey.MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, } survey.AskOne(prompt, &days) */ type MultiSelect struct { Renderer Message string Options []string Default interface{} Help string PageSize int VimMode bool FilterMessage string Filter func(filter string, value string, index int) bool Description func(value string, index int) string filter string selectedIndex int checked map[int]bool showingHelp bool } // data available to the templates when processing type MultiSelectTemplateData struct { MultiSelect Answer string ShowAnswer bool Checked map[int]bool SelectedIndex int ShowHelp bool Description func(value string, index int) string PageEntries []core.OptionAnswer Config *PromptConfig // These fields are used when rendering an individual option CurrentOpt core.OptionAnswer CurrentIndex int } // IterateOption sets CurrentOpt and CurrentIndex appropriately so a multiselect option can be rendered individually func (m MultiSelectTemplateData) IterateOption(ix int, opt core.OptionAnswer) interface{} { copy := m copy.CurrentIndex = ix copy.CurrentOpt = opt return copy } func (m MultiSelectTemplateData) GetDescription(opt core.OptionAnswer) string { if m.Description == nil { return "" } return m.Description(opt.Value, opt.Index) } var MultiSelectQuestionTemplate = ` {{- define "option"}} {{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}} {{- if index .Checked .CurrentOpt.Index }}{{color .Config.Icons.MarkedOption.Format }} {{ .Config.Icons.MarkedOption.Text }} {{else}}{{color .Config.Icons.UnmarkedOption.Format }} {{ .Config.Icons.UnmarkedOption.Text }} {{end}} {{- color "reset"}} {{- " "}}{{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{color "reset"}}{{end}} {{end}} {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}} {{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}} {{- else }} {{- " "}}{{- color "cyan"}}[Use arrows to move, space to select,{{- if not .Config.RemoveSelectAll }} to all,{{end}}{{- if not .Config.RemoveSelectNone }} to none,{{end}} type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}} {{- "\n"}} {{- range $ix, $option := .PageEntries}} {{- template "option" $.IterateOption $ix $option}} {{- end}} {{- end}}` // OnChange is called on every keypress. func (m *MultiSelect) OnChange(key rune, config *PromptConfig) { options := m.filterOptions(config) oldFilter := m.filter if key == terminal.KeyArrowUp || (m.VimMode && key == 'k') { // if we are at the top of the list if m.selectedIndex == 0 { // go to the bottom m.selectedIndex = len(options) - 1 } else { // decrement the selected index m.selectedIndex-- } } else if key == terminal.KeyTab || key == terminal.KeyArrowDown || (m.VimMode && key == 'j') { // if we are at the bottom of the list if m.selectedIndex == len(options)-1 { // start at the top m.selectedIndex = 0 } else { // increment the selected index m.selectedIndex++ } // if the user pressed down and there is room to move } else if key == terminal.KeySpace { // the option they have selected if m.selectedIndex < len(options) { selectedOpt := options[m.selectedIndex] // if we haven't seen this index before if old, ok := m.checked[selectedOpt.Index]; !ok { // set the value to true m.checked[selectedOpt.Index] = true } else { // otherwise just invert the current value m.checked[selectedOpt.Index] = !old } if !config.KeepFilter { m.filter = "" } } // only show the help message if we have one to show } else if string(key) == config.HelpInput && m.Help != "" { m.showingHelp = true } else if key == terminal.KeyEscape { m.VimMode = !m.VimMode } else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine { m.filter = "" } else if key == terminal.KeyDelete || key == terminal.KeyBackspace { if m.filter != "" { runeFilter := []rune(m.filter) m.filter = string(runeFilter[0 : len(runeFilter)-1]) } } else if key >= terminal.KeySpace { m.filter += string(key) m.VimMode = false } else if !config.RemoveSelectAll && key == terminal.KeyArrowRight { for _, v := range options { m.checked[v.Index] = true } if !config.KeepFilter { m.filter = "" } } else if !config.RemoveSelectNone && key == terminal.KeyArrowLeft { for _, v := range options { m.checked[v.Index] = false } if !config.KeepFilter { m.filter = "" } } m.FilterMessage = "" if m.filter != "" { m.FilterMessage = " " + m.filter } if oldFilter != m.filter { // filter changed options = m.filterOptions(config) if len(options) > 0 && len(options) <= m.selectedIndex { m.selectedIndex = len(options) - 1 } } // paginate the options // figure out the page size pageSize := m.PageSize // if we dont have a specific one if pageSize == 0 { // grab the global value pageSize = config.PageSize } // TODO if we have started filtering and were looking at the end of a list // and we have modified the filter then we should move the page back! opts, idx := paginate(pageSize, options, m.selectedIndex) tmplData := MultiSelectTemplateData{ MultiSelect: *m, SelectedIndex: idx, Checked: m.checked, ShowHelp: m.showingHelp, Description: m.Description, PageEntries: opts, Config: config, } // render the options _ = m.RenderWithCursorOffset(MultiSelectQuestionTemplate, tmplData, opts, idx) } func (m *MultiSelect) filterOptions(config *PromptConfig) []core.OptionAnswer { // the filtered list answers := []core.OptionAnswer{} // if there is no filter applied if m.filter == "" { // return all of the options return core.OptionAnswerList(m.Options) } // the filter to apply filter := m.Filter if filter == nil { filter = config.Filter } // apply the filter to each option for i, opt := range m.Options { // i the filter says to include the option if filter(m.filter, opt, i) { answers = append(answers, core.OptionAnswer{ Index: i, Value: opt, }) } } // we're done here return answers } func (m *MultiSelect) Prompt(config *PromptConfig) (interface{}, error) { // compute the default state m.checked = make(map[int]bool) // if there is a default if m.Default != nil { // if the default is string values if defaultValues, ok := m.Default.([]string); ok { for _, dflt := range defaultValues { for i, opt := range m.Options { // if the option corresponds to the default if opt == dflt { // we found our initial value m.checked[i] = true // stop looking break } } } // if the default value is index values } else if defaultIndices, ok := m.Default.([]int); ok { // go over every index we need to enable by default for _, idx := range defaultIndices { // and enable it m.checked[idx] = true } } } // if there are no options to render if len(m.Options) == 0 { // we failed return "", errors.New("please provide options to select from") } // figure out the page size pageSize := m.PageSize // if we dont have a specific one if pageSize == 0 { // grab the global value pageSize = config.PageSize } // paginate the options // build up a list of option answers opts, idx := paginate(pageSize, core.OptionAnswerList(m.Options), m.selectedIndex) cursor := m.NewCursor() cursor.Save() // for proper cursor placement during selection cursor.Hide() // hide the cursor defer cursor.Show() // show the cursor when we're done defer cursor.Restore() // clear any accessibility offsetting on exit tmplData := MultiSelectTemplateData{ MultiSelect: *m, SelectedIndex: idx, Description: m.Description, Checked: m.checked, PageEntries: opts, Config: config, } // ask the question err := m.RenderWithCursorOffset(MultiSelectQuestionTemplate, tmplData, opts, idx) if err != nil { return "", err } rr := m.NewRuneReader() _ = rr.SetTermMode() defer func() { _ = rr.RestoreTermMode() }() // start waiting for input for { r, _, err := rr.ReadRune() if err != nil { return "", err } if r == '\r' || r == '\n' { break } if r == terminal.KeyInterrupt { return "", terminal.InterruptErr } if r == terminal.KeyEndTransmission { break } m.OnChange(r, config) } m.filter = "" m.FilterMessage = "" answers := []core.OptionAnswer{} for i, option := range m.Options { if val, ok := m.checked[i]; ok && val { answers = append(answers, core.OptionAnswer{Value: option, Index: i}) } } return answers, nil } // Cleanup removes the options section, and renders the ask like a normal question. func (m *MultiSelect) Cleanup(config *PromptConfig, val interface{}) error { // the answer to show answer := "" for _, ans := range val.([]core.OptionAnswer) { answer = fmt.Sprintf("%s, %s", answer, ans.Value) } // if we answered anything if len(answer) > 2 { // remove the precending commas answer = answer[2:] } // execute the output summary template with the answer return m.Render( MultiSelectQuestionTemplate, MultiSelectTemplateData{ MultiSelect: *m, SelectedIndex: m.selectedIndex, Checked: m.checked, Answer: answer, ShowAnswer: true, Description: m.Description, Config: config, }, ) } survey-2.3.7/multiselect_test.go000066400000000000000000000560701434440262200170110ustar00rootroot00000000000000package survey import ( "bytes" "fmt" "io" "os" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" ) func init() { // disable color output for all prompts to simplify testing core.DisableColor = true } func TestMultiSelectRender(t *testing.T) { prompt := MultiSelect{ Message: "Pick your words:", Options: []string{"foo", "bar", "baz", "buz"}, Default: []string{"bar", "buz"}, } descriptions := []string{"oof", "rab", "zab", "zub"} helpfulPrompt := prompt helpfulPrompt.Help = "This is helpful" pagePrompt := MultiSelect{ Message: "Pick your words:", Options: []string{"foo", "bar", "baz", "buz"}, PageSize: 2, } tests := []struct { title string prompt MultiSelect data MultiSelectTemplateData expected string }{ { "question output", prompt, MultiSelectTemplateData{ SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options), Checked: map[int]bool{1: true, 3: true}, }, strings.Join( []string{ fmt.Sprintf("%s Pick your words: [Use arrows to move, space to select, to all, to none, type to filter]", defaultIcons().Question.Text), fmt.Sprintf(" %s foo", defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s bar", defaultIcons().MarkedOption.Text), fmt.Sprintf("%s %s baz", defaultIcons().SelectFocus.Text, defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s buz\n", defaultIcons().MarkedOption.Text), }, "\n", ), }, { "answer output", prompt, MultiSelectTemplateData{ Answer: "foo, buz", ShowAnswer: true, }, fmt.Sprintf("%s Pick your words: foo, buz\n", defaultIcons().Question.Text), }, { "help hidden", helpfulPrompt, MultiSelectTemplateData{ SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options), Checked: map[int]bool{1: true, 3: true}, }, strings.Join( []string{ fmt.Sprintf("%s Pick your words: [Use arrows to move, space to select, to all, to none, type to filter, %s for more help]", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput)), fmt.Sprintf(" %s foo", defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s bar", defaultIcons().MarkedOption.Text), fmt.Sprintf("%s %s baz", defaultIcons().SelectFocus.Text, defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s buz\n", defaultIcons().MarkedOption.Text), }, "\n", ), }, { "question outputhelp shown", helpfulPrompt, MultiSelectTemplateData{ SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options), Checked: map[int]bool{1: true, 3: true}, ShowHelp: true, }, strings.Join( []string{ fmt.Sprintf("%s This is helpful", defaultIcons().Help.Text), fmt.Sprintf("%s Pick your words: [Use arrows to move, space to select, to all, to none, type to filter]", defaultIcons().Question.Text), fmt.Sprintf(" %s foo", defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s bar", defaultIcons().MarkedOption.Text), fmt.Sprintf("%s %s baz", defaultIcons().SelectFocus.Text, defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s buz\n", defaultIcons().MarkedOption.Text), }, "\n", ), }, { "marked on paginating", pagePrompt, MultiSelectTemplateData{ SelectedIndex: 0, PageEntries: core.OptionAnswerList(pagePrompt.Options)[1:3], /* show unmarked items(bar, baz)*/ Checked: map[int]bool{0: true}, /* foo marked */ }, strings.Join( []string{ fmt.Sprintf("%s Pick your words: [Use arrows to move, space to select, to all, to none, type to filter]", defaultIcons().Question.Text), fmt.Sprintf("%s %s bar", defaultIcons().SelectFocus.Text, defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s baz", defaultIcons().UnmarkedOption.Text), }, "\n", ), }, { "description all", prompt, MultiSelectTemplateData{ SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options), Checked: map[int]bool{1: true, 3: true}, Description: func(value string, index int) string { return descriptions[index] }, }, strings.Join( []string{ fmt.Sprintf("%s Pick your words: [Use arrows to move, space to select, to all, to none, type to filter]", defaultIcons().Question.Text), fmt.Sprintf(" %s foo - oof", defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s bar - rab", defaultIcons().MarkedOption.Text), fmt.Sprintf("%s %s baz - zab", defaultIcons().SelectFocus.Text, defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s buz - zub\n", defaultIcons().MarkedOption.Text), }, "\n", ), }, { "description even", prompt, MultiSelectTemplateData{ SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options), Checked: map[int]bool{1: true, 3: true}, Description: func(value string, index int) string { if index%2 == 0 { return descriptions[index] } return "" }, }, strings.Join( []string{ fmt.Sprintf("%s Pick your words: [Use arrows to move, space to select, to all, to none, type to filter]", defaultIcons().Question.Text), fmt.Sprintf(" %s foo - oof", defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s bar", defaultIcons().MarkedOption.Text), fmt.Sprintf("%s %s baz - zab", defaultIcons().SelectFocus.Text, defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s buz\n", defaultIcons().MarkedOption.Text), }, "\n", ), }, { "description never", prompt, MultiSelectTemplateData{ SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options), Checked: map[int]bool{1: true, 3: true}, Description: func(value string, index int) string { return "" }, }, strings.Join( []string{ fmt.Sprintf("%s Pick your words: [Use arrows to move, space to select, to all, to none, type to filter]", defaultIcons().Question.Text), fmt.Sprintf(" %s foo", defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s bar", defaultIcons().MarkedOption.Text), fmt.Sprintf("%s %s baz", defaultIcons().SelectFocus.Text, defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s buz\n", defaultIcons().MarkedOption.Text), }, "\n", ), }, { "description repeat value", prompt, MultiSelectTemplateData{ SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options), Checked: map[int]bool{1: true, 3: true}, Description: func(value string, index int) string { return value }, }, strings.Join( []string{ fmt.Sprintf("%s Pick your words: [Use arrows to move, space to select, to all, to none, type to filter]", defaultIcons().Question.Text), fmt.Sprintf(" %s foo - foo", defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s bar - bar", defaultIcons().MarkedOption.Text), fmt.Sprintf("%s %s baz - baz", defaultIcons().SelectFocus.Text, defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s buz - buz\n", defaultIcons().MarkedOption.Text), }, "\n", ), }, { "description print index", prompt, MultiSelectTemplateData{ SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options), Checked: map[int]bool{1: true, 3: true}, Description: func(value string, index int) string { return fmt.Sprint(index) }, }, strings.Join( []string{ fmt.Sprintf("%s Pick your words: [Use arrows to move, space to select, to all, to none, type to filter]", defaultIcons().Question.Text), fmt.Sprintf(" %s foo - 0", defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s bar - 1", defaultIcons().MarkedOption.Text), fmt.Sprintf("%s %s baz - 2", defaultIcons().SelectFocus.Text, defaultIcons().UnmarkedOption.Text), fmt.Sprintf(" %s buz - 3\n", defaultIcons().MarkedOption.Text), }, "\n", ), }, } for _, test := range tests { t.Run(test.title, func(t *testing.T) { r, w, err := os.Pipe() assert.NoError(t, err) test.prompt.WithStdio(terminal.Stdio{Out: w}) test.data.MultiSelect = test.prompt // set the icon set test.data.Config = defaultPromptConfig() err = test.prompt.Render( MultiSelectQuestionTemplate, test.data, ) assert.NoError(t, err) assert.NoError(t, w.Close()) var buf bytes.Buffer _, err = io.Copy(&buf, r) assert.NoError(t, err) assert.Contains(t, buf.String(), test.expected) }) } } func TestMultiSelectPrompt(t *testing.T) { tests := []PromptTest{ { "basic interaction", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Select Monday. c.Send(string(terminal.KeyArrowDown)) c.SendLine(" ") c.ExpectEOF() }, []core.OptionAnswer{{Value: "Monday", Index: 1}}, }, { "cycle to next when tab send", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Select Monday. c.Send(string(terminal.KeyTab)) c.Send(" ") c.Send(string(terminal.KeyArrowDown)) c.SendLine(" ") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "Monday", Index: 1}, {Value: "Tuesday", Index: 2}, }, }, { "default value as []string", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Default: []string{"Tuesday", "Thursday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "Tuesday", Index: 2}, {Value: "Thursday", Index: 4}, }, }, { "default value as []int", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Default: []int{2, 4}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "Tuesday", Index: 2}, {Value: "Thursday", Index: 4}, }, }, { "overriding default", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Default: []string{"Tuesday", "Thursday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Deselect Tuesday. c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowDown)) c.SendLine(" ") c.ExpectEOF() }, []core.OptionAnswer{{Value: "Thursday", Index: 4}}, }, { "prompt for help", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Help: "Saturday is best", }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter, ? for more help]") c.Send("?") c.ExpectString("Saturday is best") // Select Saturday c.Send(string(terminal.KeyArrowUp)) c.SendLine(" ") c.ExpectEOF() }, []core.OptionAnswer{{Value: "Saturday", Index: 6}}, }, { "page size", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, PageSize: 1, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Select Monday. c.Send(string(terminal.KeyArrowDown)) c.SendLine(" ") c.ExpectEOF() }, []core.OptionAnswer{{Value: "Monday", Index: 1}}, }, { "vim mode", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, VimMode: true, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Select Tuesday. c.Send("jj ") // Select Thursday. c.Send("jj ") // Select Saturday. c.Send("jj ") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "Tuesday", Index: 2}, {Value: "Thursday", Index: 4}, {Value: "Saturday", Index: 6}, }, }, { "filter interaction", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Filter down to Tuesday. c.Send("Tues") // Select Tuesday. c.Send(" ") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{{Value: "Tuesday", Index: 2}}, }, { "filter is case-insensitive", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Filter down to Tuesday. c.Send("tues") // Select Tuesday. c.Send(" ") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{{Value: "Tuesday", Index: 2}}, }, { "custom filter", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Filter: func(filterValue string, optValue string, index int) bool { return strings.Contains(optValue, filterValue) && len(optValue) >= 7 }, }, func(c expectConsole) { c.ExpectString("What days do you prefer:") // Filter down to days which names are longer than 7 runes c.Send("day") // Select Wednesday. c.Send(string(terminal.KeyArrowDown)) c.SendLine(" ") c.ExpectEOF() }, []core.OptionAnswer{{Value: "Wednesday", Index: 3}}, }, { "clears input on select", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Filter down to Tuesday. c.Send("Tues") // Select Tuesday. c.Send(" ") // Filter down to Tuesday. c.Send("Tues") // Deselect Tuesday. c.Send(" ") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{}, }, { "select all", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Select all c.Send(string(terminal.KeyArrowRight)) c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "Sunday", Index: 0}, {Value: "Monday", Index: 1}, {Value: "Tuesday", Index: 2}, {Value: "Wednesday", Index: 3}, {Value: "Thursday", Index: 4}, {Value: "Friday", Index: 5}, {Value: "Saturday", Index: 6}, }, }, { "select none", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Select first c.Send(" ") // Select second c.Send(string(terminal.KeyArrowDown)) c.Send(" ") // Deselect all c.Send(string(terminal.KeyArrowLeft)) c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{}, }, { "select all with filter", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Send filter c.Send("tu") // Select all c.Send(string(terminal.KeyArrowRight)) c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "Tuesday", Index: 2}, {Value: "Saturday", Index: 6}, }, }, { "select all with filter and select others without filter", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Select first c.Send(" ") // Select second c.Send(string(terminal.KeyArrowDown)) c.Send(" ") // Send filter c.Send("tu") // Select all c.Send(string(terminal.KeyArrowRight)) c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "Sunday", Index: 0}, {Value: "Monday", Index: 1}, {Value: "Tuesday", Index: 2}, {Value: "Saturday", Index: 6}, }, }, { "select all with filter and deselect one without filter", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Send filter c.Send("tu") // Select all c.Send(string(terminal.KeyArrowRight)) // Deselect second c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowDown)) c.Send(" ") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "Saturday", Index: 6}, }, }, { "delete filter word", &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, func(c expectConsole) { c.ExpectString("What days do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Filter down to 'Sunday' c.Send("su") // Delete 'u' c.Send(string(terminal.KeyDelete)) // Filter down to 'Saturday' c.Send("at") // Select 'Saturday' c.Send(string(terminal.KeyArrowDown)) c.Send(" ") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "Saturday", Index: 6}, }, }, { "delete filter word in rune", &MultiSelect{ Message: "今天中午吃什么?", Options: []string{"青椒牛肉丝", "小炒肉", "小煎鸡"}, }, func(c expectConsole) { c.ExpectString("今天中午吃什么? [Use arrows to move, space to select, to all, to none, type to filter]") // Filter down to 小炒肉. c.Send("小炒") // Filter down to 小炒肉 and 小煎鸡. c.Send(string(terminal.KeyBackspace)) // Filter down to 小煎鸡. c.Send("煎") // Select 小煎鸡. c.Send(" ") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "小煎鸡", Index: 2}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { RunPromptTest(t, test) }) } } func TestMultiSelectPromptKeepFilter(t *testing.T) { tests := []PromptTest{ { "multi select with filter keep", &MultiSelect{ Message: "What color do you prefer:", Options: []string{"green", "red", "light-green", "blue", "black", "yellow", "purple"}, }, func(c expectConsole) { c.ExpectString("What color do you prefer: [Use arrows to move, space to select, to all, to none, type to filter]") // Filter down to green c.Send("green") // Select green. c.Send(" ") // Select light-green. c.Send(string(terminal.KeyArrowDown)) c.Send(" ") c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ {Value: "green", Index: 0}, {Value: "light-green", Index: 2}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { RunPromptTestKeepFilter(t, test) }) } } func TestMultiSelectPromptRemoveSelectAll(t *testing.T) { tests := []PromptTest{ { "multi select with remove select all option", &MultiSelect{ Message: "What color do you prefer:", Options: []string{"green", "red", "light-green", "blue", "black", "yellow", "purple"}, }, func(c expectConsole) { c.ExpectString("What color do you prefer: [Use arrows to move, space to select, to none, type to filter]") // Select the first option "green" c.Send(" ") // attempt to select all (this shouldn't do anything) c.Send(string(terminal.KeyArrowRight)) // end the session c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ // we should only have one option selected, not all of them {Value: "green", Index: 0}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { RunPromptTestRemoveSelectAll(t, test) }) } } func TestMultiSelectPromptRemoveSelectNone(t *testing.T) { tests := []PromptTest{ { "multi select with remove select none option", &MultiSelect{ Message: "What color do you prefer:", Options: []string{"green", "red", "light-green", "blue", "black", "yellow", "purple"}, }, func(c expectConsole) { c.ExpectString("What color do you prefer: [Use arrows to move, space to select, to all, type to filter]") // Select the first option "green" c.Send(" ") // attempt to unselect all (this shouldn't do anything) c.Send(string(terminal.KeyArrowLeft)) // end the session c.SendLine("") c.ExpectEOF() }, []core.OptionAnswer{ // we should only have one option selected, not all of them {Value: "green", Index: 0}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { RunPromptTestRemoveSelectNone(t, test) }) } } survey-2.3.7/password.go000066400000000000000000000047631434440262200152640ustar00rootroot00000000000000package survey import ( "fmt" "strings" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" ) /* Password is like a normal Input but the text shows up as *'s and there is no default. Response type is a string. password := "" prompt := &survey.Password{ Message: "Please type your password" } survey.AskOne(prompt, &password) */ type Password struct { Renderer Message string Help string } type PasswordTemplateData struct { Password ShowHelp bool Config *PromptConfig } // PasswordQuestionTemplate is a template with color formatting. See Documentation: https://github.com/mgutz/ansi#style-format var PasswordQuestionTemplate = ` {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }} {{color "reset"}} {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}` func (p *Password) Prompt(config *PromptConfig) (interface{}, error) { // render the question template userOut, _, err := core.RunTemplate( PasswordQuestionTemplate, PasswordTemplateData{ Password: *p, Config: config, }, ) if err != nil { return "", err } if _, err := fmt.Fprint(terminal.NewAnsiStdout(p.Stdio().Out), userOut); err != nil { return "", err } rr := p.NewRuneReader() _ = rr.SetTermMode() defer func() { _ = rr.RestoreTermMode() }() // no help msg? Just return any response if p.Help == "" { line, err := rr.ReadLine(config.HideCharacter) return string(line), err } cursor := p.NewCursor() var line []rune // process answers looking for help prompt answer for { line, err = rr.ReadLine(config.HideCharacter) if err != nil { return string(line), err } if string(line) == config.HelpInput { // terminal will echo the \n so we need to jump back up one row cursor.PreviousLine(1) err = p.Render( PasswordQuestionTemplate, PasswordTemplateData{ Password: *p, ShowHelp: true, Config: config, }, ) if err != nil { return "", err } continue } break } lineStr := string(line) p.AppendRenderedText(strings.Repeat(string(config.HideCharacter), len(lineStr))) return lineStr, err } // Cleanup hides the string with a fixed number of characters. func (prompt *Password) Cleanup(config *PromptConfig, val interface{}) error { return nil } survey-2.3.7/password_test.go000066400000000000000000000043321434440262200163130ustar00rootroot00000000000000package survey import ( "fmt" "testing" "github.com/AlecAivazis/survey/v2/core" "github.com/stretchr/testify/assert" ) func init() { // disable color output for all prompts to simplify testing core.DisableColor = true } func TestPasswordRender(t *testing.T) { tests := []struct { title string prompt Password data PasswordTemplateData expected string }{ { "Test Password question output", Password{Message: "Tell me your secret:"}, PasswordTemplateData{}, fmt.Sprintf("%s Tell me your secret: ", defaultIcons().Question.Text), }, { "Test Password question output with help hidden", Password{Message: "Tell me your secret:", Help: "This is helpful"}, PasswordTemplateData{}, fmt.Sprintf("%s Tell me your secret: [%s for help] ", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput)), }, { "Test Password question output with help shown", Password{Message: "Tell me your secret:", Help: "This is helpful"}, PasswordTemplateData{ShowHelp: true}, fmt.Sprintf("%s This is helpful\n%s Tell me your secret: ", defaultIcons().Help.Text, defaultIcons().Question.Text), }, } for _, test := range tests { test.data.Password = test.prompt // set the icon set test.data.Config = defaultPromptConfig() actual, _, err := core.RunTemplate( PasswordQuestionTemplate, &test.data, ) assert.Nil(t, err, test.title) assert.Equal(t, test.expected, actual, test.title) } } func TestPasswordPrompt(t *testing.T) { tests := []PromptTest{ { "Test Password prompt interaction", &Password{ Message: "Please type your password", }, func(c expectConsole) { c.ExpectString("Please type your password") c.Send("secret") c.SendLine("") c.ExpectEOF() }, "secret", }, { "Test Password prompt interaction with help", &Password{ Message: "Please type your password", Help: "It's a secret", }, func(c expectConsole) { c.ExpectString("Please type your password") c.SendLine("?") c.ExpectString("It's a secret") c.Send("secret") c.SendLine("") c.ExpectEOF() }, "secret", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { RunPromptTest(t, test) }) } } survey-2.3.7/renderer.go000066400000000000000000000116371434440262200152260ustar00rootroot00000000000000package survey import ( "bytes" "fmt" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" "golang.org/x/term" ) type Renderer struct { stdio terminal.Stdio renderedErrors bytes.Buffer renderedText bytes.Buffer } type ErrorTemplateData struct { Error error Icon Icon } var ErrorTemplate = `{{color .Icon.Format }}{{ .Icon.Text }} Sorry, your reply was invalid: {{ .Error.Error }}{{color "reset"}} ` func (r *Renderer) WithStdio(stdio terminal.Stdio) { r.stdio = stdio } func (r *Renderer) Stdio() terminal.Stdio { return r.stdio } func (r *Renderer) NewRuneReader() *terminal.RuneReader { return terminal.NewRuneReader(r.stdio) } func (r *Renderer) NewCursor() *terminal.Cursor { return &terminal.Cursor{ In: r.stdio.In, Out: r.stdio.Out, } } func (r *Renderer) Error(config *PromptConfig, invalid error) error { // cleanup the currently rendered errors r.resetPrompt(r.countLines(r.renderedErrors)) r.renderedErrors.Reset() // cleanup the rest of the prompt r.resetPrompt(r.countLines(r.renderedText)) r.renderedText.Reset() userOut, layoutOut, err := core.RunTemplate(ErrorTemplate, &ErrorTemplateData{ Error: invalid, Icon: config.Icons.Error, }) if err != nil { return err } // send the message to the user if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil { return err } // add the printed text to the rendered error buffer so we can cleanup later r.appendRenderedError(layoutOut) return nil } func (r *Renderer) OffsetCursor(offset int) { cursor := r.NewCursor() for offset > 0 { cursor.PreviousLine(1) offset-- } } func (r *Renderer) Render(tmpl string, data interface{}) error { // cleanup the currently rendered text lineCount := r.countLines(r.renderedText) r.resetPrompt(lineCount) r.renderedText.Reset() // render the template summarizing the current state userOut, layoutOut, err := core.RunTemplate(tmpl, data) if err != nil { return err } // print the summary if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil { return err } // add the printed text to the rendered text buffer so we can cleanup later r.AppendRenderedText(layoutOut) // nothing went wrong return nil } func (r *Renderer) RenderWithCursorOffset(tmpl string, data IterableOpts, opts []core.OptionAnswer, idx int) error { cursor := r.NewCursor() cursor.Restore() // clear any accessibility offsetting if err := r.Render(tmpl, data); err != nil { return err } cursor.Save() offset := computeCursorOffset(MultiSelectQuestionTemplate, data, opts, idx, r.termWidthSafe()) r.OffsetCursor(offset) return nil } // appendRenderedError appends text to the renderer's error buffer // which is used to track what has been printed. It is not exported // as errors should only be displayed via Error(config, error). func (r *Renderer) appendRenderedError(text string) { r.renderedErrors.WriteString(text) } // AppendRenderedText appends text to the renderer's text buffer // which is used to track of what has been printed. The buffer is used // to calculate how many lines to erase before updating the prompt. func (r *Renderer) AppendRenderedText(text string) { r.renderedText.WriteString(text) } func (r *Renderer) resetPrompt(lines int) { // clean out current line in case tmpl didnt end in newline cursor := r.NewCursor() cursor.HorizontalAbsolute(0) terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL) // clean up what we left behind last time for i := 0; i < lines; i++ { cursor.PreviousLine(1) terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL) } } func (r *Renderer) termWidth() (int, error) { fd := int(r.stdio.Out.Fd()) termWidth, _, err := term.GetSize(fd) return termWidth, err } func (r *Renderer) termWidthSafe() int { w, err := r.termWidth() if err != nil || w == 0 { // if we got an error due to terminal.GetSize not being supported // on current platform then just assume a very wide terminal w = 10000 } return w } // countLines will return the count of `\n` with the addition of any // lines that have wrapped due to narrow terminal width func (r *Renderer) countLines(buf bytes.Buffer) int { w := r.termWidthSafe() bufBytes := buf.Bytes() count := 0 curr := 0 for curr < len(bufBytes) { var delim int // read until the next newline or the end of the string relDelim := bytes.IndexRune(bufBytes[curr:], '\n') if relDelim != -1 { count += 1 // new line found, add it to the count delim = curr + relDelim } else { delim = len(bufBytes) // no new line found, read rest of text } str := string(bufBytes[curr:delim]) if lineWidth := terminal.StringWidth(str); lineWidth > w { // account for word wrapping count += lineWidth / w if (lineWidth % w) == 0 { // content whose width is exactly a multiplier of available width should not // count as having wrapped on the last line count -= 1 } } curr = delim + 1 } return count } survey-2.3.7/renderer_posix_test.go000066400000000000000000000032661434440262200175060ustar00rootroot00000000000000//go:build !windows // +build !windows package survey import ( "bytes" "strings" "testing" "github.com/AlecAivazis/survey/v2/terminal" pseudotty "github.com/creack/pty" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRenderer_countLines(t *testing.T) { t.Parallel() termWidth := 72 pty, tty, err := pseudotty.Open() require.Nil(t, err) defer pty.Close() defer tty.Close() err = pseudotty.Setsize(tty, &pseudotty.Winsize{ Rows: 30, Cols: uint16(termWidth), }) require.Nil(t, err) r := Renderer{ stdio: terminal.Stdio{ In: tty, Out: tty, Err: tty, }, } tests := []struct { name string buf *bytes.Buffer wants int }{ { name: "empty", buf: new(bytes.Buffer), wants: 0, }, { name: "no newline", buf: bytes.NewBufferString("hello"), wants: 0, }, { name: "short line", buf: bytes.NewBufferString("hello\n"), wants: 1, }, { name: "three short lines", buf: bytes.NewBufferString("hello\nbeautiful\nworld\n"), wants: 3, }, { name: "full line", buf: bytes.NewBufferString(strings.Repeat("A", termWidth) + "\n"), wants: 1, }, { name: "overflow", buf: bytes.NewBufferString(strings.Repeat("A", termWidth+1) + "\n"), wants: 2, }, { name: "overflow fills 2nd line", buf: bytes.NewBufferString(strings.Repeat("A", termWidth*2) + "\n"), wants: 2, }, { name: "overflow spills to 3rd line", buf: bytes.NewBufferString(strings.Repeat("A", termWidth*2+1) + "\n"), wants: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { n := r.countLines(*tt.buf) assert.Equal(t, tt.wants, n) }) } } survey-2.3.7/renderer_test.go000077500000000000000000000012141434440262200162560ustar00rootroot00000000000000package survey import ( "fmt" "testing" "github.com/AlecAivazis/survey/v2/core" ) func TestValidationError(t *testing.T) { err := fmt.Errorf("Football is not a valid month") actual, _, err := core.RunTemplate( ErrorTemplate, &ErrorTemplateData{ Error: err, Icon: defaultIcons().Error, }, ) if err != nil { t.Errorf("Failed to run template to format error: %s", err) } expected := fmt.Sprintf("%s Sorry, your reply was invalid: Football is not a valid month\n", defaultIcons().Error.Text) if actual != expected { t.Errorf("Formatted error was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected) } } survey-2.3.7/select.go000066400000000000000000000216601434440262200146740ustar00rootroot00000000000000package survey import ( "errors" "fmt" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" ) /* Select is a prompt that presents a list of various options to the user for them to select using the arrow keys and enter. Response type is a string. color := "" prompt := &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, } survey.AskOne(prompt, &color) */ type Select struct { Renderer Message string Options []string Default interface{} Help string PageSize int VimMode bool FilterMessage string Filter func(filter string, value string, index int) bool Description func(value string, index int) string filter string selectedIndex int showingHelp bool } // SelectTemplateData is the data available to the templates when processing type SelectTemplateData struct { Select PageEntries []core.OptionAnswer SelectedIndex int Answer string ShowAnswer bool ShowHelp bool Description func(value string, index int) string Config *PromptConfig // These fields are used when rendering an individual option CurrentOpt core.OptionAnswer CurrentIndex int } // IterateOption sets CurrentOpt and CurrentIndex appropriately so a select option can be rendered individually func (s SelectTemplateData) IterateOption(ix int, opt core.OptionAnswer) interface{} { copy := s copy.CurrentIndex = ix copy.CurrentOpt = opt return copy } func (s SelectTemplateData) GetDescription(opt core.OptionAnswer) string { if s.Description == nil { return "" } return s.Description(opt.Value, opt.Index) } var SelectQuestionTemplate = ` {{- define "option"}} {{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}} {{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{end}} {{- color "reset"}} {{end}} {{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} {{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} {{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}} {{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}} {{- else}} {{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}} {{- "\n"}} {{- range $ix, $option := .PageEntries}} {{- template "option" $.IterateOption $ix $option}} {{- end}} {{- end}}` // OnChange is called on every keypress. func (s *Select) OnChange(key rune, config *PromptConfig) bool { options := s.filterOptions(config) oldFilter := s.filter // if the user pressed the enter key and the index is a valid option if key == terminal.KeyEnter || key == '\n' { // if the selected index is a valid option if len(options) > 0 && s.selectedIndex < len(options) { // we're done (stop prompting the user) return true } // we're not done (keep prompting) return false // if the user pressed the up arrow or 'k' to emulate vim } else if (key == terminal.KeyArrowUp || (s.VimMode && key == 'k')) && len(options) > 0 { // if we are at the top of the list if s.selectedIndex == 0 { // start from the button s.selectedIndex = len(options) - 1 } else { // otherwise we are not at the top of the list so decrement the selected index s.selectedIndex-- } // if the user pressed down or 'j' to emulate vim } else if (key == terminal.KeyTab || key == terminal.KeyArrowDown || (s.VimMode && key == 'j')) && len(options) > 0 { // if we are at the bottom of the list if s.selectedIndex == len(options)-1 { // start from the top s.selectedIndex = 0 } else { // increment the selected index s.selectedIndex++ } // only show the help message if we have one } else if string(key) == config.HelpInput && s.Help != "" { s.showingHelp = true // if the user wants to toggle vim mode on/off } else if key == terminal.KeyEscape { s.VimMode = !s.VimMode // if the user hits any of the keys that clear the filter } else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine { s.filter = "" // if the user is deleting a character in the filter } else if key == terminal.KeyDelete || key == terminal.KeyBackspace { // if there is content in the filter to delete if s.filter != "" { runeFilter := []rune(s.filter) // subtract a line from the current filter s.filter = string(runeFilter[0 : len(runeFilter)-1]) // we removed the last value in the filter } } else if key >= terminal.KeySpace { s.filter += string(key) // make sure vim mode is disabled s.VimMode = false } s.FilterMessage = "" if s.filter != "" { s.FilterMessage = " " + s.filter } if oldFilter != s.filter { // filter changed options = s.filterOptions(config) if len(options) > 0 && len(options) <= s.selectedIndex { s.selectedIndex = len(options) - 1 } } // figure out the options and index to render // figure out the page size pageSize := s.PageSize // if we dont have a specific one if pageSize == 0 { // grab the global value pageSize = config.PageSize } // TODO if we have started filtering and were looking at the end of a list // and we have modified the filter then we should move the page back! opts, idx := paginate(pageSize, options, s.selectedIndex) tmplData := SelectTemplateData{ Select: *s, SelectedIndex: idx, ShowHelp: s.showingHelp, Description: s.Description, PageEntries: opts, Config: config, } // render the options _ = s.RenderWithCursorOffset(SelectQuestionTemplate, tmplData, opts, idx) // keep prompting return false } func (s *Select) filterOptions(config *PromptConfig) []core.OptionAnswer { // the filtered list answers := []core.OptionAnswer{} // if there is no filter applied if s.filter == "" { return core.OptionAnswerList(s.Options) } // the filter to apply filter := s.Filter if filter == nil { filter = config.Filter } for i, opt := range s.Options { // i the filter says to include the option if filter(s.filter, opt, i) { answers = append(answers, core.OptionAnswer{ Index: i, Value: opt, }) } } // return the list of answers return answers } func (s *Select) Prompt(config *PromptConfig) (interface{}, error) { // if there are no options to render if len(s.Options) == 0 { // we failed return "", errors.New("please provide options to select from") } s.selectedIndex = 0 if s.Default != nil { switch defaultValue := s.Default.(type) { case string: var found bool for i, opt := range s.Options { if opt == defaultValue { s.selectedIndex = i found = true } } if !found { return "", fmt.Errorf("default value %q not found in options", defaultValue) } case int: if defaultValue >= len(s.Options) { return "", fmt.Errorf("default index %d exceeds the number of options", defaultValue) } s.selectedIndex = defaultValue default: return "", errors.New("default value of select must be an int or string") } } // figure out the page size pageSize := s.PageSize // if we dont have a specific one if pageSize == 0 { // grab the global value pageSize = config.PageSize } // figure out the options and index to render opts, idx := paginate(pageSize, core.OptionAnswerList(s.Options), s.selectedIndex) cursor := s.NewCursor() cursor.Save() // for proper cursor placement during selection cursor.Hide() // hide the cursor defer cursor.Show() // show the cursor when we're done defer cursor.Restore() // clear any accessibility offsetting on exit tmplData := SelectTemplateData{ Select: *s, SelectedIndex: idx, Description: s.Description, ShowHelp: s.showingHelp, PageEntries: opts, Config: config, } // ask the question err := s.RenderWithCursorOffset(SelectQuestionTemplate, tmplData, opts, idx) if err != nil { return "", err } rr := s.NewRuneReader() _ = rr.SetTermMode() defer func() { _ = rr.RestoreTermMode() }() // start waiting for input for { r, _, err := rr.ReadRune() if err != nil { return "", err } if r == terminal.KeyInterrupt { return "", terminal.InterruptErr } if r == terminal.KeyEndTransmission { break } if s.OnChange(r, config) { break } } options := s.filterOptions(config) s.filter = "" s.FilterMessage = "" if s.selectedIndex < len(options) { return options[s.selectedIndex], err } return options[0], err } func (s *Select) Cleanup(config *PromptConfig, val interface{}) error { cursor := s.NewCursor() cursor.Restore() return s.Render( SelectQuestionTemplate, SelectTemplateData{ Select: *s, Answer: val.(core.OptionAnswer).Value, ShowAnswer: true, Description: s.Description, Config: config, }, ) } survey-2.3.7/select_test.go000066400000000000000000000217601434440262200157340ustar00rootroot00000000000000package survey import ( "bytes" "fmt" "io" "os" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" ) func init() { // disable color output for all prompts to simplify testing core.DisableColor = true } func TestSelectRender(t *testing.T) { prompt := Select{ Message: "Pick your word:", Options: []string{"foo", "bar", "baz", "buz"}, Default: "baz", } helpfulPrompt := prompt helpfulPrompt.Help = "This is helpful" tests := []struct { title string prompt Select data SelectTemplateData expected string }{ { "Test Select question output", prompt, SelectTemplateData{SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options)}, strings.Join( []string{ fmt.Sprintf("%s Pick your word: [Use arrows to move, type to filter]", defaultIcons().Question.Text), " foo", " bar", fmt.Sprintf("%s baz", defaultIcons().SelectFocus.Text), " buz\n", }, "\n", ), }, { "Test Select answer output", prompt, SelectTemplateData{Answer: "buz", ShowAnswer: true, PageEntries: core.OptionAnswerList(prompt.Options)}, fmt.Sprintf("%s Pick your word: buz\n", defaultIcons().Question.Text), }, { "Test Select question output with help hidden", helpfulPrompt, SelectTemplateData{SelectedIndex: 2, PageEntries: core.OptionAnswerList(prompt.Options)}, strings.Join( []string{ fmt.Sprintf("%s Pick your word: [Use arrows to move, type to filter, %s for more help]", defaultIcons().Question.Text, string(defaultPromptConfig().HelpInput)), " foo", " bar", fmt.Sprintf("%s baz", defaultIcons().SelectFocus.Text), " buz\n", }, "\n", ), }, { "Test Select question output with help shown", helpfulPrompt, SelectTemplateData{SelectedIndex: 2, ShowHelp: true, PageEntries: core.OptionAnswerList(prompt.Options)}, strings.Join( []string{ fmt.Sprintf("%s This is helpful", defaultIcons().Help.Text), fmt.Sprintf("%s Pick your word: [Use arrows to move, type to filter]", defaultIcons().Question.Text), " foo", " bar", fmt.Sprintf("%s baz", defaultIcons().SelectFocus.Text), " buz\n", }, "\n", ), }, } for _, test := range tests { t.Run(test.title, func(t *testing.T) { r, w, err := os.Pipe() assert.NoError(t, err) test.prompt.WithStdio(terminal.Stdio{Out: w}) test.data.Select = test.prompt // set the icon set test.data.Config = defaultPromptConfig() err = test.prompt.Render( SelectQuestionTemplate, test.data, ) assert.NoError(t, err) assert.NoError(t, w.Close()) var buf bytes.Buffer _, err = io.Copy(&buf, r) assert.NoError(t, err) assert.Contains(t, buf.String(), test.expected) }) } } func TestSelectPrompt(t *testing.T) { tests := []PromptTest{ { "basic interaction: blue", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, }, func(c expectConsole) { c.ExpectString("Choose a color:") // Select blue. c.SendLine(string(terminal.KeyArrowDown)) c.ExpectEOF() }, core.OptionAnswer{Index: 1, Value: "blue"}, }, { "basic interaction: green", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, }, func(c expectConsole) { c.ExpectString("Choose a color:") // Select blue. c.Send(string(terminal.KeyArrowDown)) // Select green. c.SendLine(string(terminal.KeyTab)) c.ExpectEOF() }, core.OptionAnswer{Index: 2, Value: "green"}, }, { "default value", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, Default: "green", }, func(c expectConsole) { c.ExpectString("Choose a color:") // Select green. c.SendLine("") c.ExpectEOF() }, core.OptionAnswer{Index: 2, Value: "green"}, }, { "default index", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, Default: 2, }, func(c expectConsole) { c.ExpectString("Choose a color:") // Select green. c.SendLine("") c.ExpectEOF() }, core.OptionAnswer{Index: 2, Value: "green"}, }, { "overriding default", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, Default: "blue", }, func(c expectConsole) { c.ExpectString("Choose a color:") // Select red. c.SendLine(string(terminal.KeyArrowUp)) c.ExpectEOF() }, core.OptionAnswer{Index: 0, Value: "red"}, }, { "SKIP: prompt for help", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, Help: "My favourite color is red", }, func(c expectConsole) { c.ExpectString("Choose a color:") c.SendLine("?") c.ExpectString("My favourite color is red") // Select red. c.SendLine("") c.ExpectEOF() }, core.OptionAnswer{Index: 0, Value: "red"}, }, { "PageSize", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, PageSize: 1, }, func(c expectConsole) { c.ExpectString("Choose a color:") // Select green. c.SendLine(string(terminal.KeyArrowUp)) c.ExpectEOF() }, core.OptionAnswer{Index: 2, Value: "green"}, }, { "vim mode", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, VimMode: true, }, func(c expectConsole) { c.ExpectString("Choose a color:") // Select blue. c.SendLine("j") c.ExpectEOF() }, core.OptionAnswer{Index: 1, Value: "blue"}, }, { "filter", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, }, func(c expectConsole) { c.ExpectString("Choose a color:") // Filter down to red and green. c.Send("re") // Select green. c.SendLine(string(terminal.KeyArrowDown)) c.ExpectEOF() }, core.OptionAnswer{Index: 2, Value: "green"}, }, { "filter is case-insensitive", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, }, func(c expectConsole) { c.ExpectString("Choose a color:") // Filter down to red and green. c.Send("RE") // Select green. c.SendLine(string(terminal.KeyArrowDown)) c.ExpectEOF() }, core.OptionAnswer{Index: 2, Value: "green"}, }, { "Can select the first result in a filtered list if there is a default", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, Default: "blue", }, func(c expectConsole) { c.ExpectString("Choose a color:") // Make sure only red is showing c.SendLine("red") c.ExpectEOF() }, core.OptionAnswer{Index: 0, Value: "red"}, }, { "custom filter", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, Filter: func(filter string, optValue string, optIndex int) (filtered bool) { return len(optValue) >= 5 }, }, func(c expectConsole) { c.ExpectString("Choose a color:") // Filter down to only green since custom filter only keeps options that are longer than 5 runes c.SendLine("re") c.ExpectEOF() }, core.OptionAnswer{Index: 2, Value: "green"}, }, { "answers filtered out", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, }, func(c expectConsole) { c.ExpectString("Choose a color:") // filter away everything c.SendLine("z") // send enter (should get ignored since there are no answers) c.SendLine(string(terminal.KeyEnter)) // remove the filter we just applied c.SendLine(string(terminal.KeyBackspace)) // press enter c.SendLine(string(terminal.KeyEnter)) }, core.OptionAnswer{Index: 0, Value: "red"}, }, { "delete filter word", &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "black"}, }, func(c expectConsole) { c.ExpectString("Choose a color:") // Filter down to blue. c.Send("blu") // Filter down to blue and black. c.Send(string(terminal.KeyDelete)) // Select black. c.SendLine(string(terminal.KeyArrowDown)) c.ExpectEOF() }, core.OptionAnswer{Index: 2, Value: "black"}, }, { "delete filter word in rune", &Select{ Message: "今天中午吃什么?", Options: []string{"青椒牛肉丝", "小炒肉", "小煎鸡"}, }, func(c expectConsole) { c.ExpectString("今天中午吃什么?") // Filter down to 小炒肉. c.Send("小炒") // Filter down to 小炒肉 and 小煎鸡. c.Send(string(terminal.KeyBackspace)) // Select 小煎鸡. c.SendLine(string(terminal.KeyArrowDown)) c.ExpectEOF() }, core.OptionAnswer{Index: 2, Value: "小煎鸡"}, }, } for _, test := range tests { testName := strings.TrimPrefix(test.name, "SKIP: ") t.Run(testName, func(t *testing.T) { if testName != test.name { t.Skipf("warning: flakey test %q", testName) } RunPromptTest(t, test) }) } } survey-2.3.7/survey.go000066400000000000000000000273031434440262200147520ustar00rootroot00000000000000package survey import ( "bytes" "errors" "io" "os" "strings" "unicode/utf8" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" ) // DefaultAskOptions is the default options on ask, using the OS stdio. func defaultAskOptions() *AskOptions { return &AskOptions{ Stdio: terminal.Stdio{ In: os.Stdin, Out: os.Stdout, Err: os.Stderr, }, PromptConfig: PromptConfig{ PageSize: 7, HelpInput: "?", SuggestInput: "tab", Icons: IconSet{ Error: Icon{ Text: "X", Format: "red", }, Help: Icon{ Text: "?", Format: "cyan", }, Question: Icon{ Text: "?", Format: "green+hb", }, MarkedOption: Icon{ Text: "[x]", Format: "green", }, UnmarkedOption: Icon{ Text: "[ ]", Format: "default+hb", }, SelectFocus: Icon{ Text: ">", Format: "cyan+b", }, }, Filter: func(filter string, value string, index int) (include bool) { filter = strings.ToLower(filter) // include this option if it matches return strings.Contains(strings.ToLower(value), filter) }, KeepFilter: false, ShowCursor: false, RemoveSelectAll: false, RemoveSelectNone: false, HideCharacter: '*', }, } } func defaultPromptConfig() *PromptConfig { return &defaultAskOptions().PromptConfig } func defaultIcons() *IconSet { return &defaultPromptConfig().Icons } // OptionAnswer is an ergonomic alias for core.OptionAnswer type OptionAnswer = core.OptionAnswer // Icon holds the text and format to show for a particular icon type Icon struct { Text string Format string } // IconSet holds the icons to use for various prompts type IconSet struct { HelpInput Icon Error Icon Help Icon Question Icon MarkedOption Icon UnmarkedOption Icon SelectFocus Icon } // Validator is a function passed to a Question after a user has provided a response. // If the function returns an error, then the user will be prompted again for another // response. type Validator func(ans interface{}) error // Transformer is a function passed to a Question after a user has provided a response. // The function can be used to implement a custom logic that will result to return // a different representation of the given answer. // // Look `TransformString`, `ToLower` `Title` and `ComposeTransformers` for more. type Transformer func(ans interface{}) (newAns interface{}) // Question is the core data structure for a survey questionnaire. type Question struct { Name string Prompt Prompt Validate Validator Transform Transformer } // PromptConfig holds the global configuration for a prompt type PromptConfig struct { PageSize int Icons IconSet HelpInput string SuggestInput string Filter func(filter string, option string, index int) bool KeepFilter bool ShowCursor bool RemoveSelectAll bool RemoveSelectNone bool HideCharacter rune } // Prompt is the primary interface for the objects that can take user input // and return a response. type Prompt interface { Prompt(config *PromptConfig) (interface{}, error) Cleanup(*PromptConfig, interface{}) error Error(*PromptConfig, error) error } // PromptAgainer Interface for Prompts that support prompting again after invalid input type PromptAgainer interface { PromptAgain(config *PromptConfig, invalid interface{}, err error) (interface{}, error) } // AskOpt allows setting optional ask options. type AskOpt func(options *AskOptions) error // AskOptions provides additional options on ask. type AskOptions struct { Stdio terminal.Stdio Validators []Validator PromptConfig PromptConfig } // WithStdio specifies the standard input, output and error files survey // interacts with. By default, these are os.Stdin, os.Stdout, and os.Stderr. func WithStdio(in terminal.FileReader, out terminal.FileWriter, err io.Writer) AskOpt { return func(options *AskOptions) error { options.Stdio.In = in options.Stdio.Out = out options.Stdio.Err = err return nil } } // WithFilter specifies the default filter to use when asking questions. func WithFilter(filter func(filter string, value string, index int) (include bool)) AskOpt { return func(options *AskOptions) error { // save the filter internally options.PromptConfig.Filter = filter return nil } } // WithKeepFilter sets the if the filter is kept after selections func WithKeepFilter(KeepFilter bool) AskOpt { return func(options *AskOptions) error { // set the page size options.PromptConfig.KeepFilter = KeepFilter // nothing went wrong return nil } } // WithRemoveSelectAll remove the select all option in Multiselect func WithRemoveSelectAll() AskOpt { return func(options *AskOptions) error { options.PromptConfig.RemoveSelectAll = true return nil } } // WithRemoveSelectNone remove the select none/unselect all in Multiselect func WithRemoveSelectNone() AskOpt { return func(options *AskOptions) error { options.PromptConfig.RemoveSelectNone = true return nil } } // WithValidator specifies a validator to use while prompting the user func WithValidator(v Validator) AskOpt { return func(options *AskOptions) error { // add the provided validator to the list options.Validators = append(options.Validators, v) // nothing went wrong return nil } } type wantsStdio interface { WithStdio(terminal.Stdio) } // WithPageSize sets the default page size used by prompts func WithPageSize(pageSize int) AskOpt { return func(options *AskOptions) error { // set the page size options.PromptConfig.PageSize = pageSize // nothing went wrong return nil } } // WithHelpInput changes the character that prompts look for to give the user helpful information. func WithHelpInput(r rune) AskOpt { return func(options *AskOptions) error { // set the input character options.PromptConfig.HelpInput = string(r) // nothing went wrong return nil } } // WithIcons sets the icons that will be used when prompting the user func WithIcons(setIcons func(*IconSet)) AskOpt { return func(options *AskOptions) error { // update the default icons with whatever the user says setIcons(&options.PromptConfig.Icons) // nothing went wrong return nil } } // WithShowCursor sets the show cursor behavior when prompting the user func WithShowCursor(ShowCursor bool) AskOpt { return func(options *AskOptions) error { // set the page size options.PromptConfig.ShowCursor = ShowCursor // nothing went wrong return nil } } // WithHideCharacter sets the default character shown instead of the password for password inputs func WithHideCharacter(char rune) AskOpt { return func(options *AskOptions) error { // set the hide character options.PromptConfig.HideCharacter = char // nothing went wrong return nil } } /* AskOne performs the prompt for a single prompt and asks for validation if required. Response types should be something that can be casted from the response type designated in the documentation. For example: name := "" prompt := &survey.Input{ Message: "name", } survey.AskOne(prompt, &name) */ func AskOne(p Prompt, response interface{}, opts ...AskOpt) error { err := Ask([]*Question{{Prompt: p}}, response, opts...) if err != nil { return err } return nil } /* Ask performs the prompt loop, asking for validation when appropriate. The response type can be one of two options. If a struct is passed, the answer will be written to the field whose name matches the Name field on the corresponding question. Field types should be something that can be casted from the response type designated in the documentation. Note, a survey tag can also be used to identify a Otherwise, a map[string]interface{} can be passed, responses will be written to the key with the matching name. For example: qs := []*survey.Question{ { Name: "name", Prompt: &survey.Input{Message: "What is your name?"}, Validate: survey.Required, Transform: survey.Title, }, } answers := struct{ Name string }{} err := survey.Ask(qs, &answers) */ func Ask(qs []*Question, response interface{}, opts ...AskOpt) error { // build up the configuration options options := defaultAskOptions() for _, opt := range opts { if opt == nil { continue } if err := opt(options); err != nil { return err } } // if we weren't passed a place to record the answers if response == nil { // we can't go any further return errors.New("cannot call Ask() with a nil reference to record the answers") } validate := func(q *Question, val interface{}) error { if q.Validate != nil { if err := q.Validate(val); err != nil { return err } } for _, v := range options.Validators { if err := v(val); err != nil { return err } } return nil } // go over every question for _, q := range qs { // If Prompt implements controllable stdio, pass in specified stdio. if p, ok := q.Prompt.(wantsStdio); ok { p.WithStdio(options.Stdio) } var ans interface{} var validationErr error // prompt and validation loop for { if validationErr != nil { if err := q.Prompt.Error(&options.PromptConfig, validationErr); err != nil { return err } } var err error if promptAgainer, ok := q.Prompt.(PromptAgainer); ok && validationErr != nil { ans, err = promptAgainer.PromptAgain(&options.PromptConfig, ans, validationErr) } else { ans, err = q.Prompt.Prompt(&options.PromptConfig) } if err != nil { return err } validationErr = validate(q, ans) if validationErr == nil { break } } if q.Transform != nil { // check if we have a transformer available, if so // then try to acquire the new representation of the // answer, if the resulting answer is not nil. if newAns := q.Transform(ans); newAns != nil { ans = newAns } } // tell the prompt to cleanup with the validated value if err := q.Prompt.Cleanup(&options.PromptConfig, ans); err != nil { return err } // add it to the map if err := core.WriteAnswer(response, q.Name, ans); err != nil { return err } } // return the response return nil } // paginate returns a single page of choices given the page size, the total list of // possible choices, and the current selected index in the total list. func paginate(pageSize int, choices []core.OptionAnswer, sel int) ([]core.OptionAnswer, int) { var start, end, cursor int if len(choices) < pageSize { // if we dont have enough options to fill a page start = 0 end = len(choices) cursor = sel } else if sel < pageSize/2 { // if we are in the first half page start = 0 end = pageSize cursor = sel } else if len(choices)-sel-1 < pageSize/2 { // if we are in the last half page start = len(choices) - pageSize end = len(choices) cursor = sel - start } else { // somewhere in the middle above := pageSize / 2 below := pageSize - above cursor = pageSize / 2 start = sel - above end = sel + below } // return the subset we care about and the index return choices[start:end], cursor } type IterableOpts interface { IterateOption(int, core.OptionAnswer) interface{} } func computeCursorOffset(tmpl string, data IterableOpts, opts []core.OptionAnswer, idx, tWidth int) int { tmpls, err := core.GetTemplatePair(tmpl) if err != nil { return 0 } t := tmpls[0] renderOpt := func(ix int, opt core.OptionAnswer) string { var buf bytes.Buffer _ = t.ExecuteTemplate(&buf, "option", data.IterateOption(ix, opt)) return buf.String() } offset := len(opts) - idx for i, o := range opts { if i < idx { continue } renderedOpt := renderOpt(i, o) valWidth := utf8.RuneCount([]byte(renderedOpt)) if valWidth > tWidth { splitCount := valWidth / tWidth if valWidth%tWidth == 0 { splitCount -= 1 } offset += splitCount } } return offset } survey-2.3.7/survey_posix_test.go000066400000000000000000000020061434440262200172240ustar00rootroot00000000000000//go:build !windows // +build !windows package survey import ( "testing" "github.com/AlecAivazis/survey/v2/terminal" expect "github.com/Netflix/go-expect" pseudotty "github.com/creack/pty" "github.com/hinshun/vt10x" ) func RunTest(t *testing.T, procedure func(expectConsole), test func(terminal.Stdio) error) { t.Helper() t.Parallel() pty, tty, err := pseudotty.Open() if err != nil { t.Fatalf("failed to open pseudotty: %v", err) } term := vt10x.New(vt10x.WithWriter(tty)) c, err := expect.NewConsole(expect.WithStdin(pty), expect.WithStdout(term), expect.WithCloser(pty, tty)) if err != nil { t.Fatalf("failed to create console: %v", err) } defer c.Close() donec := make(chan struct{}) go func() { defer close(donec) procedure(&consoleWithErrorHandling{console: c, t: t}) }() stdio := terminal.Stdio{In: c.Tty(), Out: c.Tty(), Err: c.Tty()} if err := test(stdio); err != nil { t.Error(err) } if err := c.Tty().Close(); err != nil { t.Errorf("error closing Tty: %v", err) } <-donec } survey-2.3.7/survey_test.go000066400000000000000000000373551434440262200160210ustar00rootroot00000000000000package survey import ( "errors" "os/exec" "strings" "testing" "time" "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" expect "github.com/Netflix/go-expect" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func init() { // disable color output for all prompts to simplify testing core.DisableColor = true } type expectConsole interface { ExpectString(string) ExpectEOF() SendLine(string) Send(string) } type consoleWithErrorHandling struct { console *expect.Console t *testing.T } func (c *consoleWithErrorHandling) ExpectString(s string) { if _, err := c.console.ExpectString(s); err != nil { c.t.Helper() c.t.Fatalf("ExpectString(%q) = %v", s, err) } } func (c *consoleWithErrorHandling) SendLine(s string) { if _, err := c.console.SendLine(s); err != nil { c.t.Helper() c.t.Fatalf("SendLine(%q) = %v", s, err) } } func (c *consoleWithErrorHandling) Send(s string) { if _, err := c.console.Send(s); err != nil { c.t.Helper() c.t.Fatalf("Send(%q) = %v", s, err) } } func (c *consoleWithErrorHandling) ExpectEOF() { if _, err := c.console.ExpectEOF(); err != nil { c.t.Helper() c.t.Fatalf("ExpectEOF() = %v", err) } } type PromptTest struct { name string prompt Prompt procedure func(expectConsole) expected interface{} } func RunPromptTest(t *testing.T, test PromptTest) { t.Helper() var answer interface{} RunTest(t, test.procedure, func(stdio terminal.Stdio) error { var err error if p, ok := test.prompt.(wantsStdio); ok { p.WithStdio(stdio) } answer, err = test.prompt.Prompt(defaultPromptConfig()) return err }) require.Equal(t, test.expected, answer) } func RunPromptTestKeepFilter(t *testing.T, test PromptTest) { t.Helper() var answer interface{} RunTest(t, test.procedure, func(stdio terminal.Stdio) error { var err error if p, ok := test.prompt.(wantsStdio); ok { p.WithStdio(stdio) } config := defaultPromptConfig() config.KeepFilter = true answer, err = test.prompt.Prompt(config) return err }) require.Equal(t, test.expected, answer) } func RunPromptTestRemoveSelectAll(t *testing.T, test PromptTest) { t.Helper() var answer interface{} RunTest(t, test.procedure, func(stdio terminal.Stdio) error { var err error if p, ok := test.prompt.(wantsStdio); ok { p.WithStdio(stdio) } config := defaultPromptConfig() config.RemoveSelectAll = true answer, err = test.prompt.Prompt(config) return err }) require.Equal(t, test.expected, answer) } func RunPromptTestRemoveSelectNone(t *testing.T, test PromptTest) { t.Helper() var answer interface{} RunTest(t, test.procedure, func(stdio terminal.Stdio) error { var err error if p, ok := test.prompt.(wantsStdio); ok { p.WithStdio(stdio) } config := defaultPromptConfig() config.RemoveSelectNone = true answer, err = test.prompt.Prompt(config) return err }) require.Equal(t, test.expected, answer) } func TestPagination_tooFew(t *testing.T) { // a small list of options choices := core.OptionAnswerList([]string{"choice1", "choice2", "choice3"}) // a page bigger than the total number pageSize := 4 // the current selection sel := 3 // compute the page info page, idx := paginate(pageSize, choices, sel) // make sure we see the full list of options assert.Equal(t, choices, page) // with the second index highlighted (no change) assert.Equal(t, 3, idx) } func TestPagination_firstHalf(t *testing.T) { // the choices for the test choices := core.OptionAnswerList([]string{"choice1", "choice2", "choice3", "choice4", "choice5", "choice6"}) // section the choices into groups of 4 so the choice is somewhere in the middle // to verify there is no displacement of the page pageSize := 4 // test the second item sel := 2 // compute the page info page, idx := paginate(pageSize, choices, sel) // we should see the first three options assert.Equal(t, choices[0:4], page) // with the second index highlighted assert.Equal(t, 2, idx) } func TestPagination_middle(t *testing.T) { // the choices for the test choices := core.OptionAnswerList([]string{"choice0", "choice1", "choice2", "choice3", "choice4", "choice5"}) // section the choices into groups of 3 pageSize := 2 // test the second item so that we can verify we are in the middle of the list sel := 3 // compute the page info page, idx := paginate(pageSize, choices, sel) // we should see the first three options assert.Equal(t, choices[2:4], page) // with the second index highlighted assert.Equal(t, 1, idx) } func TestPagination_lastHalf(t *testing.T) { // the choices for the test choices := core.OptionAnswerList([]string{"choice0", "choice1", "choice2", "choice3", "choice4", "choice5"}) // section the choices into groups of 3 pageSize := 3 // test the last item to verify we're not in the middle sel := 5 // compute the page info page, idx := paginate(pageSize, choices, sel) // we should see the first three options assert.Equal(t, choices[3:6], page) // we should be at the bottom of the list assert.Equal(t, 2, idx) } func TestAsk(t *testing.T) { if _, err := exec.LookPath("vi"); err != nil { t.Skip("vi not found in PATH") } tests := []struct { name string questions []*Question procedure func(expectConsole) expected map[string]interface{} }{ { "Test Ask for all prompts", []*Question{ { Name: "pizza", Prompt: &Confirm{ Message: "Is pizza your favorite food?", }, }, { Name: "commit-message", Prompt: &Editor{ Editor: "vi", Message: "Edit git commit message", }, }, /* TODO gets stuck { Name: "commit-message-validated", Prompt: &Editor{ Editor: "vi", Message: "Edit git commit message", }, Validate: func(v interface{}) error { s := v.(string) if strings.Contains(s, "invalid") { return fmt.Errorf("invalid error message") } return nil }, }, */ { Name: "name", Prompt: &Input{ Message: "What is your name?", }, }, /* TODO gets stuck { Name: "day", Prompt: &MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, }, */ { Name: "password", Prompt: &Password{ Message: "Please type your password", }, }, { Name: "color", Prompt: &Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green", "yellow"}, }, }, }, func(c expectConsole) { // Confirm c.ExpectString("Is pizza your favorite food? (y/N)") c.SendLine("Y") // Editor c.ExpectString("Edit git commit message [Enter to launch editor]") c.SendLine("") time.Sleep(time.Millisecond) c.Send("ccAdd editor prompt tests\x1b") c.SendLine(":wq!") /* TODO gets stuck // Editor validated c.ExpectString("Edit git commit message [Enter to launch editor]") c.SendLine("") time.Sleep(time.Millisecond) c.Send("i invalid input first try\x1b") c.SendLine(":wq!") time.Sleep(time.Millisecond) c.ExpectString("invalid error message") c.ExpectString("Edit git commit message [Enter to launch editor]") c.SendLine("") time.Sleep(time.Millisecond) c.ExpectString("first try") c.Send("ccAdd editor prompt tests, but validated\x1b") c.SendLine(":wq!") */ // Input c.ExpectString("What is your name?") c.SendLine("Johnny Appleseed") /* TODO gets stuck // MultiSelect c.ExpectString("What days do you prefer: [Use arrows to move, space to select, type to filter]") // Select Monday. c.Send(string(terminal.KeyArrowDown)) c.Send(" ") // Select Wednesday. c.Send(string(terminal.KeyArrowDown)) c.Send(string(terminal.KeyArrowDown)) c.SendLine(" ") */ // Password c.ExpectString("Please type your password") c.Send("secret") c.SendLine("") // Select c.ExpectString("Choose a color: [Use arrows to move, type to filter]") c.SendLine("yellow") c.ExpectEOF() }, map[string]interface{}{ "pizza": true, "commit-message": "Add editor prompt tests\n", /* TODO "commit-message-validated": "Add editor prompt tests, but validated\n", */ "name": "Johnny Appleseed", /* TODO "day": []string{"Monday", "Wednesday"}, */ "password": "secret", "color": core.OptionAnswer{Index: 3, Value: "yellow"}, }, }, { "Test Ask with validate survey.Required", []*Question{ { Name: "name", Prompt: &Input{ Message: "What is your name?", }, Validate: Required, }, }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("") c.ExpectString("Sorry, your reply was invalid: Value is required") time.Sleep(time.Millisecond) c.SendLine("Johnny Appleseed") c.ExpectEOF() }, map[string]interface{}{ "name": "Johnny Appleseed", }, }, { "Test Ask with transformer survey.ToLower", []*Question{ { Name: "name", Prompt: &Input{ Message: "What is your name?", }, Transform: ToLower, }, }, func(c expectConsole) { c.ExpectString("What is your name?") c.SendLine("Johnny Appleseed") c.ExpectString("What is your name? johnny appleseed") c.ExpectEOF() }, map[string]interface{}{ "name": "johnny appleseed", }, }, } for _, test := range tests { // Capture range variable. test := test t.Run(test.name, func(t *testing.T) { answers := make(map[string]interface{}) RunTest(t, test.procedure, func(stdio terminal.Stdio) error { return Ask(test.questions, &answers, WithStdio(stdio.In, stdio.Out, stdio.Err)) }) require.Equal(t, test.expected, answers) }) } } func TestAsk_returnsErrorIfTargetIsNil(t *testing.T) { // pass an empty place to leave the answers err := Ask([]*Question{}, nil) // if we didn't get an error if err == nil { // the test failed t.Error("Did not encounter error when asking with no where to record.") } } func Test_computeCursorOffset_MultiSelect(t *testing.T) { tests := []struct { name string ix int opts []core.OptionAnswer termWidth int want int }{ { name: "no opts", ix: 0, opts: []core.OptionAnswer{}, want: 0, }, { name: "one opt", ix: 0, opts: core.OptionAnswerList([]string{"one"}), want: 1, }, { name: "multiple opt", opts: core.OptionAnswerList([]string{"one", "two"}), ix: 0, want: 2, }, { name: "first choice", opts: core.OptionAnswerList([]string{"one", "two", "three", "four", "five"}), ix: 0, want: 5, }, { name: "mid choice", opts: core.OptionAnswerList([]string{"one", "two", "three", "four", "five"}), ix: 2, want: 3, }, { name: "last choice", opts: core.OptionAnswerList([]string{"one", "two", "three", "four", "five"}), ix: 4, want: 1, }, { name: "wide choices, uneven", opts: core.OptionAnswerList([]string{ "wide one wide one wide one", "two", "three", "wide four wide four wide four", "five", "six"}), termWidth: 20, ix: 0, want: 8, }, { name: "wide choices, even", opts: core.OptionAnswerList([]string{ "wide one wide one wide one", "two", "three", "012345678901", "five", "six"}), termWidth: 20, ix: 0, want: 7, }, { name: "wide choices, wide before idx", opts: core.OptionAnswerList([]string{ "wide one wide one wide one", "wide two wide two wide two", "three", "four", "five", "six"}), termWidth: 20, ix: 2, want: 4, }, } for _, tt := range tests { if tt.termWidth == 0 { tt.termWidth = 100 } tmpl := MultiSelectQuestionTemplate data := MultiSelectTemplateData{ SelectedIndex: tt.ix, Config: defaultPromptConfig(), } t.Run(tt.name, func(t *testing.T) { if got := computeCursorOffset(tmpl, data, tt.opts, tt.ix, tt.termWidth); got != tt.want { t.Errorf("computeCursorOffset() = %v, want %v", got, tt.want) } }) } } func Test_computeCursorOffset_Select(t *testing.T) { tests := []struct { name string ix int opts []core.OptionAnswer termWidth int want int }{ { name: "no opts", ix: 0, opts: []core.OptionAnswer{}, want: 0, }, { name: "one opt", ix: 0, opts: core.OptionAnswerList([]string{"one"}), want: 1, }, { name: "multiple opt", opts: core.OptionAnswerList([]string{"one", "two"}), ix: 0, want: 2, }, { name: "first choice", opts: core.OptionAnswerList([]string{"one", "two", "three", "four", "five"}), ix: 0, want: 5, }, { name: "mid choice", opts: core.OptionAnswerList([]string{"one", "two", "three", "four", "five"}), ix: 2, want: 3, }, { name: "last choice", opts: core.OptionAnswerList([]string{"one", "two", "three", "four", "five"}), ix: 4, want: 1, }, { name: "wide choices, uneven", opts: core.OptionAnswerList([]string{ "wide one wide one wide one", "two", "three", "wide four wide four wide four", "five", "six"}), termWidth: 20, ix: 0, want: 8, }, { name: "wide choices, even", opts: core.OptionAnswerList([]string{ "wide one wide one wide one", "two", "three", "01234567890123456", "five", "six"}), termWidth: 20, ix: 0, want: 7, }, { name: "wide choices, wide before idx", opts: core.OptionAnswerList([]string{ "wide one wide one wide one", "wide two wide two wide two", "three", "four", "five", "six"}), termWidth: 20, ix: 2, want: 4, }, } for _, tt := range tests { if tt.termWidth == 0 { tt.termWidth = 100 } tmpl := SelectQuestionTemplate data := SelectTemplateData{ SelectedIndex: tt.ix, Config: defaultPromptConfig(), } t.Run(tt.name, func(t *testing.T) { if got := computeCursorOffset(tmpl, data, tt.opts, tt.ix, tt.termWidth); got != tt.want { t.Errorf("computeCursorOffset() = %v, want %v", got, tt.want) } }) } } func TestAsk_Validation(t *testing.T) { p := &mockPrompt{ answers: []string{"", "company", "COM", "com"}, } var res struct { TLDN string } err := Ask([]*Question{ { Name: "TLDN", Prompt: p, Validate: func(v interface{}) error { s := v.(string) if strings.ToLower(s) != s { return errors.New("value contains uppercase characters") } return nil }, }, }, &res, WithValidator(MinLength(1)), WithValidator(MaxLength(5))) if err != nil { t.Fatalf("Ask() = %v", err) } if res.TLDN != "com" { t.Errorf("answer: %q, want %q", res.TLDN, "com") } if p.cleanups != 1 { t.Errorf("cleanups: %d, want %d", p.cleanups, 1) } if err1 := p.printedErrors[0].Error(); err1 != "value is too short. Min length is 1" { t.Errorf("printed error 1: %q, want %q", err1, "value is too short. Min length is 1") } if err2 := p.printedErrors[1].Error(); err2 != "value is too long. Max length is 5" { t.Errorf("printed error 2: %q, want %q", err2, "value is too long. Max length is 5") } if err3 := p.printedErrors[2].Error(); err3 != "value contains uppercase characters" { t.Errorf("printed error 2: %q, want %q", err3, "value contains uppercase characters") } } type mockPrompt struct { index int answers []string cleanups int printedErrors []error } func (p *mockPrompt) Prompt(*PromptConfig) (interface{}, error) { if p.index >= len(p.answers) { return nil, errors.New("no more answers") } val := p.answers[p.index] p.index++ return val, nil } func (p *mockPrompt) Cleanup(*PromptConfig, interface{}) error { p.cleanups++ return nil } func (p *mockPrompt) Error(_ *PromptConfig, err error) error { p.printedErrors = append(p.printedErrors, err) return nil } survey-2.3.7/survey_windows_test.go000066400000000000000000000003611434440262200175560ustar00rootroot00000000000000package survey import ( "testing" "github.com/AlecAivazis/survey/v2/terminal" ) func RunTest(t *testing.T, procedure func(expectConsole), test func(terminal.Stdio) error) { t.Skip("warning: Windows does not support psuedoterminals") } survey-2.3.7/terminal/000077500000000000000000000000001434440262200146745ustar00rootroot00000000000000survey-2.3.7/terminal/LICENSE.txt000066400000000000000000000021061434440262200165160ustar00rootroot00000000000000Copyright (c) 2014 Takashi Kokubun MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. survey-2.3.7/terminal/README.md000066400000000000000000000002461434440262200161550ustar00rootroot00000000000000# survey/terminal This package started as a copy of [kokuban/go-ansi](http://github.com/k0kubun/go-ansi) but has since been modified to fit survey's specific needs. survey-2.3.7/terminal/buffered_reader.go000066400000000000000000000005041434440262200203260ustar00rootroot00000000000000package terminal import ( "bytes" "io" ) type BufferedReader struct { In io.Reader Buffer *bytes.Buffer } func (br *BufferedReader) Read(p []byte) (int, error) { n, err := br.Buffer.Read(p) if err != nil && err != io.EOF { return n, err } else if err == nil { return n, nil } return br.In.Read(p[n:]) } survey-2.3.7/terminal/cursor.go000066400000000000000000000121541434440262200165430ustar00rootroot00000000000000//go:build !windows // +build !windows package terminal import ( "bufio" "bytes" "fmt" "io" "regexp" "strconv" ) var COORDINATE_SYSTEM_BEGIN Short = 1 var dsrPattern = regexp.MustCompile(`\x1b\[(\d+);(\d+)R$`) type Cursor struct { In FileReader Out FileWriter } // Up moves the cursor n cells to up. func (c *Cursor) Up(n int) error { _, err := fmt.Fprintf(c.Out, "\x1b[%dA", n) return err } // Down moves the cursor n cells to down. func (c *Cursor) Down(n int) error { _, err := fmt.Fprintf(c.Out, "\x1b[%dB", n) return err } // Forward moves the cursor n cells to right. func (c *Cursor) Forward(n int) error { _, err := fmt.Fprintf(c.Out, "\x1b[%dC", n) return err } // Back moves the cursor n cells to left. func (c *Cursor) Back(n int) error { _, err := fmt.Fprintf(c.Out, "\x1b[%dD", n) return err } // NextLine moves cursor to beginning of the line n lines down. func (c *Cursor) NextLine(n int) error { if err := c.Down(1); err != nil { return err } return c.HorizontalAbsolute(0) } // PreviousLine moves cursor to beginning of the line n lines up. func (c *Cursor) PreviousLine(n int) error { if err := c.Up(1); err != nil { return err } return c.HorizontalAbsolute(0) } // HorizontalAbsolute moves cursor horizontally to x. func (c *Cursor) HorizontalAbsolute(x int) error { _, err := fmt.Fprintf(c.Out, "\x1b[%dG", x) return err } // Show shows the cursor. func (c *Cursor) Show() error { _, err := fmt.Fprint(c.Out, "\x1b[?25h") return err } // Hide hide the cursor. func (c *Cursor) Hide() error { _, err := fmt.Fprint(c.Out, "\x1b[?25l") return err } // move moves the cursor to a specific x,y location. func (c *Cursor) move(x int, y int) error { _, err := fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y) return err } // Save saves the current position func (c *Cursor) Save() error { _, err := fmt.Fprint(c.Out, "\x1b7") return err } // Restore restores the saved position of the cursor func (c *Cursor) Restore() error { _, err := fmt.Fprint(c.Out, "\x1b8") return err } // for comparability purposes between windows // in unix we need to print out a new line on some terminals func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error { if cur.Y == terminalSize.Y { if _, err := fmt.Fprintln(c.Out); err != nil { return err } } return c.NextLine(1) } // Location returns the current location of the cursor in the terminal func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) { // ANSI escape sequence for DSR - Device Status Report // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences if _, err := fmt.Fprint(c.Out, "\x1b[6n"); err != nil { return nil, err } // There may be input in Stdin prior to CursorLocation so make sure we don't // drop those bytes. var loc []int var match string for loc == nil { // Reports the cursor position (CPR) to the application as (as though typed at // the keyboard) ESC[n;mR, where n is the row and m is the column. reader := bufio.NewReader(c.In) text, err := reader.ReadSlice(byte('R')) if err != nil { return nil, err } loc = dsrPattern.FindStringIndex(string(text)) if loc == nil { // After reading slice to byte 'R', the bufio Reader may have read more // bytes into its internal buffer which will be discarded on next ReadSlice. // We create a temporary buffer to read the remaining buffered slice and // write them to output buffer. buffered := make([]byte, reader.Buffered()) _, err = io.ReadFull(reader, buffered) if err != nil { return nil, err } // Stdin contains R that doesn't match DSR, so pass the bytes along to // output buffer. buf.Write(text) buf.Write(buffered) } else { // Write the non-matching leading bytes to output buffer. buf.Write(text[:loc[0]]) // Save the matching bytes to extract the row and column of the cursor. match = string(text[loc[0]:loc[1]]) } } matches := dsrPattern.FindStringSubmatch(string(match)) if len(matches) != 3 { return nil, fmt.Errorf("incorrect number of matches: %d", len(matches)) } col, err := strconv.Atoi(matches[2]) if err != nil { return nil, err } row, err := strconv.Atoi(matches[1]) if err != nil { return nil, err } return &Coord{Short(col), Short(row)}, nil } func (cur Coord) CursorIsAtLineEnd(size *Coord) bool { return cur.X == size.X } func (cur Coord) CursorIsAtLineBegin() bool { return cur.X == COORDINATE_SYSTEM_BEGIN } // Size returns the height and width of the terminal. func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) { // the general approach here is to move the cursor to the very bottom // of the terminal, ask for the current location and then move the // cursor back where we started // hide the cursor (so it doesn't blink when getting the size of the terminal) c.Hide() defer c.Show() // save the current location of the cursor c.Save() defer c.Restore() // move the cursor to the very bottom of the terminal c.move(999, 999) // ask for the current location bottom, err := c.Location(buf) if err != nil { return nil, err } // since the bottom was calculated in the lower right corner, it // is the dimensions we are looking for return bottom, nil } survey-2.3.7/terminal/cursor_windows.go000066400000000000000000000077501434440262200203230ustar00rootroot00000000000000package terminal import ( "bytes" "syscall" "unsafe" ) var COORDINATE_SYSTEM_BEGIN Short = 0 // shared variable to save the cursor location from CursorSave() var cursorLoc Coord type Cursor struct { In FileReader Out FileWriter } func (c *Cursor) Up(n int) error { return c.cursorMove(0, n) } func (c *Cursor) Down(n int) error { return c.cursorMove(0, -1*n) } func (c *Cursor) Forward(n int) error { return c.cursorMove(n, 0) } func (c *Cursor) Back(n int) error { return c.cursorMove(-1*n, 0) } // save the cursor location func (c *Cursor) Save() error { loc, err := c.Location(nil) if err != nil { return err } cursorLoc = *loc return nil } func (c *Cursor) Restore() error { handle := syscall.Handle(c.Out.Fd()) // restore it to the original position _, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc)))) return normalizeError(err) } func (cur Coord) CursorIsAtLineEnd(size *Coord) bool { return cur.X == size.X } func (cur Coord) CursorIsAtLineBegin() bool { return cur.X == 0 } func (c *Cursor) cursorMove(x int, y int) error { handle := syscall.Handle(c.Out.Fd()) var csbi consoleScreenBufferInfo if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil { return err } var cursor Coord cursor.X = csbi.cursorPosition.X + Short(x) cursor.Y = csbi.cursorPosition.Y + Short(y) _, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor)))) return normalizeError(err) } func (c *Cursor) NextLine(n int) error { if err := c.Up(n); err != nil { return err } return c.HorizontalAbsolute(0) } func (c *Cursor) PreviousLine(n int) error { if err := c.Down(n); err != nil { return err } return c.HorizontalAbsolute(0) } // for comparability purposes between windows // in windows we don't have to print out a new line func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error { return c.NextLine(1) } func (c *Cursor) HorizontalAbsolute(x int) error { handle := syscall.Handle(c.Out.Fd()) var csbi consoleScreenBufferInfo if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil { return err } var cursor Coord cursor.X = Short(x) cursor.Y = csbi.cursorPosition.Y if csbi.size.X < cursor.X { cursor.X = csbi.size.X } _, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor)))) return normalizeError(err) } func (c *Cursor) Show() error { handle := syscall.Handle(c.Out.Fd()) var cci consoleCursorInfo if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil { return err } cci.visible = 1 _, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) return normalizeError(err) } func (c *Cursor) Hide() error { handle := syscall.Handle(c.Out.Fd()) var cci consoleCursorInfo if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil { return err } cci.visible = 0 _, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) return normalizeError(err) } func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) { handle := syscall.Handle(c.Out.Fd()) var csbi consoleScreenBufferInfo if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil { return nil, err } return &csbi.cursorPosition, nil } func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) { handle := syscall.Handle(c.Out.Fd()) var csbi consoleScreenBufferInfo if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil { return nil, err } // windows' coordinate system begins at (0, 0) csbi.size.X-- csbi.size.Y-- return &csbi.size, nil } survey-2.3.7/terminal/display.go000066400000000000000000000001731434440262200166710ustar00rootroot00000000000000package terminal type EraseLineMode int const ( ERASE_LINE_END EraseLineMode = iota ERASE_LINE_START ERASE_LINE_ALL ) survey-2.3.7/terminal/display_posix.go000066400000000000000000000003041434440262200201070ustar00rootroot00000000000000//go:build !windows // +build !windows package terminal import ( "fmt" ) func EraseLine(out FileWriter, mode EraseLineMode) error { _, err := fmt.Fprintf(out, "\x1b[%dK", mode) return err } survey-2.3.7/terminal/display_windows.go000066400000000000000000000013331434440262200204420ustar00rootroot00000000000000package terminal import ( "syscall" "unsafe" ) func EraseLine(out FileWriter, mode EraseLineMode) error { handle := syscall.Handle(out.Fd()) var csbi consoleScreenBufferInfo if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil { return err } var w uint32 var x Short cursor := csbi.cursorPosition switch mode { case ERASE_LINE_END: x = csbi.size.X case ERASE_LINE_START: x = 0 case ERASE_LINE_ALL: cursor.X = 0 x = csbi.size.X } _, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w))) return normalizeError(err) } survey-2.3.7/terminal/error.go000066400000000000000000000002331434440262200163520ustar00rootroot00000000000000package terminal import ( "errors" ) var ( //lint:ignore ST1012 keeping old name for backwards compatibility InterruptErr = errors.New("interrupt") ) survey-2.3.7/terminal/output.go000066400000000000000000000006711434440262200165670ustar00rootroot00000000000000//go:build !windows // +build !windows package terminal import ( "io" ) // NewAnsiStdout returns special stdout, which converts escape sequences to Windows API calls // on Windows environment. func NewAnsiStdout(out FileWriter) io.Writer { return out } // NewAnsiStderr returns special stderr, which converts escape sequences to Windows API calls // on Windows environment. func NewAnsiStderr(out FileWriter) io.Writer { return out } survey-2.3.7/terminal/output_windows.go000066400000000000000000000121271434440262200203400ustar00rootroot00000000000000package terminal import ( "bytes" "fmt" "io" "strconv" "strings" "syscall" "unsafe" "github.com/mattn/go-isatty" ) const ( foregroundBlue = 0x1 foregroundGreen = 0x2 foregroundRed = 0x4 foregroundIntensity = 0x8 foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity) backgroundBlue = 0x10 backgroundGreen = 0x20 backgroundRed = 0x40 backgroundIntensity = 0x80 backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) ) type Writer struct { out FileWriter handle syscall.Handle orgAttr word } func NewAnsiStdout(out FileWriter) io.Writer { var csbi consoleScreenBufferInfo if !isatty.IsTerminal(out.Fd()) { return out } handle := syscall.Handle(out.Fd()) procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) return &Writer{out: out, handle: handle, orgAttr: csbi.attributes} } func NewAnsiStderr(out FileWriter) io.Writer { var csbi consoleScreenBufferInfo if !isatty.IsTerminal(out.Fd()) { return out } handle := syscall.Handle(out.Fd()) procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) return &Writer{out: out, handle: handle, orgAttr: csbi.attributes} } func (w *Writer) Write(data []byte) (n int, err error) { r := bytes.NewReader(data) for { var ch rune var size int ch, size, err = r.ReadRune() if err != nil { if err == io.EOF { err = nil } return } n += size switch ch { case '\x1b': size, err = w.handleEscape(r) n += size if err != nil { return } default: _, err = fmt.Fprint(w.out, string(ch)) if err != nil { return } } } } func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) { buf := make([]byte, 0, 10) buf = append(buf, "\x1b"...) var ch rune var size int // Check '[' continues after \x1b ch, size, err = r.ReadRune() if err != nil { if err == io.EOF { err = nil } fmt.Fprint(w.out, string(buf)) return } n += size if ch != '[' { fmt.Fprint(w.out, string(buf)) return } // Parse escape code var code rune argBuf := make([]byte, 0, 10) for { ch, size, err = r.ReadRune() if err != nil { if err == io.EOF { err = nil } fmt.Fprint(w.out, string(buf)) return } n += size if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') { code = ch break } argBuf = append(argBuf, string(ch)...) } err = w.applyEscapeCode(buf, string(argBuf), code) return } func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) error { c := &Cursor{Out: w.out} switch arg + string(code) { case "?25h": return c.Show() case "?25l": return c.Hide() } if code >= 'A' && code <= 'G' { if n, err := strconv.Atoi(arg); err == nil { switch code { case 'A': return c.Up(n) case 'B': return c.Down(n) case 'C': return c.Forward(n) case 'D': return c.Back(n) case 'E': return c.NextLine(n) case 'F': return c.PreviousLine(n) case 'G': return c.HorizontalAbsolute(n) } } } switch code { case 'm': return w.applySelectGraphicRendition(arg) default: buf = append(buf, string(code)...) _, err := fmt.Fprint(w.out, string(buf)) return err } } // Original implementation: https://github.com/mattn/go-colorable func (w *Writer) applySelectGraphicRendition(arg string) error { if arg == "" { _, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr)) return normalizeError(err) } var csbi consoleScreenBufferInfo if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil { return err } attr := csbi.attributes for _, param := range strings.Split(arg, ";") { n, err := strconv.Atoi(param) if err != nil { continue } switch { case n == 0 || n == 100: attr = w.orgAttr case 1 <= n && n <= 5: attr |= foregroundIntensity case 30 <= n && n <= 37: attr = (attr & backgroundMask) if (n-30)&1 != 0 { attr |= foregroundRed } if (n-30)&2 != 0 { attr |= foregroundGreen } if (n-30)&4 != 0 { attr |= foregroundBlue } case 40 <= n && n <= 47: attr = (attr & foregroundMask) if (n-40)&1 != 0 { attr |= backgroundRed } if (n-40)&2 != 0 { attr |= backgroundGreen } if (n-40)&4 != 0 { attr |= backgroundBlue } case 90 <= n && n <= 97: attr = (attr & backgroundMask) attr |= foregroundIntensity if (n-90)&1 != 0 { attr |= foregroundRed } if (n-90)&2 != 0 { attr |= foregroundGreen } if (n-90)&4 != 0 { attr |= foregroundBlue } case 100 <= n && n <= 107: attr = (attr & foregroundMask) attr |= backgroundIntensity if (n-100)&1 != 0 { attr |= backgroundRed } if (n-100)&2 != 0 { attr |= backgroundGreen } if (n-100)&4 != 0 { attr |= backgroundBlue } } } _, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr)) return normalizeError(err) } func normalizeError(err error) error { if syserr, ok := err.(syscall.Errno); ok && syserr == 0 { return nil } return err } survey-2.3.7/terminal/runereader.go000066400000000000000000000262031434440262200173620ustar00rootroot00000000000000package terminal import ( "fmt" "unicode" "golang.org/x/text/width" ) type RuneReader struct { stdio Stdio state runeReaderState } func NewRuneReader(stdio Stdio) *RuneReader { return &RuneReader{ stdio: stdio, state: newRuneReaderState(stdio.In), } } func (rr *RuneReader) printChar(char rune, mask rune) error { // if we don't need to mask the input if mask == 0 { // just print the character the user pressed _, err := fmt.Fprintf(rr.stdio.Out, "%c", char) return err } // otherwise print the mask we were given _, err := fmt.Fprintf(rr.stdio.Out, "%c", mask) return err } type OnRuneFn func(rune, []rune) ([]rune, bool, error) func (rr *RuneReader) ReadLine(mask rune, onRunes ...OnRuneFn) ([]rune, error) { return rr.ReadLineWithDefault(mask, []rune{}, onRunes...) } func (rr *RuneReader) ReadLineWithDefault(mask rune, d []rune, onRunes ...OnRuneFn) ([]rune, error) { line := []rune{} // we only care about horizontal displacements from the origin so start counting at 0 index := 0 cursor := &Cursor{ In: rr.stdio.In, Out: rr.stdio.Out, } onRune := func(r rune, line []rune) ([]rune, bool, error) { return line, false, nil } // if the user pressed a key the caller was interested in capturing if len(onRunes) > 0 { onRune = onRunes[0] } // we get the terminal width and height (if resized after this point the property might become invalid) terminalSize, _ := cursor.Size(rr.Buffer()) // we set the current location of the cursor once cursorCurrent, _ := cursor.Location(rr.Buffer()) increment := func() { if cursorCurrent.CursorIsAtLineEnd(terminalSize) { cursorCurrent.X = COORDINATE_SYSTEM_BEGIN cursorCurrent.Y++ } else { cursorCurrent.X++ } } decrement := func() { if cursorCurrent.CursorIsAtLineBegin() { cursorCurrent.X = terminalSize.X cursorCurrent.Y-- } else { cursorCurrent.X-- } } if len(d) > 0 { index = len(d) if _, err := fmt.Fprint(rr.stdio.Out, string(d)); err != nil { return d, err } line = d for range d { increment() } } for { // wait for some input r, _, err := rr.ReadRune() if err != nil { return line, err } if l, stop, err := onRune(r, line); stop || err != nil { return l, err } // if the user pressed enter or some other newline/termination like ctrl+d if r == '\r' || r == '\n' || r == KeyEndTransmission { // delete what's printed out on the console screen (cleanup) for index > 0 { if cursorCurrent.CursorIsAtLineBegin() { EraseLine(rr.stdio.Out, ERASE_LINE_END) cursor.PreviousLine(1) cursor.Forward(int(terminalSize.X)) } else { cursor.Back(1) } decrement() index-- } // move the cursor the a new line cursor.MoveNextLine(cursorCurrent, terminalSize) // we're done processing the input return line, nil } // if the user interrupts (ie with ctrl+c) if r == KeyInterrupt { // go to the beginning of the next line if _, err := fmt.Fprint(rr.stdio.Out, "\r\n"); err != nil { return line, err } // we're done processing the input, and treat interrupt like an error return line, InterruptErr } // allow for backspace/delete editing of inputs if r == KeyBackspace || r == KeyDelete { // and we're not at the beginning of the line if index > 0 && len(line) > 0 { // if we are at the end of the word if index == len(line) { // just remove the last letter from the internal representation // also count the number of cells the rune before the cursor occupied cells := runeWidth(line[len(line)-1]) line = line[:len(line)-1] // go back one if cursorCurrent.X == 1 { cursor.PreviousLine(1) cursor.Forward(int(terminalSize.X)) } else { cursor.Back(cells) } // clear the rest of the line EraseLine(rr.stdio.Out, ERASE_LINE_END) } else { // we need to remove a character from the middle of the word cells := runeWidth(line[index-1]) // remove the current index from the list line = append(line[:index-1], line[index:]...) // save the current position of the cursor, as we have to move the cursor one back to erase the current symbol // and then move the cursor for each symbol in line[index-1:] to print it out, afterwards we want to restore // the cursor to its previous location. cursor.Save() // clear the rest of the line cursor.Back(cells) // print what comes after for _, char := range line[index-1:] { //Erase symbols which are left over from older print EraseLine(rr.stdio.Out, ERASE_LINE_END) // print characters to the new line appropriately if err := rr.printChar(char, mask); err != nil { return line, err } } // erase what's left over from last print if cursorCurrent.Y < terminalSize.Y { cursor.NextLine(1) EraseLine(rr.stdio.Out, ERASE_LINE_END) } // restore cursor cursor.Restore() if cursorCurrent.CursorIsAtLineBegin() { cursor.PreviousLine(1) cursor.Forward(int(terminalSize.X)) } else { cursor.Back(cells) } } // decrement the index index-- decrement() } else { // otherwise the user pressed backspace while at the beginning of the line _ = soundBell(rr.stdio.Out) } // we're done processing this key continue } // if the left arrow is pressed if r == KeyArrowLeft { // if we have space to the left if index > 0 { //move the cursor to the prev line if necessary if cursorCurrent.CursorIsAtLineBegin() { cursor.PreviousLine(1) cursor.Forward(int(terminalSize.X)) } else { cursor.Back(runeWidth(line[index-1])) } //decrement the index index-- decrement() } else { // otherwise we are at the beginning of where we started reading lines // sound the bell _ = soundBell(rr.stdio.Out) } // we're done processing this key press continue } // if the right arrow is pressed if r == KeyArrowRight { // if we have space to the right if index < len(line) { // move the cursor to the next line if necessary if cursorCurrent.CursorIsAtLineEnd(terminalSize) { cursor.NextLine(1) } else { cursor.Forward(runeWidth(line[index])) } index++ increment() } else { // otherwise we are at the end of the word and can't go past // sound the bell _ = soundBell(rr.stdio.Out) } // we're done processing this key press continue } // the user pressed one of the special keys if r == SpecialKeyHome { for index > 0 { if cursorCurrent.CursorIsAtLineBegin() { cursor.PreviousLine(1) cursor.Forward(int(terminalSize.X)) cursorCurrent.Y-- cursorCurrent.X = terminalSize.X } else { cursor.Back(runeWidth(line[index-1])) cursorCurrent.X -= Short(runeWidth(line[index-1])) } index-- } continue // user pressed end } else if r == SpecialKeyEnd { for index != len(line) { if cursorCurrent.CursorIsAtLineEnd(terminalSize) { cursor.NextLine(1) cursorCurrent.Y++ cursorCurrent.X = COORDINATE_SYSTEM_BEGIN } else { cursor.Forward(runeWidth(line[index])) cursorCurrent.X += Short(runeWidth(line[index])) } index++ } continue // user pressed forward delete key } else if r == SpecialKeyDelete { // if index at the end of the line nothing to delete if index != len(line) { // save the current position of the cursor, as we have to erase the current symbol // and then move the cursor for each symbol in line[index:] to print it out, afterwards we want to restore // the cursor to its previous location. cursor.Save() // remove the symbol after the cursor line = append(line[:index], line[index+1:]...) // print the updated line for _, char := range line[index:] { EraseLine(rr.stdio.Out, ERASE_LINE_END) // print out the character if err := rr.printChar(char, mask); err != nil { return line, err } } // erase what's left on last line if cursorCurrent.Y < terminalSize.Y { cursor.NextLine(1) EraseLine(rr.stdio.Out, ERASE_LINE_END) } // restore cursor cursor.Restore() if len(line) == 0 || index == len(line) { EraseLine(rr.stdio.Out, ERASE_LINE_END) } } continue } // if the letter is another escape sequence if unicode.IsControl(r) || r == IgnoreKey { // ignore it continue } // the user pressed a regular key // if we are at the end of the line if index == len(line) { // just append the character at the end of the line line = append(line, r) // save the location of the cursor index++ increment() // print out the character if err := rr.printChar(r, mask); err != nil { return line, err } } else { // we are in the middle of the word so we need to insert the character the user pressed line = append(line[:index], append([]rune{r}, line[index:]...)...) // save the current position of the cursor, as we have to move the cursor back to erase the current symbol // and then move for each symbol in line[index:] to print it out, afterwards we want to restore // cursor's location to its previous one. cursor.Save() EraseLine(rr.stdio.Out, ERASE_LINE_END) // remove the symbol after the cursor // print the updated line for _, char := range line[index:] { EraseLine(rr.stdio.Out, ERASE_LINE_END) // print out the character if err := rr.printChar(char, mask); err != nil { return line, err } increment() } // if we are at the last line, we want to visually insert a new line and append to it. if cursorCurrent.CursorIsAtLineEnd(terminalSize) && cursorCurrent.Y == terminalSize.Y { // add a new line to the terminal if _, err := fmt.Fprintln(rr.stdio.Out); err != nil { return line, err } // restore the position of the cursor horizontally cursor.Restore() // restore the position of the cursor vertically cursor.PreviousLine(1) } else { // restore cursor cursor.Restore() } // check if cursor needs to move to next line cursorCurrent, _ = cursor.Location(rr.Buffer()) if cursorCurrent.CursorIsAtLineEnd(terminalSize) { cursor.NextLine(1) } else { cursor.Forward(runeWidth(r)) } // increment the index index++ increment() } } } // runeWidth returns the number of columns spanned by a rune when printed to the terminal func runeWidth(r rune) int { switch width.LookupRune(r).Kind() { case width.EastAsianWide, width.EastAsianFullwidth: return 2 } if !unicode.IsPrint(r) { return 0 } return 1 } // isAnsiMarker returns if a rune denotes the start of an ANSI sequence func isAnsiMarker(r rune) bool { return r == '\x1B' } // isAnsiTerminator returns if a rune denotes the end of an ANSI sequence func isAnsiTerminator(r rune) bool { return (r >= 0x40 && r <= 0x5a) || (r == 0x5e) || (r >= 0x60 && r <= 0x7e) } // StringWidth returns the visible width of a string when printed to the terminal func StringWidth(str string) int { w := 0 ansi := false for _, r := range str { // increase width only when outside of ANSI escape sequences if ansi || isAnsiMarker(r) { ansi = !isAnsiTerminator(r) } else { w += runeWidth(r) } } return w } survey-2.3.7/terminal/runereader_bsd.go000066400000000000000000000007411434440262200202110ustar00rootroot00000000000000// copied from: https://github.com/golang/crypto/blob/master/ssh/terminal/util_bsd.go // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build darwin || dragonfly || freebsd || netbsd || openbsd // +build darwin dragonfly freebsd netbsd openbsd package terminal import "syscall" const ioctlReadTermios = syscall.TIOCGETA const ioctlWriteTermios = syscall.TIOCSETA survey-2.3.7/terminal/runereader_linux.go000066400000000000000000000011261434440262200205760ustar00rootroot00000000000000// copied from https://github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build linux && !ppc64le // +build linux,!ppc64le package terminal // These constants are declared here, rather than importing // them from the syscall package as some syscall packages, even // on linux, for example gccgo, do not declare them. const ioctlReadTermios = 0x5401 // syscall.TCGETS const ioctlWriteTermios = 0x5402 // syscall.TCSETS survey-2.3.7/terminal/runereader_posix.go000066400000000000000000000065031434440262200206050ustar00rootroot00000000000000//go:build !windows // +build !windows // The terminal mode manipulation code is derived heavily from: // https://github.com/golang/crypto/blob/master/ssh/terminal/util.go: // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package terminal import ( "bufio" "bytes" "fmt" "syscall" "unsafe" ) const ( normalKeypad = '[' applicationKeypad = 'O' ) type runeReaderState struct { term syscall.Termios reader *bufio.Reader buf *bytes.Buffer } func newRuneReaderState(input FileReader) runeReaderState { buf := new(bytes.Buffer) return runeReaderState{ reader: bufio.NewReader(&BufferedReader{ In: input, Buffer: buf, }), buf: buf, } } func (rr *RuneReader) Buffer() *bytes.Buffer { return rr.state.buf } // For reading runes we just want to disable echo. func (rr *RuneReader) SetTermMode() error { if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 { return err } newState := rr.state.term newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG // Because we are clearing canonical mode, we need to ensure VMIN & VTIME are // set to the values we expect. This combination puts things in standard // "blocking read" mode (see termios(3)). newState.Cc[syscall.VMIN] = 1 newState.Cc[syscall.VTIME] = 0 if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { return err } return nil } func (rr *RuneReader) RestoreTermMode() error { if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 { return err } return nil } // ReadRune Parse escape sequences such as ESC [ A for arrow keys. // See https://vt100.net/docs/vt102-ug/appendixc.html func (rr *RuneReader) ReadRune() (rune, int, error) { r, size, err := rr.state.reader.ReadRune() if err != nil { return r, size, err } if r != KeyEscape { return r, size, err } if rr.state.reader.Buffered() == 0 { // no more characters so must be `Esc` key return KeyEscape, 1, nil } r, size, err = rr.state.reader.ReadRune() if err != nil { return r, size, err } // ESC O ... or ESC [ ...? if r != normalKeypad && r != applicationKeypad { return r, size, fmt.Errorf("unexpected escape sequence from terminal: %q", []rune{KeyEscape, r}) } keypad := r r, size, err = rr.state.reader.ReadRune() if err != nil { return r, size, err } switch r { case 'A': // ESC [ A or ESC O A return KeyArrowUp, 1, nil case 'B': // ESC [ B or ESC O B return KeyArrowDown, 1, nil case 'C': // ESC [ C or ESC O C return KeyArrowRight, 1, nil case 'D': // ESC [ D or ESC O D return KeyArrowLeft, 1, nil case 'F': // ESC [ F or ESC O F return SpecialKeyEnd, 1, nil case 'H': // ESC [ H or ESC O H return SpecialKeyHome, 1, nil case '3': // ESC [ 3 if keypad == normalKeypad { // discard the following '~' key from buffer _, _ = rr.state.reader.Discard(1) return SpecialKeyDelete, 1, nil } } // discard the following '~' key from buffer _, _ = rr.state.reader.Discard(1) return IgnoreKey, 1, nil } survey-2.3.7/terminal/runereader_ppc64le.go000066400000000000000000000004371434440262200207200ustar00rootroot00000000000000//go:build ppc64le && linux // +build ppc64le,linux package terminal // Used syscall numbers from https://github.com/golang/go/blob/master/src/syscall/ztypes_linux_ppc64le.go const ioctlReadTermios = 0x402c7413 // syscall.TCGETS const ioctlWriteTermios = 0x802c7414 // syscall.TCSETS survey-2.3.7/terminal/runereader_test.go000066400000000000000000000030711434440262200204170ustar00rootroot00000000000000package terminal import ( "testing" ) func TestRuneWidthInvisible(t *testing.T) { var example rune = '⁣' expected := 0 actual := runeWidth(example) if actual != expected { t.Errorf("Expected '%c' to have width %d, found %d", example, expected, actual) } } func TestRuneWidthNormal(t *testing.T) { var example rune = 'a' expected := 1 actual := runeWidth(example) if actual != expected { t.Errorf("Expected '%c' to have width %d, found %d", example, expected, actual) } } func TestRuneWidthWide(t *testing.T) { var example rune = '错' expected := 2 actual := runeWidth(example) if actual != expected { t.Errorf("Expected '%c' to have width %d, found %d", example, expected, actual) } } func TestStringWidthEmpty(t *testing.T) { example := "" expected := 0 actual := StringWidth(example) if actual != expected { t.Errorf("Expected '%s' to have width %d, found %d", example, expected, actual) } } func TestStringWidthNormal(t *testing.T) { example := "Green" expected := 5 actual := StringWidth(example) if actual != expected { t.Errorf("Expected '%s' to have width %d, found %d", example, expected, actual) } } func TestStringWidthFormat(t *testing.T) { example := "\033[31mRed\033[0m" expected := 3 actual := StringWidth(example) if actual != expected { t.Errorf("Expected '%s' to have width %d, found %d", example, expected, actual) } example = "\033[1;34mbold\033[21mblue\033[0m" expected = 8 actual = StringWidth(example) if actual != expected { t.Errorf("Expected '%s' to have width %d, found %d", example, expected, actual) } } survey-2.3.7/terminal/runereader_windows.go000066400000000000000000000066501434440262200211400ustar00rootroot00000000000000package terminal import ( "bytes" "syscall" "unsafe" ) var ( dll = syscall.NewLazyDLL("kernel32.dll") setConsoleMode = dll.NewProc("SetConsoleMode") getConsoleMode = dll.NewProc("GetConsoleMode") readConsoleInput = dll.NewProc("ReadConsoleInputW") ) const ( EVENT_KEY = 0x0001 // key codes for arrow keys // https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx VK_DELETE = 0x2E VK_END = 0x23 VK_HOME = 0x24 VK_LEFT = 0x25 VK_UP = 0x26 VK_RIGHT = 0x27 VK_DOWN = 0x28 RIGHT_CTRL_PRESSED = 0x0004 LEFT_CTRL_PRESSED = 0x0008 ENABLE_ECHO_INPUT uint32 = 0x0004 ENABLE_LINE_INPUT uint32 = 0x0002 ENABLE_PROCESSED_INPUT uint32 = 0x0001 ) type inputRecord struct { eventType uint16 padding uint16 event [16]byte } type keyEventRecord struct { bKeyDown int32 wRepeatCount uint16 wVirtualKeyCode uint16 wVirtualScanCode uint16 unicodeChar uint16 wdControlKeyState uint32 } type runeReaderState struct { term uint32 } func newRuneReaderState(input FileReader) runeReaderState { return runeReaderState{} } func (rr *RuneReader) Buffer() *bytes.Buffer { return nil } func (rr *RuneReader) SetTermMode() error { r, _, err := getConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(unsafe.Pointer(&rr.state.term))) // windows return 0 on error if r == 0 { return err } newState := rr.state.term newState &^= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT r, _, err = setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(newState)) // windows return 0 on error if r == 0 { return err } return nil } func (rr *RuneReader) RestoreTermMode() error { r, _, err := setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(rr.state.term)) // windows return 0 on error if r == 0 { return err } return nil } func (rr *RuneReader) ReadRune() (rune, int, error) { ir := &inputRecord{} bytesRead := 0 for { rv, _, e := readConsoleInput.Call(rr.stdio.In.Fd(), uintptr(unsafe.Pointer(ir)), 1, uintptr(unsafe.Pointer(&bytesRead))) // windows returns non-zero to indicate success if rv == 0 && e != nil { return 0, 0, e } if ir.eventType != EVENT_KEY { continue } // the event data is really a c struct union, so here we have to do an usafe // cast to put the data into the keyEventRecord (since we have already verified // above that this event does correspond to a key event key := (*keyEventRecord)(unsafe.Pointer(&ir.event[0])) // we only care about key down events if key.bKeyDown == 0 { continue } if key.wdControlKeyState&(LEFT_CTRL_PRESSED|RIGHT_CTRL_PRESSED) != 0 && key.unicodeChar == 'C' { return KeyInterrupt, bytesRead, nil } // not a normal character so look up the input sequence from the // virtual key code mappings (VK_*) if key.unicodeChar == 0 { switch key.wVirtualKeyCode { case VK_DOWN: return KeyArrowDown, bytesRead, nil case VK_LEFT: return KeyArrowLeft, bytesRead, nil case VK_RIGHT: return KeyArrowRight, bytesRead, nil case VK_UP: return KeyArrowUp, bytesRead, nil case VK_DELETE: return SpecialKeyDelete, bytesRead, nil case VK_HOME: return SpecialKeyHome, bytesRead, nil case VK_END: return SpecialKeyEnd, bytesRead, nil default: // not a virtual key that we care about so just continue on to // the next input key continue } } r := rune(key.unicodeChar) return r, bytesRead, nil } } survey-2.3.7/terminal/sequences.go000066400000000000000000000012401434440262200172130ustar00rootroot00000000000000package terminal import ( "fmt" "io" ) const ( KeyArrowLeft = '\x02' KeyArrowRight = '\x06' KeyArrowUp = '\x10' KeyArrowDown = '\x0e' KeySpace = ' ' KeyEnter = '\r' KeyBackspace = '\b' KeyDelete = '\x7f' KeyInterrupt = '\x03' KeyEndTransmission = '\x04' KeyEscape = '\x1b' KeyDeleteWord = '\x17' // Ctrl+W KeyDeleteLine = '\x18' // Ctrl+X SpecialKeyHome = '\x01' SpecialKeyEnd = '\x11' SpecialKeyDelete = '\x12' IgnoreKey = '\000' KeyTab = '\t' ) func soundBell(out io.Writer) error { _, err := fmt.Fprint(out, "\a") return err } survey-2.3.7/terminal/stdio.go000066400000000000000000000006141434440262200163460ustar00rootroot00000000000000package terminal import ( "io" ) // Stdio is the standard input/output the terminal reads/writes with. type Stdio struct { In FileReader Out FileWriter Err io.Writer } // FileWriter provides a minimal interface for Stdin. type FileWriter interface { io.Writer Fd() uintptr } // FileReader provides a minimal interface for Stdout. type FileReader interface { io.Reader Fd() uintptr } survey-2.3.7/terminal/syscall_windows.go000066400000000000000000000016771434440262200204620ustar00rootroot00000000000000package terminal import ( "syscall" ) var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo") procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") ) type wchar uint16 type dword uint32 type word uint16 type smallRect struct { left Short top Short right Short bottom Short } type consoleScreenBufferInfo struct { size Coord cursorPosition Coord attributes word window smallRect maximumWindowSize Coord } type consoleCursorInfo struct { size dword visible int32 } survey-2.3.7/terminal/terminal.go000066400000000000000000000001141434440262200170320ustar00rootroot00000000000000package terminal type Short int16 type Coord struct { X Short Y Short } survey-2.3.7/tests/000077500000000000000000000000001434440262200142235ustar00rootroot00000000000000survey-2.3.7/tests/README.md000066400000000000000000000004631434440262200155050ustar00rootroot00000000000000# survey/tests Because of the nature of this library, I was having a hard time finding a reliable way to run unit tests, therefore I decided to try to create a suite of integration tests which must be run successfully before a PR can be merged. I will try to add to this suite as new edge cases are known. survey-2.3.7/tests/ask.go000066400000000000000000000023261434440262200153330ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var simpleQs = []*survey.Question{ { Name: "name", Prompt: &survey.Input{ Message: "What is your name?", Default: "Johnny Appleseed", }, }, { Name: "color", Prompt: &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green", "yellow"}, Default: "yellow", }, Validate: survey.Required, }, } var singlePrompt = &survey.Input{ Message: "What is your name?", Default: "Johnny Appleseed", } func main() { fmt.Println("Asking many.") // a place to store the answers ans := struct { Name string Color string }{} err := survey.Ask(simpleQs, &ans) if err != nil { fmt.Println(err.Error()) return } fmt.Println("Asking one.") answer := "" err = survey.AskOne(singlePrompt, &answer) if err != nil { fmt.Println(err.Error()) return } fmt.Printf("Answered with %v.\n", answer) fmt.Println("Asking one with validation.") vAns := "" err = survey.AskOne(&survey.Input{Message: "What is your name?"}, &vAns, survey.WithValidator(survey.Required)) if err != nil { fmt.Println(err.Error()) return } fmt.Printf("Answered with %v.\n", vAns) } survey-2.3.7/tests/confirm.go000066400000000000000000000013321434440262200162060ustar00rootroot00000000000000//go:build ignore package main import ( "github.com/AlecAivazis/survey/v2" TestUtil "github.com/AlecAivazis/survey/v2/tests/util" ) var answer = false var goodTable = []TestUtil.TestTableEntry{ { "Enter 'yes'", &survey.Confirm{ Message: "yes:", }, &answer, nil, }, { "Enter 'no'", &survey.Confirm{ Message: "yes:", }, &answer, nil, }, { "default", &survey.Confirm{ Message: "yes:", Default: true, }, &answer, nil, }, { "not recognized (enter random letter)", &survey.Confirm{ Message: "yes:", Default: true, }, &answer, nil, }, { "no help - type '?'", &survey.Confirm{ Message: "yes:", Default: true, }, &answer, nil, }, } func main() { TestUtil.RunTable(goodTable) } survey-2.3.7/tests/doubleSelect.go000066400000000000000000000013041434440262200171620ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) var simpleQs = []*survey.Question{ { Name: "color", Prompt: &survey.Select{ Message: "select1:", Options: []string{"red", "blue", "green"}, }, Validate: survey.Required, }, { Name: "color2", Prompt: &survey.Select{ Message: "select2:", Options: []string{"red", "blue", "green"}, }, Validate: survey.Required, }, } func main() { answers := struct { Color string Color2 string }{} // ask the question err := survey.Ask(simpleQs, &answers) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("%s and %s.\n", answers.Color, answers.Color2) } survey-2.3.7/tests/editor.go000066400000000000000000000032271434440262200160440ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "strings" "github.com/AlecAivazis/survey/v2" TestUtil "github.com/AlecAivazis/survey/v2/tests/util" ) var answer = "" var goodTable = []TestUtil.TestTableEntry{ { "should open in editor", &survey.Editor{ Message: "should open", }, &answer, nil, }, { "has help", &survey.Editor{ Message: "press ? to see message", Help: "Does this work?", }, &answer, nil, }, { "should not include the default value in the prompt", &survey.Editor{ Message: "the default value 'Hello World' should not include in the prompt", HideDefault: true, Default: "Hello World", }, &answer, nil, }, { "should write the default value to the temporary file before the launch of the editor", &survey.Editor{ Message: "the default value 'Hello World' is written to the temporary file before the launch of the editor", AppendDefault: true, Default: "Hello World", }, &answer, nil, }, { Name: "should print the validation error, and recall the submitted invalid value instead of the default", Prompt: &survey.Editor{ Message: "the default value 'Hello World' is written to the temporary file before the launch of the editor", AppendDefault: true, Default: `this is the default value. change it to something containing "invalid" (in vi type "ccinvalidZZ")`, }, Value: &answer, Validate: func(v interface{}) error { s := v.(string) if strings.Contains(s, "invalid") { return fmt.Errorf(`this is the error message. change the input to something not containing "invalid"`) } return nil }, }, } func main() { TestUtil.RunTable(goodTable) } survey-2.3.7/tests/help.go000066400000000000000000000026011434440262200155010ustar00rootroot00000000000000//go:build ignore package main import ( "github.com/AlecAivazis/survey/v2" TestUtil "github.com/AlecAivazis/survey/v2/tests/util" ) var ( confirmAns = false inputAns = "" multiselectAns = []string{} selectAns = "" passwordAns = "" ) var goodTable = []TestUtil.TestTableEntry{ { "confirm", &survey.Confirm{ Message: "Is it raining?", Help: "Go outside, if your head becomes wet the answer is probably 'yes'", }, &confirmAns, nil, }, { "input", &survey.Input{ Message: "What is your phone number:", Help: "Phone number should include the area code, parentheses optional", }, &inputAns, nil, }, { "select", &survey.MultiSelect{ Message: "What days are you available:", Help: "We are closed weekends and avaibility is limited on Wednesday", Options: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"}, Default: []string{"Monday", "Tuesday", "Thursday", "Friday"}, }, &multiselectAns, nil, }, { "select", &survey.Select{ Message: "Choose a color:", Help: "Blue is the best color, but it is your choice", Options: []string{"red", "blue", "green"}, Default: "blue", }, &selectAns, nil, }, { "password", &survey.Password{ Message: "Enter a secret:", Help: "Don't really enter a secret, this is just for testing", }, &passwordAns, nil, }, } func main() { TestUtil.RunTable(goodTable) } survey-2.3.7/tests/input.go000066400000000000000000000016631434440262200157170ustar00rootroot00000000000000//go:build ignore package main import ( "github.com/AlecAivazis/survey/v2" TestUtil "github.com/AlecAivazis/survey/v2/tests/util" ) var val = "" var table = []TestUtil.TestTableEntry{ { "no default", &survey.Input{Message: "Hello world"}, &val, nil, }, { "default", &survey.Input{Message: "Hello world", Default: "default"}, &val, nil, }, { "no help, send '?'", &survey.Input{Message: "Hello world"}, &val, nil, }, { "Home, End Button test in random location", &survey.Input{Message: "Hello world"}, &val, nil, }, { "Delete and forward delete test at random location (test if screen overflows)", &survey.Input{Message: "Hello world"}, &val, nil, }, { "Moving around lines with left & right arrow keys", &survey.Input{Message: "Hello world"}, &val, nil, }, { "Runes with width > 1. Enter 一 you get to the next line", &survey.Input{Message: "Hello world"}, &val, nil, }, } func main() { TestUtil.RunTable(table) } survey-2.3.7/tests/longSelect.go000066400000000000000000000004561434440262200166560ustar00rootroot00000000000000//go:build ignore package main import "github.com/AlecAivazis/survey/v2" func main() { color := "" prompt := &survey.Select{ Message: "Choose a color:", Options: []string{ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", }, } survey.AskOne(prompt, &color) } survey-2.3.7/tests/multiselect.go000066400000000000000000000033331434440262200171060ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" TestUtil "github.com/AlecAivazis/survey/v2/tests/util" ) var answer = []string{} var table = []TestUtil.TestTableEntry{ { "standard", &survey.MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, }, &answer, nil, }, { "default (sunday, tuesday)", &survey.MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Default: []string{"Sunday", "Tuesday"}, }, &answer, nil, }, { "default not found", &survey.MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Default: []string{"Sundayaa"}, }, &answer, nil, }, { "no help - type ?", &survey.MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Default: []string{"Sundayaa"}, }, &answer, nil, }, { "can navigate with j/k", &survey.MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Default: []string{"Sundayaa"}, }, &answer, nil, }, { "descriptions", &survey.MultiSelect{ Message: "What days do you prefer:", Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, Description: func(value string, index int) string { return value + fmt.Sprint(index) }, }, &answer, nil, }, } func main() { TestUtil.RunTable(table) } survey-2.3.7/tests/password.go000066400000000000000000000010341434440262200164120ustar00rootroot00000000000000//go:build ignore package main import ( "github.com/AlecAivazis/survey/v2" TestUtil "github.com/AlecAivazis/survey/v2/tests/util" ) var value = "" var table = []TestUtil.TestTableEntry{ { "standard", &survey.Password{Message: "Please type your password:"}, &value, nil, }, { "please make sure paste works", &survey.Password{Message: "Please paste your password:"}, &value, nil, }, { "no help, send '?'", &survey.Password{Message: "Please type your password:"}, &value, nil, }, } func main() { TestUtil.RunTable(table) } survey-2.3.7/tests/select.go000066400000000000000000000027131434440262200160340ustar00rootroot00000000000000//go:build ignore package main import ( "github.com/AlecAivazis/survey/v2" TestUtil "github.com/AlecAivazis/survey/v2/tests/util" ) var answer = "" var goodTable = []TestUtil.TestTableEntry{ { "standard", &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, }, &answer, nil, }, { "short", &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue"}, }, &answer, nil, }, { "default", &survey.Select{ Message: "Choose a color (should default blue):", Options: []string{"red", "blue", "green"}, Default: "blue", }, &answer, nil, }, { "one", &survey.Select{ Message: "Choose one:", Options: []string{"hello"}, }, &answer, nil, }, { "no help, type ?", &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue"}, }, &answer, nil, }, { "passes through bottom", &survey.Select{ Message: "Choose one:", Options: []string{"red", "blue"}, }, &answer, nil, }, { "passes through top", &survey.Select{ Message: "Choose one:", Options: []string{"red", "blue"}, }, &answer, nil, }, { "can navigate with j/k", &survey.Select{ Message: "Choose one:", Options: []string{"red", "blue", "green"}, }, &answer, nil, }, } var badTable = []TestUtil.TestTableEntry{ { "no options", &survey.Select{ Message: "Choose one:", }, &answer, nil, }, } func main() { TestUtil.RunTable(goodTable) TestUtil.RunErrorTable(badTable) } survey-2.3.7/tests/selectThenInput.go000066400000000000000000000012721434440262200176720ustar00rootroot00000000000000//go:build ignore package main import ( "fmt" "github.com/AlecAivazis/survey/v2" ) // the questions to ask var simpleQs = []*survey.Question{ { Name: "color", Prompt: &survey.Select{ Message: "Choose a color:", Options: []string{"red", "blue", "green"}, }, Validate: survey.Required, }, { Name: "name", Prompt: &survey.Input{ Message: "What is your name?", }, Validate: survey.Required, }, } func main() { answers := struct { Color string Name string }{} // ask the question err := survey.Ask(simpleQs, &answers) if err != nil { fmt.Println(err.Error()) return } // print the answers fmt.Printf("%s chose %s.\n", answers.Name, answers.Color) } survey-2.3.7/tests/util/000077500000000000000000000000001434440262200152005ustar00rootroot00000000000000survey-2.3.7/tests/util/test.go000066400000000000000000000022701434440262200165070ustar00rootroot00000000000000package TestUtil import ( "fmt" "reflect" "github.com/AlecAivazis/survey/v2" ) type TestTableEntry struct { Name string Prompt survey.Prompt Value interface{} Validate func(interface{}) error } func formatAnswer(ans interface{}) { // show the answer to the user fmt.Printf("Answered %v.\n", reflect.ValueOf(ans).Elem()) fmt.Println("---------------------") } func RunTable(table []TestTableEntry) { // go over every entry in the table for _, entry := range table { // tell the user what we are going to ask them fmt.Println(entry.Name) // perform the ask err := survey.AskOne(entry.Prompt, entry.Value) if err != nil { fmt.Printf("AskOne on %v's prompt failed: %v.", entry.Name, err.Error()) break } // show the answer to the user formatAnswer(entry.Value) } } func RunErrorTable(table []TestTableEntry) { // go over every entry in the table for _, entry := range table { // tell the user what we are going to ask them fmt.Println(entry.Name) // perform the ask err := survey.AskOne(entry.Prompt, entry.Value, survey.WithValidator(entry.Validate)) if err == nil { fmt.Printf("AskOne on %v's prompt didn't fail.", entry.Name) break } } } survey-2.3.7/transform.go000066400000000000000000000050701434440262200154250ustar00rootroot00000000000000package survey import ( "reflect" "strings" "golang.org/x/text/cases" "golang.org/x/text/language" ) // TransformString returns a `Transformer` based on the "f" // function which accepts a string representation of the answer // and returns a new one, transformed, answer. // Take for example the functions inside the std `strings` package, // they can be converted to a compatible `Transformer` by using this function, // i.e: `TransformString(strings.Title)`, `TransformString(strings.ToUpper)`. // // Note that `TransformString` is just a helper, `Transformer` can be used // to transform any type of answer. func TransformString(f func(s string) string) Transformer { return func(ans interface{}) interface{} { // if the answer value passed in is the zero value of the appropriate type if isZero(reflect.ValueOf(ans)) { // skip this `Transformer` by returning a zero value of string. // The original answer will be not affected, // see survey.go#L125. // A zero value of string should be returned to be handled by // next Transformer in a composed Tranformer, // see tranform.go#L75 return "" } // "ans" is never nil here, so we don't have to check that // see survey.go#L338 for more. // Make sure that the the answer's value was a typeof string. s, ok := ans.(string) if !ok { return "" } return f(s) } } // ToLower is a `Transformer`. // It receives an answer value // and returns a copy of the "ans" // with all Unicode letters mapped to their lower case. // // Note that if "ans" is not a string then it will // return a nil value, meaning that the above answer // will not be affected by this call at all. func ToLower(ans interface{}) interface{} { transformer := TransformString(strings.ToLower) return transformer(ans) } // Title is a `Transformer`. // It receives an answer value // and returns a copy of the "ans" // with all Unicode letters that begin words // mapped to their title case. // // Note that if "ans" is not a string then it will // return a nil value, meaning that the above answer // will not be affected by this call at all. func Title(ans interface{}) interface{} { transformer := TransformString(cases.Title(language.English).String) return transformer(ans) } // ComposeTransformers is a variadic function used to create one transformer from many. func ComposeTransformers(transformers ...Transformer) Transformer { // return a transformer that calls each one sequentially return func(ans interface{}) interface{} { // execute each transformer for _, t := range transformers { ans = t(ans) } return ans } } survey-2.3.7/transform_test.go000066400000000000000000000027731434440262200164730ustar00rootroot00000000000000package survey import ( "strings" "testing" ) func testStringTransformer(t *testing.T, f func(string) string) { transformer := TransformString(f) tests := []string{ "hello my name is", "where are you from", "does that matter?", } for _, tt := range tests { if expected, got := f(tt), transformer(tt); expected != got { t.Errorf("TransformString transformer failed to transform the answer, expected '%s' but got '%s'.", expected, got) } } } func TestTransformString(t *testing.T) { testStringTransformer(t, strings.ToTitle) // all letters titled testStringTransformer(t, strings.ToLower) // all letters lowercase } func TestComposeTransformers(t *testing.T) { // create a transformer which makes no sense, // remember: transformer can be used for any type // we just test the built'n functions that // happens to be for strings only. transformer := ComposeTransformers( Title, ToLower, ) ans := "my name is" if expected, got := strings.ToLower(ans), transformer(ans); expected != got { // the result should be lowercase. t.Errorf("TestComposeTransformers transformer failed to transform the answer to title->lowercase, expected '%s' but got '%s'.", expected, got) } var emptyAns string if expected, got := "", transformer(emptyAns); expected != got { // TransformString transformers should be skipped and return zero value string t.Errorf("TestComposeTransformers transformer failed to skip transforming on optional empty input, expected '%s' but got '%s'.", expected, got) } } survey-2.3.7/validate.go000066400000000000000000000077101434440262200152060ustar00rootroot00000000000000package survey import ( "errors" "fmt" "reflect" "github.com/AlecAivazis/survey/v2/core" ) // Required does not allow an empty value func Required(val interface{}) error { // the reflect value of the result value := reflect.ValueOf(val) // if the value passed in is the zero value of the appropriate type if isZero(value) && value.Kind() != reflect.Bool { //lint:ignore ST1005 this error message should render as capitalized return errors.New("Value is required") } return nil } // MaxLength requires that the string is no longer than the specified value func MaxLength(length int) Validator { // return a validator that checks the length of the string return func(val interface{}) error { if str, ok := val.(string); ok { // if the string is longer than the given value if len([]rune(str)) > length { // yell loudly return fmt.Errorf("value is too long. Max length is %v", length) } } else { // otherwise we cannot convert the value into a string and cannot enforce length return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name()) } // the input is fine return nil } } // MinLength requires that the string is longer or equal in length to the specified value func MinLength(length int) Validator { // return a validator that checks the length of the string return func(val interface{}) error { if str, ok := val.(string); ok { // if the string is shorter than the given value if len([]rune(str)) < length { // yell loudly return fmt.Errorf("value is too short. Min length is %v", length) } } else { // otherwise we cannot convert the value into a string and cannot enforce length return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name()) } // the input is fine return nil } } // MaxItems requires that the list is no longer than the specified value func MaxItems(numberItems int) Validator { // return a validator that checks the length of the list return func(val interface{}) error { if list, ok := val.([]core.OptionAnswer); ok { // if the list is longer than the given value if len(list) > numberItems { // yell loudly return fmt.Errorf("value is too long. Max items is %v", numberItems) } } else { // otherwise we cannot convert the value into a list of answer and cannot enforce length return fmt.Errorf("cannot impose the length on something other than a list of answers") } // the input is fine return nil } } // MinItems requires that the list is longer or equal in length to the specified value func MinItems(numberItems int) Validator { // return a validator that checks the length of the list return func(val interface{}) error { if list, ok := val.([]core.OptionAnswer); ok { // if the list is shorter than the given value if len(list) < numberItems { // yell loudly return fmt.Errorf("value is too short. Min items is %v", numberItems) } } else { // otherwise we cannot convert the value into a list of answer and cannot enforce length return fmt.Errorf("cannot impose the length on something other than a list of answers") } // the input is fine return nil } } // ComposeValidators is a variadic function used to create one validator from many. func ComposeValidators(validators ...Validator) Validator { // return a validator that calls each one sequentially return func(val interface{}) error { // execute each validator for _, validator := range validators { // if the answer's value is not valid if err := validator(val); err != nil { // return the error return err } } // we passed all validators, the answer is valid return nil } } // isZero returns true if the passed value is the zero object func isZero(v reflect.Value) bool { switch v.Kind() { case reflect.Slice, reflect.Map: return v.Len() == 0 } // compare the types directly with more general coverage return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) } survey-2.3.7/validate_test.go000066400000000000000000000122561434440262200162460ustar00rootroot00000000000000package survey import ( "math/rand" "testing" "github.com/AlecAivazis/survey/v2/core" ) func TestRequired_canSucceedOnPrimitiveTypes(t *testing.T) { // a string to test str := "hello" // if the string is not valid if valid := Required(str); valid != nil { // t.Error("Non null returned an error when one wasn't expected.") } } func TestRequired_canFailOnPrimitiveTypes(t *testing.T) { // a string to test str := "" // if the string is valid if notValid := Required(str); notValid == nil { // t.Error("Non null did not return an error when one was expected.") } } func TestRequired_canSucceedOnMap(t *testing.T) { // an non-empty map to test val := map[string]int{"hello": 1} // if the string is not valid if valid := Required(val); valid != nil { // t.Error("Non null returned an error when one wasn't expected.") } } func TestRequired_passesOnFalse(t *testing.T) { // a false value to pass to the validator val := false // if the boolean is invalid if notValid := Required(val); notValid != nil { // t.Error("False failed a required check.") } } func TestRequired_canFailOnMap(t *testing.T) { // an non-empty map to test val := map[string]int{} // if the string is valid if notValid := Required(val); notValid == nil { // t.Error("Non null did not return an error when one was expected.") } } func TestRequired_canSucceedOnLists(t *testing.T) { // a string to test str := []string{"hello"} // if the string is not valid if valid := Required(str); valid != nil { // t.Error("Non null returned an error when one wasn't expected.") } } func TestRequired_canFailOnLists(t *testing.T) { // a string to test str := []string{} // if the string is not valid if notValid := Required(str); notValid == nil { // t.Error("Non null did not return an error when one was expected.") } } const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func randString(n int) string { b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] } return string(b) } func TestMaxItems(t *testing.T) { // the list to test testList := []core.OptionAnswer{ {Value: "a", Index: 0}, {Value: "b", Index: 1}, {Value: "c", Index: 2}, {Value: "d", Index: 3}, {Value: "e", Index: 4}, {Value: "f", Index: 5}, } // validate the list if err := MaxItems(4)(testList); err == nil { t.Error("No error returned with input greater than 6 items.") } } func TestMinItems(t *testing.T) { // the list to test testList := []core.OptionAnswer{ {Value: "a", Index: 0}, {Value: "b", Index: 1}, {Value: "c", Index: 2}, {Value: "d", Index: 3}, {Value: "e", Index: 4}, {Value: "f", Index: 5}, } // validate the list if err := MinItems(10)(testList); err == nil { t.Error("No error returned with input less than 10 items.") } } func TestMaxLength(t *testing.T) { // the string to test testStr := randString(150) // validate the string if err := MaxLength(140)(testStr); err == nil { t.Error("No error returned with input greater than 150 characters.") } // emoji test emojiStr := "I😍Golang" // validate visible length with Maxlength if err := MaxLength(10)(emojiStr); err != nil { t.Errorf("Error returned with emoji containing 8 characters long input.") } } func TestMinLength(t *testing.T) { // validate the string if err := MinLength(12)(randString(10)); err == nil { t.Error("No error returned with input less than 12 characters.") } // emoji test emojiStr := "I😍Golang" // validate visibly 8 characters long string with MinLength if err := MinLength(10)(emojiStr); err == nil { t.Error("No error returned with emoji containing input less than 10 characters.") } } func TestMinLength_onInt(t *testing.T) { // validate the string if err := MinLength(12)(1); err == nil { t.Error("No error returned when enforcing length on int.") } } func TestMaxLength_onInt(t *testing.T) { // validate the string if err := MaxLength(12)(1); err == nil { t.Error("No error returned when enforcing length on int.") } } func TestComposeValidators_passes(t *testing.T) { // create a validator that requires a string of no more than 10 characters valid := ComposeValidators( Required, MaxLength(10), ) str := randString(12) // if a valid string fails if err := valid(str); err == nil { // the test failed t.Error("Composed validator did not pass. Wanted string less than 10 chars, passed in", str) } } func TestComposeValidators_failsOnFirstError(t *testing.T) { // create a validator that requires a string of no more than 10 characters valid := ComposeValidators( Required, MaxLength(10), ) // if an empty string passes if err := valid(""); err == nil { // the test failed t.Error("Composed validator did not fail on first test like expected.") } } func TestComposeValidators_failsOnSubsequentValidators(t *testing.T) { // create a validator that requires a string of no more than 10 characters valid := ComposeValidators( Required, MaxLength(10), ) str := randString(12) // if a string longer than 10 passes if err := valid(str); err == nil { // the test failed t.Error("Composed validator did not fail on second first test like expected. Should fail max length > 10 :", str) } }