Use Survey For Interactions With User (#186)

fixes

Use Survey For Interactions With User

Add Vendor "github.com/AlecAivazis/survey/v2"

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/186
Reviewed-by: techknowlogick <techknowlogick@gitea.io>
Reviewed-by: Norwin <noerw@noreply.gitea.io>
This commit is contained in:
6543
2020-10-03 02:54:09 +00:00
parent e23f56e81c
commit 7ac3ffcc1b
81 changed files with 12088 additions and 40 deletions

22
vendor/github.com/AlecAivazis/survey/v2/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,22 @@
language: go
go:
- 1.12
os:
- linux
- linux-ppc64le
- osx
- windows
go_import_path: github.com/AlecAivazis/survey/v2
before_install:
- go get github.com/alecaivazis/run
install:
- run install-deps
script:
- run tests
# - run autoplay-tests

View File

@ -0,0 +1,77 @@
# 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.
## Table of Contents
1. [Code of Conduct](#code-of-conduct)
1. [Getting Help](#getting-help)
1. [Filing a Bug Report](#how-to-file-a-bug-report)
1. [Suggesting an API change](#suggesting-an-api-change)
1. [Submitting a Contribution](#submitting-a-contribution)
1. [Writing and Running Tests](#writing-and-running-tests)
## 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
Feel free to [open up an issue](https://github.com/AlecAivazis/survey/v2/issues/new) on GitHub when asking a question so others will be able to find it. Please remember to tag the issue with the `Question` label so the maintainers can get to your question as soon as possible. If the question is urgent, feel free to reach out to `@AlecAivazis` directly in the gophers slack channel.
## How to file a bug report
Bugs are tracked using the Github Issue tracker. When filing a bug, please remember to label the issue as a `Bug` and answer/provide the following:
1. What operating system and terminal are you using?
1. An example that showcases the bug.
1. What did you expect to see?
1. What did you see instead?
## Suggesting an API change
If you have an idea, I'm more than happy to discuss it. Please open an issue and we can work through it. In order to maintain some sense of stability, additions to the top-level API are taken just as seriously as changes that break it. Adding stuff is much easier than removing it.
## Submitting a contribution
In order to maintain stability, most features get fully integrated in more than one PR. This allows for more opportunity to think through each API change without amassing large amounts of tech debt and API changes at once. If your feature can be broken into separate chunks, it will be able to be reviewed much quicker. For example, if the PR that implemented the `Validate` field was submitted in a PR separately from one that included `survey.Required`, it would be able to get merge without having to decide how many different `Validators` we want to provide as part of `survey`'s API.
When submitting a contribution,
- Provide a description of the feature or change
- Reference the ticket addressed by the PR if there is one
- Following community standards, add comments for all exported members so that all necessary information is available on godocs
- Remember to update the project README.md with changes to the high-level API
- Include both positive and negative unit tests (when applicable)
- Contributions with visual ramifications or interaction changes should be accompanied with the appropriate `go-expect` tests. For more information on writing these tests, see [Writing and Running Tests](#writing-and-running-tests)
## 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.

78
vendor/github.com/AlecAivazis/survey/v2/Gopkg.lock generated vendored Normal file
View File

@ -0,0 +1,78 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/Netflix/go-expect"
packages = ["."]
revision = "c93bf25de8e869da25cf26bcd2932b36141f61ae"
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/hinshun/vt10x"
packages = ["."]
revision = "1954e646417484a2a687ea344edade2c2b6523c8"
[[projects]]
branch = "master"
name = "github.com/kballard/go-shellquote"
packages = ["."]
revision = "95032a82bc518f77982ea72343cc1ade730072f0"
[[projects]]
name = "github.com/kr/pty"
packages = ["."]
revision = "282ce0e5322c82529687d609ee670fac7c7d917c"
version = "v1.1.1"
[[projects]]
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
branch = "master"
name = "github.com/mgutz/ansi"
packages = ["."]
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/stretchr/testify"
packages = [
"assert",
"require"
]
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "9527bec2660bd847c050fda93a0f0c6dee0800bb"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "371508ebad4798adc38a118f858b5c17a65b58594203548f9feb74cb781dd907"
solver-name = "gps-cdcl"
solver-version = 1

54
vendor/github.com/AlecAivazis/survey/v2/Gopkg.toml generated vendored Normal file
View File

@ -0,0 +1,54 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/Netflix/go-expect"
[[constraint]]
branch = "master"
name = "github.com/hinshun/vt10x"
[[constraint]]
name = "github.com/mattn/go-isatty"
version = "0.0.3"
[[constraint]]
branch = "master"
name = "github.com/mgutz/ansi"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[prune]
go-tests = true
unused-packages = true
[[constraint]]
branch = "master"
name = "github.com/kballard/go-shellquote"

21
vendor/github.com/AlecAivazis/survey/v2/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT 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.

454
vendor/github.com/AlecAivazis/survey/v2/README.md generated vendored Normal file
View File

@ -0,0 +1,454 @@
# Survey
[![Build Status](https://travis-ci.org/AlecAivazis/survey.svg?branch=feature%2Fpretty)](https://travis-ci.org/AlecAivazis/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 prompts.
<img width="550" src="https://thumbs.gfycat.com/VillainousGraciousKouprey-size_restricted.gif"/>
```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)
}
```
## Table of Contents
1. [Examples](#examples)
1. [Running the Prompts](#running-the-prompts)
1. [Prompts](#prompts)
1. [Input](#input)
1. [Multiline](#multiline)
1. [Password](#password)
1. [Confirm](#confirm)
1. [Select](#select)
1. [MultiSelect](#multiselect)
1. [Editor](#editor)
1. [Filtering Options](#filtering-options)
1. [Validation](#validation)
1. [Built-in Validators](#built-in-validators)
1. [Help Text](#help-text)
1. [Changing the input rune](#changing-the-input-rune)
1. [Changing the Icons ](#changing-the-icons)
1. [Custom Types](#custom-types)
1. [Testing](#testing)
1. [FAQ](#faq)
## Examples
Examples can be found in the `examples/` directory. Run them
to see basic behavior:
```bash
go get github.com/AlecAivazis/survey/v2
cd $GOPATH/src/github.com/AlecAivazis/survey
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
<img src="https://thumbs.gfycat.com/LankyBlindAmericanpainthorse-size_restricted.gif" width="400px"/>
```golang
name := ""
prompt := &survey.Input{
Message: "ping",
}
survey.AskOne(prompt, &name)
```
### Multiline
<img src="https://thumbs.gfycat.com/ImperfectShimmeringBeagle-size_restricted.gif" width="400px"/>
```golang
text := ""
prompt := &survey.Multiline{
Message: "ping",
}
survey.AskOne(prompt, &text)
```
### Password
<img src="https://thumbs.gfycat.com/CompassionateSevereHypacrosaurus-size_restricted.gif" width="400px" />
```golang
password := ""
prompt := &survey.Password{
Message: "Please type your password",
}
survey.AskOne(prompt, &password)
```
### Confirm
<img src="https://thumbs.gfycat.com/UnkemptCarefulGermanpinscher-size_restricted.gif" width="400px"/>
```golang
name := false
prompt := &survey.Confirm{
Message: "Do you like pie?",
}
survey.AskOne(prompt, &name)
```
### Select
<img src="https://thumbs.gfycat.com/GrimFilthyAmazonparrot-size_restricted.gif" width="450px"/>
```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))
```
### 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 | |
## Help Text
All of the prompts have a `Help` field which can be defined to provide more information to your users:
<img src="https://thumbs.gfycat.com/CloudyRemorsefulFossa-size_restricted.gif" width="400px" style="margin-top: 8px"/>
```golang
&survey.Input{
Message: "What is your phone number:",
Help: "Phone number should include the area code",
}
```
### 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
### Why isn't sending a SIGINT (aka. CTRL-C) signal working?
When you send an interrupt signal to the process, it only interrupts the current prompt instead of the entire process. This manifests in a `github.com/AlecAivazis/survey/v2/terminal.InterruptErr` being returned from `Ask` and `AskOne`. If you want to stop the process, handle the returned error in your code:
```go
err := survey.AskOne(prompt, &myVar)
if err == terminal.InterruptErr {
fmt.Println("interrupted")
os.Exit(0)
} else if err != nil {
panic(err)
}
```

19
vendor/github.com/AlecAivazis/survey/v2/_tasks.hcl generated vendored Normal file
View File

@ -0,0 +1,19 @@
task "install-deps" {
description = "Install all of package dependencies"
pipeline = [
"go get {{.files}}",
]
}
task "tests" {
description = "Run the test suite"
command = "go test {{.files}}"
environment {
GOFLAGS = "-mod=vendor"
}
}
variables {
files = "$(go list -v ./... | grep -iEv \"tests|examples\")"
}

153
vendor/github.com/AlecAivazis/survey/v2/confirm.go generated vendored Normal file
View File

@ -0,0 +1,153 @@
package 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 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
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
}
// should not get here
return c.Default, 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,
},
)
}

View File

@ -0,0 +1,91 @@
package core
import (
"bytes"
"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 ""
},
}
//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
if DisableColor {
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
}

356
vendor/github.com/AlecAivazis/survey/v2/core/write.go generated vendored Normal file
View File

@ -0,0 +1,356 @@
package 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
}
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
fieldIndex, err := findFieldIndex(elem, name)
// if something went wrong
if err != nil {
// bubble up
return err
}
field := elem.Field(fieldIndex)
// 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:
// err := survey.Ask(qs, &v);
// if err != nil {
// if name, ok := core.IsFieldNotMatch(err); ok {
// [...name is the not matched question name]
// }
// }
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 findFieldIndex(s reflect.Value, name string) (int, error) {
// the type of the value
sType := s.Type()
// first look for matching tags so we can overwrite matching field names
for i := 0; i < sType.NumField(); i++ {
// the field we are current scanning
field := sType.Field(i)
// the value of the survey tag
tag := field.Tag.Get(tagName)
// if the tag matches the name we are looking for
if tag != "" && tag == name {
// then we found our index
return i, nil
}
}
// then look for matching names
for i := 0; i < sType.NumField(); i++ {
// the field we are current scanning
field := sType.Field(i)
// if the name of the field matches what we're looking for
if strings.ToLower(field.Name) == strings.ToLower(name) {
return i, nil
}
}
// we didn't find the field
return -1, errFieldNotMatch{name}
}
// 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:
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
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
copy(t.Slice(i, i+1).Index(0), v.Index(i))
}
}
} else {
// set the value to the target
t.Set(v)
}
// we're done
return
}

222
vendor/github.com/AlecAivazis/survey/v2/editor.go generated vendored Normal file
View File

@ -0,0 +1,222 @@
package 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 <Received>.
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 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 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: "<Received>",
ShowAnswer: true,
Config: config,
},
)
}

1
vendor/github.com/AlecAivazis/survey/v2/filter.go generated vendored Normal file
View File

@ -0,0 +1 @@
package survey

19
vendor/github.com/AlecAivazis/survey/v2/go.mod generated vendored Normal file
View File

@ -0,0 +1,19 @@
module github.com/AlecAivazis/survey/v2
require (
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kr/pty v1.1.4 // indirect
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/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.1
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 // indirect
golang.org/x/text v0.3.0
)
go 1.13

31
vendor/github.com/AlecAivazis/survey/v2/go.sum generated vendored Normal file
View File

@ -0,0 +1,31 @@
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
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-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
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/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 h1:R4dVlxdmKenVdMRS/tTspEpSTRWINYrHD8ySIU9yCIU=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

110
vendor/github.com/AlecAivazis/survey/v2/input.go generated vendored Normal file
View File

@ -0,0 +1,110 @@
package survey
/*
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
}
// data available to the templates when processing
type InputTemplateData struct {
Input
Answer string
ShowAnswer bool
ShowHelp bool
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 and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ print .Config.HelpInput }} for help]{{color "reset"}} {{end}}
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
{{- end}}`
func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
// render the template
err := i.Render(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
Config: config,
},
)
if err != nil {
return "", err
}
// start reading runes from the standard in
rr := i.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
cursor := i.NewCursor()
line := []rune{}
// get the next line
for {
line, err = rr.ReadLine(0)
if err != nil {
return string(line), err
}
// terminal will echo the \n so we need to jump back up one row
cursor.PreviousLine(1)
if string(line) == config.HelpInput && i.Help != "" {
err = i.Render(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
ShowHelp: true,
Config: config,
},
)
if err != nil {
return "", err
}
continue
}
break
}
// if the line is empty
if line == nil || len(line) == 0 {
// use the default value
return i.Default, err
}
lineStr := string(line)
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,
Answer: val.(string),
ShowAnswer: true,
Config: config,
},
)
}

110
vendor/github.com/AlecAivazis/survey/v2/multiline.go generated vendored Normal file
View File

@ -0,0 +1,110 @@
package 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 rr.RestoreTermMode()
cursor := i.NewCursor()
multiline := make([]string, 0)
emptyOnce := false
// get the next line
for {
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,
},
)
}

327
vendor/github.com/AlecAivazis/survey/v2/multiselect.go generated vendored Normal file
View File

@ -0,0 +1,327 @@
package 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
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
PageEntries []core.OptionAnswer
Config *PromptConfig
}
var MultiSelectQuestionTemplate = `
{{- 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, <right> to all, <left> to none, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
{{- "\n"}}
{{- range $ix, $option := .PageEntries}}
{{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
{{- if index $.Checked $option.Index }}{{color $.Config.Icons.MarkedOption.Format }} {{ $.Config.Icons.MarkedOption.Text }} {{else}}{{color $.Config.Icons.UnmarkedOption.Format }} {{ $.Config.Icons.UnmarkedOption.Text }} {{end}}
{{- color "reset"}}
{{- " "}}{{$option.Value}}{{"\n"}}
{{- 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.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 != "" {
m.filter = m.filter[0 : len(m.filter)-1]
}
} else if key >= terminal.KeySpace {
m.filter += string(key)
m.VimMode = false
} else if key == terminal.KeyArrowRight {
for _, v := range options {
m.checked[v.Index] = true
}
if !config.KeepFilter {
m.filter = ""
}
} else if 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)
// render the options
m.Render(
MultiSelectQuestionTemplate,
MultiSelectTemplateData{
MultiSelect: *m,
SelectedIndex: idx,
Checked: m.checked,
ShowHelp: m.showingHelp,
PageEntries: opts,
Config: config,
},
)
}
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.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
// ask the question
err := m.Render(
MultiSelectQuestionTemplate,
MultiSelectTemplateData{
MultiSelect: *m,
SelectedIndex: idx,
Checked: m.checked,
PageEntries: opts,
Config: config,
},
)
if err != nil {
return "", err
}
rr := m.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
// start waiting for input
for {
r, _, _ := rr.ReadRune()
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,
Config: config,
},
)
}

101
vendor/github.com/AlecAivazis/survey/v2/password.go generated vendored Normal file
View File

@ -0,0 +1,101 @@
package 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,
},
)
fmt.Fprint(terminal.NewAnsiStdout(p.Stdio().Out), userOut)
if err != nil {
return "", err
}
rr := p.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
// no help msg? Just return any response
if p.Help == "" {
line, err := rr.ReadLine('*')
return string(line), err
}
cursor := p.NewCursor()
line := []rune{}
// process answers looking for help prompt answer
for {
line, err = rr.ReadLine('*')
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("*", 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
}

157
vendor/github.com/AlecAivazis/survey/v2/renderer.go generated vendored Normal file
View File

@ -0,0 +1,157 @@
package survey
import (
"bytes"
"fmt"
"unicode/utf8"
"github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/terminal"
goterm "golang.org/x/crypto/ssh/terminal"
)
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
fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut)
// add the printed text to the rendered error buffer so we can cleanup later
r.appendRenderedError(layoutOut)
return nil
}
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
fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut)
// add the printed text to the rendered text buffer so we can cleanup later
r.AppendRenderedText(layoutOut)
// nothing went wrong
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 := goterm.GetSize(fd)
return termWidth, err
}
// 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, 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
}
bufBytes := buf.Bytes()
count := 0
curr := 0
delim := -1
for curr < len(bufBytes) {
// 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
}
// account for word wrapping
count += int(utf8.RuneCount(bufBytes[curr:delim]) / w)
curr = delim + 1
}
return count
}

328
vendor/github.com/AlecAivazis/survey/v2/select.go generated vendored Normal file
View File

@ -0,0 +1,328 @@
package survey
import (
"errors"
"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
filter string
selectedIndex int
useDefault bool
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
Config *PromptConfig
}
var SelectQuestionTemplate = `
{{- 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, $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}}
{{- 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 {
s.useDefault = false
// 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.KeyArrowDown || (s.VimMode && key == 'j') && len(options) > 0 {
s.useDefault = false
// 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 != "" {
// subtract a line from the current filter
s.filter = s.filter[0 : len(s.filter)-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
// make sure that we use the current value in the filtered list
s.useDefault = 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)
// render the options
s.Render(
SelectQuestionTemplate,
SelectTemplateData{
Select: *s,
SelectedIndex: idx,
ShowHelp: s.showingHelp,
PageEntries: opts,
Config: config,
},
)
// 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")
}
// start off with the first option selected
sel := 0
// if there is a default
if s.Default != "" {
// find the choice
for i, opt := range s.Options {
// if the option corresponds to the default
if opt == s.Default {
// we found our initial value
sel = i
// stop looking
break
}
}
}
// save the selected index
s.selectedIndex = sel
// 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), sel)
// ask the question
err := s.Render(
SelectQuestionTemplate,
SelectTemplateData{
Select: *s,
PageEntries: opts,
SelectedIndex: idx,
Config: config,
},
)
if err != nil {
return "", err
}
// by default, use the default value
s.useDefault = true
rr := s.NewRuneReader()
rr.SetTermMode()
defer rr.RestoreTermMode()
cursor := s.NewCursor()
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
// 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 = ""
// the index to report
var val string
// if we are supposed to use the default value
if s.useDefault || s.selectedIndex >= len(options) {
// if there is a default value
if s.Default != nil {
// if the default is a string
if defaultString, ok := s.Default.(string); ok {
// use the default value
val = defaultString
// the default value could also be an interpret which is interpretted as the index
} else if defaultIndex, ok := s.Default.(int); ok {
val = s.Options[defaultIndex]
} else {
return val, errors.New("default value of select must be an int or string")
}
} else if len(options) > 0 {
// there is no default value so use the first
val = options[0].Value
}
// otherwise the selected index points to the value
} else if s.selectedIndex < len(options) {
// the
val = options[s.selectedIndex].Value
}
// now that we have the value lets go hunt down the right index to return
idx = -1
for i, optionValue := range s.Options {
if optionValue == val {
idx = i
}
}
return core.OptionAnswer{Value: val, Index: idx}, err
}
func (s *Select) Cleanup(config *PromptConfig, val interface{}) error {
return s.Render(
SelectQuestionTemplate,
SelectTemplateData{
Select: *s,
Answer: val.(core.OptionAnswer).Value,
ShowAnswer: true,
Config: config,
},
)
}

398
vendor/github.com/AlecAivazis/survey/v2/survey.go generated vendored Normal file
View File

@ -0,0 +1,398 @@
package survey
import (
"errors"
"io"
"os"
"strings"
"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: "?",
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,
},
}
}
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
Filter func(filter string, option string, index int) bool
KeepFilter bool
}
// 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
}
}
// 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
}
}
/*
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")
}
// 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)
}
// grab the user input and save it
ans, err := q.Prompt.Prompt(&options.PromptConfig)
// if there was a problem
if err != nil {
return err
}
// build up a list of validators that we have to apply to this question
validators := []Validator{}
// make sure to include the question specific one
if q.Validate != nil {
validators = append(validators, q.Validate)
}
// add any "global" validators
for _, validator := range options.Validators {
validators = append(validators, validator)
}
// apply every validator to thte response
for _, validator := range validators {
// wait for a valid response
for invalid := validator(ans); invalid != nil; invalid = validator(ans) {
err := q.Prompt.Error(&options.PromptConfig, invalid)
// if there was a problem
if err != nil {
return err
}
// ask for more input
if promptAgainer, ok := q.Prompt.(PromptAgainer); ok {
ans, err = promptAgainer.PromptAgain(&options.PromptConfig, ans, invalid)
} else {
ans, err = q.Prompt.Prompt(&options.PromptConfig)
}
// if there was a problem
if err != nil {
return err
}
}
}
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
q.Prompt.Cleanup(&options.PromptConfig, ans)
// if something went wrong
if err != nil {
// stop listening
return err
}
// add it to the map
err = core.WriteAnswer(response, q.Name, ans)
// if something went wrong
if 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
}

View File

@ -0,0 +1,22 @@
Copyright (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.

View File

@ -0,0 +1,3 @@
# 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.

View File

@ -0,0 +1,22 @@
package 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:])
}

View File

@ -0,0 +1,190 @@
// +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) {
fmt.Fprintf(c.Out, "\x1b[%dA", n)
}
// Down moves the cursor n cells to down.
func (c *Cursor) Down(n int) {
fmt.Fprintf(c.Out, "\x1b[%dB", n)
}
// Forward moves the cursor n cells to right.
func (c *Cursor) Forward(n int) {
fmt.Fprintf(c.Out, "\x1b[%dC", n)
}
// Back moves the cursor n cells to left.
func (c *Cursor) Back(n int) {
fmt.Fprintf(c.Out, "\x1b[%dD", n)
}
// NextLine moves cursor to beginning of the line n lines down.
func (c *Cursor) NextLine(n int) {
fmt.Fprintf(c.Out, "\x1b[%dE", n)
}
// PreviousLine moves cursor to beginning of the line n lines up.
func (c *Cursor) PreviousLine(n int) {
fmt.Fprintf(c.Out, "\x1b[%dF", n)
}
// HorizontalAbsolute moves cursor horizontally to x.
func (c *Cursor) HorizontalAbsolute(x int) {
fmt.Fprintf(c.Out, "\x1b[%dG", x)
}
// Show shows the cursor.
func (c *Cursor) Show() {
fmt.Fprint(c.Out, "\x1b[?25h")
}
// Hide hide the cursor.
func (c *Cursor) Hide() {
fmt.Fprint(c.Out, "\x1b[?25l")
}
// Move moves the cursor to a specific x,y location.
func (c *Cursor) Move(x int, y int) {
fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
}
// Save saves the current position
func (c *Cursor) Save() {
fmt.Fprint(c.Out, "\x1b7")
}
// Restore restores the saved position of the cursor
func (c *Cursor) Restore() {
fmt.Fprint(c.Out, "\x1b8")
}
// 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) {
if cur.Y == terminalSize.Y {
fmt.Fprintln(c.Out)
}
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
fmt.Fprint(c.Out, "\x1b[6n")
// 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()
// save the current location of the cursor
c.Save()
// 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
}
// move back where we began
c.Restore()
// show the cursor
c.Show()
// since the bottom was calculated in the lower right corner, it
// is the dimensions we are looking for
return bottom, nil
}

View File

@ -0,0 +1,138 @@
package 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) {
c.cursorMove(0, n)
}
func (c *Cursor) Down(n int) {
c.cursorMove(0, -1*n)
}
func (c *Cursor) Forward(n int) {
c.cursorMove(n, 0)
}
func (c *Cursor) Back(n int) {
c.cursorMove(-1*n, 0)
}
// save the cursor location
func (c *Cursor) Save() {
cursorLoc, _ = c.Location(nil)
}
func (c *Cursor) Restore() {
handle := syscall.Handle(c.Out.Fd())
// restore it to the original position
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
}
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) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
var cursor Coord
cursor.X = csbi.cursorPosition.X + Short(x)
cursor.Y = csbi.cursorPosition.Y + Short(y)
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
}
func (c *Cursor) NextLine(n int) {
c.Up(n)
c.HorizontalAbsolute(0)
}
func (c *Cursor) PreviousLine(n int) {
c.Down(n)
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) {
c.NextLine(1)
}
func (c *Cursor) HorizontalAbsolute(x int) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
var cursor Coord
cursor.X = Short(x)
cursor.Y = csbi.cursorPosition.Y
if csbi.size.X < cursor.X {
cursor.X = csbi.size.X
}
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
}
func (c *Cursor) Show() {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
cci.visible = 1
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
}
func (c *Cursor) Hide() {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
cci.visible = 0
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
}
func (c *Cursor) Location(buf *bytes.Buffer) (Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
return csbi.cursorPosition, nil
}
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
// windows' coordinate system begins at (0, 0)
csbi.size.X--
csbi.size.Y--
return &csbi.size, nil
}

View File

@ -0,0 +1,9 @@
package terminal
type EraseLineMode int
const (
ERASE_LINE_END EraseLineMode = iota
ERASE_LINE_START
ERASE_LINE_ALL
)

View File

@ -0,0 +1,11 @@
// +build !windows
package terminal
import (
"fmt"
)
func EraseLine(out FileWriter, mode EraseLineMode) {
fmt.Fprintf(out, "\x1b[%dK", mode)
}

View File

@ -0,0 +1,27 @@
package terminal
import (
"syscall"
"unsafe"
)
func EraseLine(out FileWriter, mode EraseLineMode) {
handle := syscall.Handle(out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
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
}
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
}

View File

@ -0,0 +1,9 @@
package terminal
import (
"errors"
)
var (
InterruptErr = errors.New("interrupt")
)

View File

@ -0,0 +1,19 @@
// +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
}

View File

@ -0,0 +1,227 @@
package terminal
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/mattn/go-isatty"
)
var (
cursorFunctions = map[rune]func(c *Cursor) func(int){
'A': func(c *Cursor) func(int) { return c.Up },
'B': func(c *Cursor) func(int) { return c.Down },
'C': func(c *Cursor) func(int) { return c.Forward },
'D': func(c *Cursor) func(int) { return c.Back },
'E': func(c *Cursor) func(int) { return c.NextLine },
'F': func(c *Cursor) func(int) { return c.PreviousLine },
'G': func(c *Cursor) func(int) { return c.HorizontalAbsolute },
}
)
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 {
ch, size, err := r.ReadRune()
if err != nil {
break
}
n += size
switch ch {
case '\x1b':
size, err = w.handleEscape(r)
n += size
if err != nil {
break
}
default:
fmt.Fprint(w.out, string(ch))
}
}
return
}
func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
buf := make([]byte, 0, 10)
buf = append(buf, "\x1b"...)
// Check '[' continues after \x1b
ch, size, err := r.ReadRune()
if 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 {
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)...)
}
w.applyEscapeCode(buf, string(argBuf), code)
return
}
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) {
c := &Cursor{Out: w.out}
switch arg + string(code) {
case "?25h":
c.Show()
return
case "?25l":
c.Hide()
return
}
if f, ok := cursorFunctions[code]; ok {
if n, err := strconv.Atoi(arg); err == nil {
f(c)(n)
return
}
}
switch code {
case 'm':
w.applySelectGraphicRendition(arg)
default:
buf = append(buf, string(code)...)
fmt.Fprint(w.out, string(buf))
}
}
// Original implementation: https://github.com/mattn/go-colorable
func (w *Writer) applySelectGraphicRendition(arg string) {
if arg == "" {
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
return
}
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
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
}
}
}
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
}

View File

@ -0,0 +1,330 @@
package terminal
import (
"fmt"
"unicode"
"golang.org/x/text/width"
)
type RuneReader struct {
stdio Stdio
cursor *Cursor
state runeReaderState
}
func NewRuneReader(stdio Stdio) *RuneReader {
return &RuneReader{
stdio: stdio,
state: newRuneReaderState(stdio.In),
}
}
func (rr *RuneReader) printChar(char rune, mask rune) {
// if we don't need to mask the input
if mask == 0 {
// just print the character the user pressed
fmt.Fprintf(rr.stdio.Out, "%c", char)
} else {
// otherwise print the mask we were given
fmt.Fprintf(rr.stdio.Out, "%c", mask)
}
}
func (rr *RuneReader) ReadLine(mask rune) ([]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,
}
// 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())
for {
// wait for some input
r, _, err := rr.ReadRune()
if err != nil {
return line, err
}
// increment cursor location
cursorCurrent.X++
// 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))
cursorCurrent.X = terminalSize.X
cursorCurrent.Y--
} else {
cursor.Back(1)
cursorCurrent.X--
}
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
fmt.Fprint(rr.stdio.Out, "\r\n")
// 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
rr.printChar(char, mask)
}
// 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--
} 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--
} 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++
} 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.X = terminalSize.X
cursorCurrent.Y--
} else {
cursor.Back(runeWidth(line[index-1]))
cursorCurrent.X--
}
index--
}
continue
// user pressed end
} else if r == SpecialKeyEnd {
for index != len(line) {
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursor.NextLine(1)
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
cursorCurrent.Y++
} else {
cursor.Forward(runeWidth(line[index]))
cursorCurrent.X++
}
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
rr.printChar(char, mask)
}
// 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++
// print out the character
rr.printChar(r, mask)
} 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
rr.printChar(char, mask)
cursorCurrent.X++
}
// 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
fmt.Fprintln(rr.stdio.Out)
// restore the position of the cursor horizontally
cursor.Restore()
// restore the position of the cursor vertically
cursor.Up(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++
}
}
}
func runeWidth(r rune) int {
switch width.LookupRune(r).Kind() {
case width.EastAsianWide, width.EastAsianFullwidth:
return 2
}
return 1
}

View File

@ -0,0 +1,13 @@
// 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.
// +build darwin dragonfly freebsd netbsd openbsd
package terminal
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
const ioctlWriteTermios = syscall.TIOCSETA

View File

@ -0,0 +1,13 @@
// 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.
// +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

View File

@ -0,0 +1,111 @@
// +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"
)
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
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
}
func (rr *RuneReader) ReadRune() (rune, int, error) {
r, size, err := rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
// parse ^[ sequences to look for arrow keys
if r == '\033' {
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
}
if r != '[' {
return r, size, fmt.Errorf("Unexpected Escape Sequence: %q", []rune{'\033', r})
}
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
switch r {
case 'D':
return KeyArrowLeft, 1, nil
case 'C':
return KeyArrowRight, 1, nil
case 'A':
return KeyArrowUp, 1, nil
case 'B':
return KeyArrowDown, 1, nil
case 'H': // Home button
return SpecialKeyHome, 1, nil
case 'F': // End button
return SpecialKeyEnd, 1, nil
case '3': // Delete Button
// discard the following '~' key from buffer
rr.state.reader.Discard(1)
return SpecialKeyDelete, 1, nil
default:
// discard the following '~' key from buffer
rr.state.reader.Discard(1)
return IgnoreKey, 1, nil
}
return r, size, fmt.Errorf("Unknown Escape Sequence: %q", []rune{'\033', '[', r})
}
return r, size, err
}

View File

@ -0,0 +1,7 @@
// +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

View File

@ -0,0 +1,142 @@
package 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
}
}

View File

@ -0,0 +1,30 @@
package 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'
)
func soundBell(out io.Writer) {
fmt.Fprint(out, "\a")
}

View File

@ -0,0 +1,24 @@
package 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
}

View File

@ -0,0 +1,39 @@
package 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
}

View File

@ -0,0 +1,8 @@
package terminal
type Short int16
type Coord struct {
X Short
Y Short
}

76
vendor/github.com/AlecAivazis/survey/v2/transform.go generated vendored Normal file
View File

@ -0,0 +1,76 @@
package survey
import (
"reflect"
"strings"
)
// 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 nil value.
// The original answer will be not affected,
// see survey.go#L125.
return nil
}
// "ans" is never nil here, so we don't have to check that
// see survey.go#L97 for more.
// Make sure that the the answer's value was a typeof string.
s, ok := ans.(string)
if !ok {
return nil
}
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(strings.Title)
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
}
}

87
vendor/github.com/AlecAivazis/survey/v2/validate.go generated vendored Normal file
View File

@ -0,0 +1,87 @@
package survey
import (
"errors"
"fmt"
"reflect"
)
// 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 {
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
}
}
// 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())
}