feat(*): 提交指定的文件

main
熊二 2 years ago
parent ad146bfd79
commit c3e0106b9b
  1. 14
      go.mod
  2. 155
      main.go
  3. 154
      survey/confirm.go
  4. 104
      survey/core/template.go
  5. 376
      survey/core/write.go
  6. 226
      survey/editor.go
  7. 1
      survey/filter.go
  8. 219
      survey/input.go
  9. 112
      survey/multiline.go
  10. 360
      survey/multiselect.go
  11. 106
      survey/password.go
  12. 195
      survey/renderer.go
  13. 329
      survey/select.go
  14. 474
      survey/survey.go
  15. 22
      survey/terminal/buffered_reader.go
  16. 209
      survey/terminal/cursor.go
  17. 164
      survey/terminal/cursor_windows.go
  18. 9
      survey/terminal/display.go
  19. 13
      survey/terminal/display_posix.go
  20. 31
      survey/terminal/display_windows.go
  21. 10
      survey/terminal/error.go
  22. 20
      survey/terminal/output.go
  23. 253
      survey/terminal/output_windows.go
  24. 417
      survey/terminal/runereader.go
  25. 14
      survey/terminal/runereader_bsd.go
  26. 14
      survey/terminal/runereader_linux.go
  27. 132
      survey/terminal/runereader_posix.go
  28. 8
      survey/terminal/runereader_ppc64le.go
  29. 142
      survey/terminal/runereader_windows.go
  30. 32
      survey/terminal/sequences.go
  31. 24
      survey/terminal/stdio.go
  32. 39
      survey/terminal/syscall_windows.go
  33. 8
      survey/terminal/terminal.go
  34. 82
      survey/transform.go
  35. 128
      survey/validate.go

@ -2,14 +2,16 @@ module git.yaojiankang.top/hupeh/gcz
go 1.19
require github.com/AlecAivazis/survey/v2 v2.3.6
require (
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-isatty v0.0.8
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
golang.org/x/text v0.3.3
)
require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
golang.org/x/text v0.3.3 // indirect
)

@ -1,8 +1,10 @@
package main
import (
"bytes"
"errors"
"fmt"
"github.com/AlecAivazis/survey/v2"
"git.yaojiankang.top/hupeh/gcz/survey"
"os"
"os/exec"
"strings"
@ -202,6 +204,7 @@ type Message struct {
Emoji Emoji
Scope string
Subject string
Files []string
}
func (m *Message) String() string {
@ -215,23 +218,145 @@ func (m *Message) String() string {
}
str += ": "
}
str += m.Subject + "\n\n"
if len(m.Emoji) > 0 {
str += ":" + m.Emoji.Flag() + ": "
}
str += m.Subject
return str
}
func (m *Message) Preview() string {
var str string
for i, s := range strings.Split(m.String(), "\n") {
if i > 0 {
str += "\n"
if len(m.Type) > 0 {
str += string(m.Type)
if len(m.Scope) > 0 {
str += "(" + m.Scope + ")"
} else {
str += "(*)"
}
str += "\t" + s
str += ": "
}
if len(m.Emoji) > 0 {
str += string(m.Emoji) + " "
}
str += m.Subject
return str
}
func (m *Message) Valid() bool {
return len(m.Type) > 0 && len(m.Subject) > 0
return len(m.Type) > 0 && len(m.Subject) > 0 && len(m.Files) > 0
}
type file struct {
Name string
Add bool
Del bool
New bool
}
func readFiles(path string, f func(string)) error {
entries, err := os.ReadDir(path)
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() {
err = readFiles(path+"/"+entry.Name(), f)
if err != nil {
return err
}
} else {
f(path + "/" + entry.Name())
}
}
return nil
}
func (m *Message) AskFiles() error {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("git", "status", "-s", "-uall")
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return err
}
if stderr.Len() > 0 {
return errors.New(stderr.String())
}
infos := make(map[string]string)
var opts []string
var defs []string
lines := strings.Split(stdout.String(), "\n")
for _, line := range lines {
if len(line) < 3 {
continue
}
// 目录表示里面文件全名新增
if strings.HasSuffix(line, "/") {
err := readFiles(strings.TrimSpace(line[2:]), func(s string) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command("git", "check-ignore", s)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
fmt.Println(err)
return
}
if stderr.Len() > 0 {
return
}
if strings.TrimSpace(stdout.String()) == s {
return
}
opts = append(opts, s)
infos[s] = "新增"
})
if err != nil {
return err
}
} else {
name := strings.TrimSpace(line[2:])
opts = append(opts, name)
switch line[1] {
case 'D':
infos[name] = "删除"
case 'A':
defs = append(defs, name)
case 'M':
infos[name] = "修改"
case 'R':
infos[name] = "重命名"
case 'T':
infos[name] = "修改文件类型"
case 'C':
infos[name] = "复制"
case 'U':
infos[name] = "更新但未合并"
case '?':
infos[name] = "新增"
}
}
}
fmt.Println(infos)
var sels []string
err := survey.AskOne(&survey.MultiSelect{
Message: "选择提交的文件",
Options: opts,
Default: defs,
Description: func(value string, index int) string {
if desc, ok := infos[value]; ok {
return desc
}
return ""
},
}, &sels)
if err != nil {
return err
}
m.Files = sels
return nil
}
func (m *Message) AskType() error {
@ -340,6 +465,9 @@ func (m *Message) AskSubject() error {
}
func (m *Message) Ask() error {
if err := m.AskFiles(); err != nil {
return err
}
if err := m.AskType(); err != nil {
return err
}
@ -364,7 +492,7 @@ func (m *Message) Confirm() (bool, error) {
}
ok := false
prompt := &survey.Confirm{
Message: "输入结果如下:\n\n" + m.Preview() + "\r立即提交?",
Message: "输入结果如下:\n\n \x1b[4m" + m.Preview() + "\x1b[0m\n\n\r立即提交?",
}
err := survey.AskOne(prompt, &ok)
if err != nil {
@ -377,13 +505,22 @@ func (m *Message) Confirm() (bool, error) {
}
func (m *Message) Commit() error {
cmd := exec.Command("git", "commit", "-m", m.String())
args := []string{"commit", "-o"}
for _, f := range m.Files {
args = append(args, "./"+f)
}
args = append(args, "-m", m.String())
cmd := exec.Command("git", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func main() {
//readFiles(".", func(s string) {
// fmt.Println(s)
//})
var msg Message
ok, err := msg.Confirm()
if err == nil && ok {

@ -0,0 +1,154 @@
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 func() {
_ = rr.RestoreTermMode()
}()
// start waiting for input
for {
line, err := rr.ReadLine(0)
if err != nil {
return false, err
}
// move back up a line to compensate for the \n echoed from terminal
cursor.PreviousLine(1)
val := string(line)
// get the answer that matches the
var answer bool
switch {
case yesRx.Match([]byte(val)):
answer = true
case noRx.Match([]byte(val)):
answer = false
case val == "":
answer = c.Default
case val == config.HelpInput && c.Help != "":
err := c.Render(
ConfirmQuestionTemplate,
ConfirmTemplateData{
Confirm: *c,
ShowHelp: true,
Config: config,
},
)
if err != nil {
// use the default value and bubble up
return c.Default, err
}
showHelp = true
continue
default:
// we didnt get a valid answer, so print error and prompt again
//lint:ignore ST1005 it should be fine for this error message to have punctuation
if err := c.Error(config, fmt.Errorf("%q is not a valid answer, please try again.", val)); err != nil {
return c.Default, err
}
err := c.Render(
ConfirmQuestionTemplate,
ConfirmTemplateData{
Confirm: *c,
ShowHelp: showHelp,
Config: config,
},
)
if err != nil {
// use the default value and bubble up
return c.Default, err
}
continue
}
return answer, nil
}
}
/*
Prompt prompts the user with a simple text field and expects a reply followed
by a carriage return.
likesPie := false
prompt := &survey.Confirm{ Message: "What is your name?" }
survey.AskOne(prompt, &likesPie)
*/
func (c *Confirm) Prompt(config *PromptConfig) (interface{}, error) {
// render the question template
err := c.Render(
ConfirmQuestionTemplate,
ConfirmTemplateData{
Confirm: *c,
Config: config,
},
)
if err != nil {
return "", err
}
// get input and return
return c.getBool(false, config)
}
// Cleanup overwrite the line with the finalized formatted version
func (c *Confirm) Cleanup(config *PromptConfig, val interface{}) error {
// if the value was previously true
ans := yesNo(val.(bool))
// render the template
return c.Render(
ConfirmQuestionTemplate,
ConfirmTemplateData{
Confirm: *c,
Answer: ans,
Config: config,
},
)
}

@ -0,0 +1,104 @@
package core
import (
"bytes"
"os"
"sync"
"text/template"
"github.com/mgutz/ansi"
)
// DisableColor can be used to make testing reliable
var DisableColor = false
var TemplateFuncsWithColor = map[string]interface{}{
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
"color": ansi.ColorCode,
}
var TemplateFuncsNoColor = map[string]interface{}{
// Templates without Color formatting. For layout/ testing.
"color": func(color string) string {
return ""
},
}
// envColorDisabled returns if output colors are forbid by environment variables
func envColorDisabled() bool {
return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0"
}
// envColorForced returns if output colors are forced from environment variables
func envColorForced() bool {
val, ok := os.LookupEnv("CLICOLOR_FORCE")
return ok && val != "0"
}
// RunTemplate returns two formatted strings given a template and
// the data it requires. The first string returned is generated for
// user-facing output and may or may not contain ANSI escape codes
// for colored output. The second string does not contain escape codes
// and can be used by the renderer for layout purposes.
func RunTemplate(tmpl string, data interface{}) (string, string, error) {
tPair, err := GetTemplatePair(tmpl)
if err != nil {
return "", "", err
}
userBuf := bytes.NewBufferString("")
err = tPair[0].Execute(userBuf, data)
if err != nil {
return "", "", err
}
layoutBuf := bytes.NewBufferString("")
err = tPair[1].Execute(layoutBuf, data)
if err != nil {
return userBuf.String(), "", err
}
return userBuf.String(), layoutBuf.String(), err
}
var (
memoizedGetTemplate = map[string][2]*template.Template{}
memoMutex = &sync.RWMutex{}
)
// GetTemplatePair returns a pair of compiled templates where the
// first template is generated for user-facing output and the
// second is generated for use by the renderer. The second
// template does not contain any color escape codes, whereas
// the first template may or may not depending on DisableColor.
func GetTemplatePair(tmpl string) ([2]*template.Template, error) {
memoMutex.RLock()
if t, ok := memoizedGetTemplate[tmpl]; ok {
memoMutex.RUnlock()
return t, nil
}
memoMutex.RUnlock()
templatePair := [2]*template.Template{nil, nil}
templateNoColor, err := template.New("prompt").Funcs(TemplateFuncsNoColor).Parse(tmpl)
if err != nil {
return [2]*template.Template{}, err
}
templatePair[1] = templateNoColor
envColorHide := envColorDisabled() && !envColorForced()
if DisableColor || envColorHide {
templatePair[0] = templatePair[1]
} else {
templateWithColor, err := template.New("prompt").Funcs(TemplateFuncsWithColor).Parse(tmpl)
templatePair[0] = templateWithColor
if err != nil {
return [2]*template.Template{}, err
}
}
memoMutex.Lock()
memoizedGetTemplate[tmpl] = templatePair
memoMutex.Unlock()
return templatePair, nil
}

@ -0,0 +1,376 @@
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
}
type reflectField struct {
value reflect.Value
fieldType reflect.StructField
}
func OptionAnswerList(incoming []string) []OptionAnswer {
list := []OptionAnswer{}
for i, opt := range incoming {
list = append(list, OptionAnswer{Value: opt, Index: i})
}
return list
}
func WriteAnswer(t interface{}, name string, v interface{}) (err error) {
// if the field is a custom type
if s, ok := t.(Settable); ok {
// use the interface method
return s.WriteAnswer(name, v)
}
// the target to write to
target := reflect.ValueOf(t)
// the value to write from
value := reflect.ValueOf(v)
// make sure we are writing to a pointer
if target.Kind() != reflect.Ptr {
return errors.New("you must pass a pointer as the target of a Write operation")
}
// the object "inside" of the target pointer
elem := target.Elem()
// handle the special types
switch elem.Kind() {
// if we are writing to a struct
case reflect.Struct:
// if we are writing to an option answer than we want to treat
// it like a single thing and not a place to deposit answers
if elem.Type().Name() == "OptionAnswer" {
// copy the value over to the normal struct
return copy(elem, value)
}
// get the name of the field that matches the string we were given
field, _, err := findField(elem, name)
// if something went wrong
if err != nil {
// bubble up
return err
}
// handle references to the Settable interface aswell
if s, ok := field.Interface().(Settable); ok {
// use the interface method
return s.WriteAnswer(name, v)
}
if field.CanAddr() {
if s, ok := field.Addr().Interface().(Settable); ok {
// use the interface method
return s.WriteAnswer(name, v)
}
}
// copy the value over to the normal struct
return copy(field, value)
case reflect.Map:
mapType := reflect.TypeOf(t).Elem()
if mapType.Key().Kind() != reflect.String {
return errors.New("answer maps key must be of type string")
}
// copy only string value/index value to map if,
// map is not of type interface and is 'OptionAnswer'
if value.Type().Name() == "OptionAnswer" {
if kval := mapType.Elem().Kind(); kval == reflect.String {
mt := *t.(*map[string]string)
mt[name] = value.FieldByName("Value").String()
return nil
} else if kval == reflect.Int {
mt := *t.(*map[string]int)
mt[name] = int(value.FieldByName("Index").Int())
return nil
}
}
if mapType.Elem().Kind() != reflect.Interface {
return errors.New("answer maps must be of type map[string]interface")
}
mt := *t.(*map[string]interface{})
mt[name] = value.Interface()
return nil
}
// otherwise just copy the value to the target
return copy(elem, value)
}
type errFieldNotMatch struct {
questionName string
}
func (err errFieldNotMatch) Error() string {
return fmt.Sprintf("could not find field matching %v", err.questionName)
}
func (err errFieldNotMatch) Is(target error) bool { // implements the dynamic errors.Is interface.
if target != nil {
if name, ok := IsFieldNotMatch(target); ok {
// if have a filled questionName then perform "deeper" comparison.
return name == "" || err.questionName == "" || name == err.questionName
}
}
return false
}
// IsFieldNotMatch reports whether an "err" is caused by a non matching field.
// It returns the Question.Name that couldn't be matched with a destination field.
//
// Usage:
//
// if err := survey.Ask(qs, &v); err != nil {
// if name, ok := core.IsFieldNotMatch(err); ok {
// // name is the question name that did not match a field
// }
// }
func IsFieldNotMatch(err error) (string, bool) {
if err != nil {
if v, ok := err.(errFieldNotMatch); ok {
return v.questionName, true
}
}
return "", false
}
// BUG(AlecAivazis): the current implementation might cause weird conflicts if there are
// two fields with same name that only differ by casing.
func findField(s reflect.Value, name string) (reflect.Value, reflect.StructField, error) {
fields := flattenFields(s)
// first look for matching tags so we can overwrite matching field names
for _, f := range fields {
// the value of the survey tag
tag := f.fieldType.Tag.Get(tagName)
// if the tag matches the name we are looking for
if tag != "" && tag == name {
// then we found our index
return f.value, f.fieldType, nil
}
}
// then look for matching names
for _, f := range fields {
// if the name of the field matches what we're looking for
if strings.EqualFold(f.fieldType.Name, name) {
return f.value, f.fieldType, nil
}
}
// we didn't find the field
return reflect.Value{}, reflect.StructField{}, errFieldNotMatch{name}
}
func flattenFields(s reflect.Value) []reflectField {
sType := s.Type()
numField := sType.NumField()
fields := make([]reflectField, 0, numField)
for i := 0; i < numField; i++ {
fieldType := sType.Field(i)
field := s.Field(i)
if field.Kind() == reflect.Struct && fieldType.Anonymous {
// field is a promoted structure
fields = append(fields, flattenFields(field)...)
continue
}
fields = append(fields, reflectField{field, fieldType})
}
return fields
}
// isList returns true if the element is something we can Len()
func isList(v reflect.Value) bool {
switch v.Type().Kind() {
case reflect.Array, reflect.Slice:
return true
default:
return false
}
}
// Write takes a value and copies it to the target
func copy(t reflect.Value, v reflect.Value) (err error) {
// if something ends up panicing we need to catch it in a deferred func
defer func() {
if r := recover(); r != nil {
// if we paniced with an error
if _, ok := r.(error); ok {
// cast the result to an error object
err = r.(error)
} else if _, ok := r.(string); ok {
// otherwise we could have paniced with a string so wrap it in an error
err = errors.New(r.(string))
}
}
}()
// if we are copying from a string result to something else
if v.Kind() == reflect.String && v.Type() != t.Type() {
var castVal interface{}
var casterr error
vString := v.Interface().(string)
switch t.Kind() {
case reflect.Bool:
castVal, casterr = strconv.ParseBool(vString)
case reflect.Int:
castVal, casterr = strconv.Atoi(vString)
case reflect.Int8:
var val64 int64
val64, casterr = strconv.ParseInt(vString, 10, 8)
if casterr == nil {
castVal = int8(val64)
}
case reflect.Int16:
var val64 int64
val64, casterr = strconv.ParseInt(vString, 10, 16)
if casterr == nil {
castVal = int16(val64)
}
case reflect.Int32:
var val64 int64
val64, casterr = strconv.ParseInt(vString, 10, 32)
if casterr == nil {
castVal = int32(val64)
}
case reflect.Int64:
if t.Type() == reflect.TypeOf(time.Duration(0)) {
castVal, casterr = time.ParseDuration(vString)
} else {
castVal, casterr = strconv.ParseInt(vString, 10, 64)
}
case reflect.Uint:
var val64 uint64
val64, casterr = strconv.ParseUint(vString, 10, 8)
if casterr == nil {
castVal = uint(val64)
}
case reflect.Uint8:
var val64 uint64
val64, casterr = strconv.ParseUint(vString, 10, 8)
if casterr == nil {
castVal = uint8(val64)
}
case reflect.Uint16:
var val64 uint64
val64, casterr = strconv.ParseUint(vString, 10, 16)
if casterr == nil {
castVal = uint16(val64)
}
case reflect.Uint32:
var val64 uint64
val64, casterr = strconv.ParseUint(vString, 10, 32)
if casterr == nil {
castVal = uint32(val64)
}
case reflect.Uint64:
castVal, casterr = strconv.ParseUint(vString, 10, 64)
case reflect.Float32:
var val64 float64
val64, casterr = strconv.ParseFloat(vString, 32)
if casterr == nil {
castVal = float32(val64)
}
case reflect.Float64:
castVal, casterr = strconv.ParseFloat(vString, 64)
default:
//lint:ignore ST1005 allow this error message to be capitalized
return fmt.Errorf("Unable to convert from string to type %s", t.Kind())
}
if casterr != nil {
return casterr
}
t.Set(reflect.ValueOf(castVal))
return
}
// if we are copying from an OptionAnswer to something
if v.Type().Name() == "OptionAnswer" {
// copying an option answer to a string
if t.Kind() == reflect.String {
// copies the Value field of the struct
t.Set(reflect.ValueOf(v.FieldByName("Value").Interface()))
return
}
// copying an option answer to an int
if t.Kind() == reflect.Int {
// copies the Index field of the struct
t.Set(reflect.ValueOf(v.FieldByName("Index").Interface()))
return
}
// copying an OptionAnswer to an OptionAnswer
if t.Type().Name() == "OptionAnswer" {
t.Set(v)
return
}
// we're copying an option answer to an incorrect type
//lint:ignore ST1005 allow this error message to be capitalized
return fmt.Errorf("Unable to convert from OptionAnswer to type %s", t.Kind())
}
// if we are copying from one slice or array to another
if isList(v) && isList(t) {
// loop over every item in the desired value
for i := 0; i < v.Len(); i++ {
// write to the target given its kind
switch t.Kind() {
// if its a slice
case reflect.Slice:
// an object of the correct type
obj := reflect.Indirect(reflect.New(t.Type().Elem()))
// write the appropriate value to the obj and catch any errors
if err := copy(obj, v.Index(i)); err != nil {
return err
}
// just append the value to the end
t.Set(reflect.Append(t, obj))
// otherwise it could be an array
case reflect.Array:
// set the index to the appropriate value
if err := copy(t.Slice(i, i+1).Index(0), v.Index(i)); err != nil {
return err
}
}
}
} else {
// set the value to the target
t.Set(v)
}
// we're done
return
}

@ -0,0 +1,226 @@
package survey
import (
"bytes"
"io/ioutil"
"os"
"os/exec"
"runtime"
"git.yaojiankang.top/hupeh/gcz/survey/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 func() {
_ = rr.RestoreTermMode()
}()
cursor := e.NewCursor()
cursor.Hide()
defer cursor.Show()
for {
r, _, err := rr.ReadRune()
if err != nil {
return "", err
}
if r == '\r' || r == '\n' {
break
}
if r == terminal.KeyInterrupt {
return "", terminal.InterruptErr
}
if r == terminal.KeyEndTransmission {
break
}
if string(r) == config.HelpInput && e.Help != "" {
err = e.Render(
EditorQuestionTemplate,
EditorTemplateData{
Editor: *e,
ShowHelp: true,
Config: config,
},
)
if err != nil {
return "", err
}
}
continue
}
// prepare the temp file
pattern := e.FileName
if pattern == "" {
pattern = "survey*.txt"
}
f, err := ioutil.TempFile("", pattern)
if err != nil {
return "", err
}
defer func() {
_ = os.Remove(f.Name())
}()
// write utf8 BOM header
// The reason why we do this is because notepad.exe on Windows determines the
// encoding of an "empty" text file by the locale, for example, GBK in China,
// while golang string only handles utf8 well. However, a text file with utf8
// BOM header is not considered "empty" on Windows, and the encoding will then
// be determined utf8 by notepad.exe, instead of GBK or other encodings.
if _, err := f.Write(bom); err != nil {
return "", err
}
// write initial value
if _, err := f.WriteString(initialValue); err != nil {
return "", err
}
// close the fd to prevent the editor unable to save file
if err := f.Close(); err != nil {
return "", err
}
// check is input editor exist
if e.Editor != "" {
editor = e.Editor
}
stdio := e.Stdio()
args, err := shellquote.Split(editor)
if err != nil {
return "", err
}
args = append(args, f.Name())
// open the editor
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = stdio.In
cmd.Stdout = stdio.Out
cmd.Stderr = stdio.Err
cursor.Show()
if err := cmd.Run(); err != nil {
return "", err
}
// raw is a BOM-unstripped UTF8 byte slice
raw, err := ioutil.ReadFile(f.Name())
if err != nil {
return "", err
}
// strip BOM header
text := string(bytes.TrimPrefix(raw, bom))
// check length, return default value on empty
if len(text) == 0 && !e.AppendDefault {
return e.Default, nil
}
return text, nil
}
func (e *Editor) Cleanup(config *PromptConfig, val interface{}) error {
return e.Render(
EditorQuestionTemplate,
EditorTemplateData{
Editor: *e,
Answer: "<Received>",
ShowAnswer: true,
Config: config,
},
)
}

@ -0,0 +1 @@
package survey

@ -0,0 +1,219 @@
package survey
import (
"errors"
"git.yaojiankang.top/hupeh/gcz/survey/core"
"git.yaojiankang.top/hupeh/gcz/survey/terminal"
)
/*
Input is a regular text input that prints each character the user types on the screen
and accepts the input with the enter key. Response type is a string.
name := ""
prompt := &survey.Input{ Message: "What is your name?" }
survey.AskOne(prompt, &name)
*/
type Input struct {
Renderer
Message string
Default string
Help string
Suggest func(toComplete string) []string
answer string
typedAnswer string
options []core.OptionAnswer
selectedIndex int
showingHelp bool
}
// data available to the templates when processing
type InputTemplateData struct {
Input
ShowAnswer bool
ShowHelp bool
Answer string
PageEntries []core.OptionAnswer
SelectedIndex int
Config *PromptConfig
}
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
var InputQuestionTemplate = `
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
{{- if .ShowAnswer}}
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
{{- else if .PageEntries -}}
{{- .Answer}} [Use arrows to move, enter to select, type to continue]
{{- "\n"}}
{{- range $ix, $choice := .PageEntries}}
{{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
{{- $choice.Value}}
{{- color "reset"}}{{"\n"}}
{{- end}}
{{- else }}
{{- if or (and .Help (not .ShowHelp)) .Suggest }}{{color "cyan"}}[
{{- if and .Help (not .ShowHelp)}}{{ print .Config.HelpInput }} for help {{- if and .Suggest}}, {{end}}{{end -}}
{{- if and .Suggest }}{{color "cyan"}}{{ print .Config.SuggestInput }} for suggestions{{end -}}
]{{color "reset"}} {{end}}
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
{{- end}}`
func (i *Input) onRune(config *PromptConfig) terminal.OnRuneFn {
return terminal.OnRuneFn(func(key rune, line []rune) ([]rune, bool, error) {
if i.options != nil && (key == terminal.KeyEnter || key == '\n') {
return []rune(i.answer), true, nil
} else if i.options != nil && key == terminal.KeyEscape {
i.answer = i.typedAnswer
i.options = nil
} else if key == terminal.KeyArrowUp && len(i.options) > 0 {
if i.selectedIndex == 0 {
i.selectedIndex = len(i.options) - 1
} else {
i.selectedIndex--
}
i.answer = i.options[i.selectedIndex].Value
} else if (key == terminal.KeyArrowDown || key == terminal.KeyTab) && len(i.options) > 0 {
if i.selectedIndex == len(i.options)-1 {
i.selectedIndex = 0
} else {
i.selectedIndex++
}
i.answer = i.options[i.selectedIndex].Value
} else if key == terminal.KeyTab && i.Suggest != nil {
i.answer = string(line)
i.typedAnswer = i.answer
options := i.Suggest(i.answer)
i.selectedIndex = 0
if len(options) == 0 {
return line, false, nil
}
i.answer = options[0]
if len(options) == 1 {
i.typedAnswer = i.answer
i.options = nil
} else {
i.options = core.OptionAnswerList(options)
}
} else {
if i.options == nil {
return line, false, nil
}
if key >= terminal.KeySpace {
i.answer += string(key)
}
i.typedAnswer = i.answer
i.options = nil
}
pageSize := config.PageSize
opts, idx := paginate(pageSize, i.options, i.selectedIndex)
err := i.Render(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
Answer: i.answer,
ShowHelp: i.showingHelp,
SelectedIndex: idx,
PageEntries: opts,
Config: config,
},
)
if err == nil {
err = errReadLineAgain
}
return []rune(i.typedAnswer), true, err
})
}
var errReadLineAgain = errors.New("read line again")
func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
// render the template
err := i.Render(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
Config: config,
ShowHelp: i.showingHelp,
},
)
if err != nil {
return "", err
}
// start reading runes from the standard in
rr := i.NewRuneReader()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
cursor := i.NewCursor()
if !config.ShowCursor {
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
}
var line []rune
for {
if i.options != nil {
line = []rune{}
}
line, err = rr.ReadLineWithDefault(0, line, i.onRune(config))
if err == errReadLineAgain {
continue
}
if err != nil {
return "", err
}
break
}
i.answer = string(line)
// readline print an empty line, go up before we render the follow up
cursor.Up(1)
// if we ran into the help string
if i.answer == config.HelpInput && i.Help != "" {
// show the help and prompt again
i.showingHelp = true
return i.Prompt(config)
}
// if the line is empty
if len(i.answer) == 0 {
// use the default value
return i.Default, err
}
lineStr := i.answer
i.AppendRenderedText(lineStr)
// we're done
return lineStr, err
}
func (i *Input) Cleanup(config *PromptConfig, val interface{}) error {
return i.Render(
InputQuestionTemplate,
InputTemplateData{
Input: *i,
ShowAnswer: true,
Config: config,
Answer: val.(string),
},
)
}

@ -0,0 +1,112 @@
package survey
import (
"strings"
"git.yaojiankang.top/hupeh/gcz/survey/terminal"
)
type Multiline struct {
Renderer
Message string
Default string
Help string
}
// data available to the templates when processing
type MultilineTemplateData struct {
Multiline
Answer string
ShowAnswer bool
ShowHelp bool
Config *PromptConfig
}
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
var MultilineQuestionTemplate = `
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
{{- if .ShowAnswer}}
{{- "\n"}}{{color "cyan"}}{{.Answer}}{{color "reset"}}
{{- if .Answer }}{{ "\n" }}{{ end }}
{{- else }}
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
{{- color "cyan"}}[Enter 2 empty lines to finish]{{color "reset"}}
{{- end}}`
func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) {
// render the template
err := i.Render(
MultilineQuestionTemplate,
MultilineTemplateData{
Multiline: *i,
Config: config,
},
)
if err != nil {
return "", err
}
// start reading runes from the standard in
rr := i.NewRuneReader()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
cursor := i.NewCursor()
multiline := make([]string, 0)
emptyOnce := false
// get the next line
for {
var line []rune
line, err = rr.ReadLine(0)
if err != nil {
return string(line), err
}
if string(line) == "" {
if emptyOnce {
numLines := len(multiline) + 2
cursor.PreviousLine(numLines)
for j := 0; j < numLines; j++ {
terminal.EraseLine(i.Stdio().Out, terminal.ERASE_LINE_ALL)
cursor.NextLine(1)
}
cursor.PreviousLine(numLines)
break
}
emptyOnce = true
} else {
emptyOnce = false
}
multiline = append(multiline, string(line))
}
val := strings.Join(multiline, "\n")
val = strings.TrimSpace(val)
// if the line is empty
if len(val) == 0 {
// use the default value
return i.Default, err
}
i.AppendRenderedText(val)
return val, err
}
func (i *Multiline) Cleanup(config *PromptConfig, val interface{}) error {
return i.Render(
MultilineQuestionTemplate,
MultilineTemplateData{
Multiline: *i,
Answer: val.(string),
ShowAnswer: true,
Config: config,
},
)
}

@ -0,0 +1,360 @@
package survey
import (
"errors"
"fmt"
"git.yaojiankang.top/hupeh/gcz/survey/core"
"git.yaojiankang.top/hupeh/gcz/survey/terminal"
)
/*
MultiSelect is a prompt that presents a list of various options to the user
for them to select using the arrow keys and enter. Response type is a slice of strings.
days := []string{}
prompt := &survey.MultiSelect{
Message: "What days do you prefer:",
Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
}
survey.AskOne(prompt, &days)
*/
type MultiSelect struct {
Renderer
Message string
Options []string
Default interface{}
Help string
PageSize int
VimMode bool
FilterMessage string
Filter func(filter string, value string, index int) bool
Description func(value string, index int) string
filter string
selectedIndex int
checked map[int]bool
showingHelp bool
}
// data available to the templates when processing
type MultiSelectTemplateData struct {
MultiSelect
Answer string
ShowAnswer bool
Checked map[int]bool
SelectedIndex int
ShowHelp bool
Description func(value string, index int) string
PageEntries []core.OptionAnswer
Config *PromptConfig
// These fields are used when rendering an individual option
CurrentOpt core.OptionAnswer
CurrentIndex int
}
// IterateOption sets CurrentOpt and CurrentIndex appropriately so a multiselect option can be rendered individually
func (m MultiSelectTemplateData) IterateOption(ix int, opt core.OptionAnswer) interface{} {
copy := m
copy.CurrentIndex = ix
copy.CurrentOpt = opt
return copy
}
func (m MultiSelectTemplateData) GetDescription(opt core.OptionAnswer) string {
if m.Description == nil {
return ""
}
return m.Description(opt.Value, opt.Index)
}
var MultiSelectQuestionTemplate = `
{{- define "option"}}
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
{{- if index .Checked .CurrentOpt.Index }}{{color .Config.Icons.MarkedOption.Format }} {{ .Config.Icons.MarkedOption.Text }} {{else}}{{color .Config.Icons.UnmarkedOption.Format }} {{ .Config.Icons.UnmarkedOption.Text }} {{end}}
{{- color "reset"}}
{{- " "}}{{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{color "reset"}}{{end}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
{{- else }}
{{- " "}}{{- color "cyan"}}[Use arrows to move, space to select,{{- if not .Config.RemoveSelectAll }} <right> to all,{{end}}{{- if not .Config.RemoveSelectNone }} <left> to none,{{end}} type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
{{- "\n"}}
{{- range $ix, $option := .PageEntries}}
{{- template "option" $.IterateOption $ix $option}}
{{- end}}
{{- end}}`
// OnChange is called on every keypress.
func (m *MultiSelect) OnChange(key rune, config *PromptConfig) {
options := m.filterOptions(config)
oldFilter := m.filter
if key == terminal.KeyArrowUp || (m.VimMode && key == 'k') {
// if we are at the top of the list
if m.selectedIndex == 0 {
// go to the bottom
m.selectedIndex = len(options) - 1
} else {
// decrement the selected index
m.selectedIndex--
}
} else if key == terminal.KeyTab || key == terminal.KeyArrowDown || (m.VimMode && key == 'j') {
// if we are at the bottom of the list
if m.selectedIndex == len(options)-1 {
// start at the top
m.selectedIndex = 0
} else {
// increment the selected index
m.selectedIndex++
}
// if the user pressed down and there is room to move
} else if key == terminal.KeySpace {
// the option they have selected
if m.selectedIndex < len(options) {
selectedOpt := options[m.selectedIndex]
// if we haven't seen this index before
if old, ok := m.checked[selectedOpt.Index]; !ok {
// set the value to true
m.checked[selectedOpt.Index] = true
} else {
// otherwise just invert the current value
m.checked[selectedOpt.Index] = !old
}
if !config.KeepFilter {
m.filter = ""
}
}
// only show the help message if we have one to show
} else if string(key) == config.HelpInput && m.Help != "" {
m.showingHelp = true
} else if key == terminal.KeyEscape {
m.VimMode = !m.VimMode
} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
m.filter = ""
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
if m.filter != "" {
runeFilter := []rune(m.filter)
m.filter = string(runeFilter[0 : len(runeFilter)-1])
}
} else if key >= terminal.KeySpace {
m.filter += string(key)
m.VimMode = false
} else if !config.RemoveSelectAll && key == terminal.KeyArrowRight {
for _, v := range options {
m.checked[v.Index] = true
}
if !config.KeepFilter {
m.filter = ""
}
} else if !config.RemoveSelectNone && key == terminal.KeyArrowLeft {
for _, v := range options {
m.checked[v.Index] = false
}
if !config.KeepFilter {
m.filter = ""
}
}
m.FilterMessage = ""
if m.filter != "" {
m.FilterMessage = " " + m.filter
}
if oldFilter != m.filter {
// filter changed
options = m.filterOptions(config)
if len(options) > 0 && len(options) <= m.selectedIndex {
m.selectedIndex = len(options) - 1
}
}
// paginate the options
// figure out the page size
pageSize := m.PageSize
// if we dont have a specific one
if pageSize == 0 {
// grab the global value
pageSize = config.PageSize
}
// TODO if we have started filtering and were looking at the end of a list
// and we have modified the filter then we should move the page back!
opts, idx := paginate(pageSize, options, m.selectedIndex)
tmplData := MultiSelectTemplateData{
MultiSelect: *m,
SelectedIndex: idx,
Checked: m.checked,
ShowHelp: m.showingHelp,
Description: m.Description,
PageEntries: opts,
Config: config,
}
// render the options
_ = m.RenderWithCursorOffset(MultiSelectQuestionTemplate, tmplData, opts, idx)
}
func (m *MultiSelect) filterOptions(config *PromptConfig) []core.OptionAnswer {
// the filtered list
answers := []core.OptionAnswer{}
// if there is no filter applied
if m.filter == "" {
// return all of the options
return core.OptionAnswerList(m.Options)
}
// the filter to apply
filter := m.Filter
if filter == nil {
filter = config.Filter
}
// apply the filter to each option
for i, opt := range m.Options {
// i the filter says to include the option
if filter(m.filter, opt, i) {
answers = append(answers, core.OptionAnswer{
Index: i,
Value: opt,
})
}
}
// we're done here
return answers
}
func (m *MultiSelect) Prompt(config *PromptConfig) (interface{}, error) {
// compute the default state
m.checked = make(map[int]bool)
// if there is a default
if m.Default != nil {
// if the default is string values
if defaultValues, ok := m.Default.([]string); ok {
for _, dflt := range defaultValues {
for i, opt := range m.Options {
// if the option corresponds to the default
if opt == dflt {
// we found our initial value
m.checked[i] = true
// stop looking
break
}
}
}
// if the default value is index values
} else if defaultIndices, ok := m.Default.([]int); ok {
// go over every index we need to enable by default
for _, idx := range defaultIndices {
// and enable it
m.checked[idx] = true
}
}
}
// if there are no options to render
if len(m.Options) == 0 {
// we failed
return "", errors.New("please provide options to select from")
}
// figure out the page size
pageSize := m.PageSize
// if we dont have a specific one
if pageSize == 0 {
// grab the global value
pageSize = config.PageSize
}
// paginate the options
// build up a list of option answers
opts, idx := paginate(pageSize, core.OptionAnswerList(m.Options), m.selectedIndex)
cursor := m.NewCursor()
cursor.Save() // for proper cursor placement during selection
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
defer cursor.Restore() // clear any accessibility offsetting on exit
tmplData := MultiSelectTemplateData{
MultiSelect: *m,
SelectedIndex: idx,
Description: m.Description,
Checked: m.checked,
PageEntries: opts,
Config: config,
}
// ask the question
err := m.RenderWithCursorOffset(MultiSelectQuestionTemplate, tmplData, opts, idx)
if err != nil {
return "", err
}
rr := m.NewRuneReader()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
// start waiting for input
for {
r, _, err := rr.ReadRune()
if err != nil {
return "", err
}
if r == '\r' || r == '\n' {
break
}
if r == terminal.KeyInterrupt {
return "", terminal.InterruptErr
}
if r == terminal.KeyEndTransmission {
break
}
m.OnChange(r, config)
}
m.filter = ""
m.FilterMessage = ""
answers := []core.OptionAnswer{}
for i, option := range m.Options {
if val, ok := m.checked[i]; ok && val {
answers = append(answers, core.OptionAnswer{Value: option, Index: i})
}
}
return answers, nil
}
// Cleanup removes the options section, and renders the ask like a normal question.
func (m *MultiSelect) Cleanup(config *PromptConfig, val interface{}) error {
// the answer to show
answer := ""
for _, ans := range val.([]core.OptionAnswer) {
answer = fmt.Sprintf("%s, %s", answer, ans.Value)
}
// if we answered anything
if len(answer) > 2 {
// remove the precending commas
answer = answer[2:]
}
// execute the output summary template with the answer
return m.Render(
MultiSelectQuestionTemplate,
MultiSelectTemplateData{
MultiSelect: *m,
SelectedIndex: m.selectedIndex,
Checked: m.checked,
Answer: answer,
ShowAnswer: true,
Description: m.Description,
Config: config,
},
)
}

@ -0,0 +1,106 @@
package survey
import (
"fmt"
"strings"
"git.yaojiankang.top/hupeh/gcz/survey/core"
"git.yaojiankang.top/hupeh/gcz/survey/terminal"
)
/*
Password is like a normal Input but the text shows up as *'s and there is no default. Response
type is a string.
password := ""
prompt := &survey.Password{ Message: "Please type your password" }
survey.AskOne(prompt, &password)
*/
type Password struct {
Renderer
Message string
Help string
}
type PasswordTemplateData struct {
Password
ShowHelp bool
Config *PromptConfig
}
// PasswordQuestionTemplate is a template with color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
var PasswordQuestionTemplate = `
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}`
func (p *Password) Prompt(config *PromptConfig) (interface{}, error) {
// render the question template
userOut, _, err := core.RunTemplate(
PasswordQuestionTemplate,
PasswordTemplateData{
Password: *p,
Config: config,
},
)
if err != nil {
return "", err
}
if _, err := fmt.Fprint(terminal.NewAnsiStdout(p.Stdio().Out), userOut); err != nil {
return "", err
}
rr := p.NewRuneReader()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
// no help msg? Just return any response
if p.Help == "" {
line, err := rr.ReadLine(config.HideCharacter)
return string(line), err
}
cursor := p.NewCursor()
var line []rune
// process answers looking for help prompt answer
for {
line, err = rr.ReadLine(config.HideCharacter)
if err != nil {
return string(line), err
}
if string(line) == config.HelpInput {
// terminal will echo the \n so we need to jump back up one row
cursor.PreviousLine(1)
err = p.Render(
PasswordQuestionTemplate,
PasswordTemplateData{
Password: *p,
ShowHelp: true,
Config: config,
},
)
if err != nil {
return "", err
}
continue
}
break
}
lineStr := string(line)
p.AppendRenderedText(strings.Repeat(string(config.HideCharacter), len(lineStr)))
return lineStr, err
}
// Cleanup hides the string with a fixed number of characters.
func (prompt *Password) Cleanup(config *PromptConfig, val interface{}) error {
return nil
}

@ -0,0 +1,195 @@
package survey
import (
"bytes"
"fmt"
"git.yaojiankang.top/hupeh/gcz/survey/core"
"git.yaojiankang.top/hupeh/gcz/survey/terminal"
"golang.org/x/term"
)
type Renderer struct {
stdio terminal.Stdio
renderedErrors bytes.Buffer
renderedText bytes.Buffer
}
type ErrorTemplateData struct {
Error error
Icon Icon
}
var ErrorTemplate = `{{color .Icon.Format }}{{ .Icon.Text }} Sorry, your reply was invalid: {{ .Error.Error }}{{color "reset"}}
`
func (r *Renderer) WithStdio(stdio terminal.Stdio) {
r.stdio = stdio
}
func (r *Renderer) Stdio() terminal.Stdio {
return r.stdio
}
func (r *Renderer) NewRuneReader() *terminal.RuneReader {
return terminal.NewRuneReader(r.stdio)
}
func (r *Renderer) NewCursor() *terminal.Cursor {
return &terminal.Cursor{
In: r.stdio.In,
Out: r.stdio.Out,
}
}
func (r *Renderer) Error(config *PromptConfig, invalid error) error {
// cleanup the currently rendered errors
r.resetPrompt(r.countLines(r.renderedErrors))
r.renderedErrors.Reset()
// cleanup the rest of the prompt
r.resetPrompt(r.countLines(r.renderedText))
r.renderedText.Reset()
userOut, layoutOut, err := core.RunTemplate(ErrorTemplate, &ErrorTemplateData{
Error: invalid,
Icon: config.Icons.Error,
})
if err != nil {
return err
}
// send the message to the user
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
return err
}
// add the printed text to the rendered error buffer so we can cleanup later
r.appendRenderedError(layoutOut)
return nil
}
func (r *Renderer) OffsetCursor(offset int) {
cursor := r.NewCursor()
for offset > 0 {
cursor.PreviousLine(1)
offset--
}
}
func (r *Renderer) Render(tmpl string, data interface{}) error {
// cleanup the currently rendered text
lineCount := r.countLines(r.renderedText)
r.resetPrompt(lineCount)
r.renderedText.Reset()
// render the template summarizing the current state
userOut, layoutOut, err := core.RunTemplate(tmpl, data)
if err != nil {
return err
}
// print the summary
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
return err
}
// add the printed text to the rendered text buffer so we can cleanup later
r.AppendRenderedText(layoutOut)
// nothing went wrong
return nil
}
func (r *Renderer) RenderWithCursorOffset(tmpl string, data IterableOpts, opts []core.OptionAnswer, idx int) error {
cursor := r.NewCursor()
cursor.Restore() // clear any accessibility offsetting
if err := r.Render(tmpl, data); err != nil {
return err
}
cursor.Save()
offset := computeCursorOffset(MultiSelectQuestionTemplate, data, opts, idx, r.termWidthSafe())
r.OffsetCursor(offset)
return nil
}
// appendRenderedError appends text to the renderer's error buffer
// which is used to track what has been printed. It is not exported
// as errors should only be displayed via Error(config, error).
func (r *Renderer) appendRenderedError(text string) {
r.renderedErrors.WriteString(text)
}
// AppendRenderedText appends text to the renderer's text buffer
// which is used to track of what has been printed. The buffer is used
// to calculate how many lines to erase before updating the prompt.
func (r *Renderer) AppendRenderedText(text string) {
r.renderedText.WriteString(text)
}
func (r *Renderer) resetPrompt(lines int) {
// clean out current line in case tmpl didnt end in newline
cursor := r.NewCursor()
cursor.HorizontalAbsolute(0)
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
// clean up what we left behind last time
for i := 0; i < lines; i++ {
cursor.PreviousLine(1)
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
}
}
func (r *Renderer) termWidth() (int, error) {
fd := int(r.stdio.Out.Fd())
termWidth, _, err := term.GetSize(fd)
return termWidth, err
}
func (r *Renderer) termWidthSafe() int {
w, err := r.termWidth()
if err != nil || w == 0 {
// if we got an error due to terminal.GetSize not being supported
// on current platform then just assume a very wide terminal
w = 10000
}
return w
}
// countLines will return the count of `\n` with the addition of any
// lines that have wrapped due to narrow terminal width
func (r *Renderer) countLines(buf bytes.Buffer) int {
w := r.termWidthSafe()
bufBytes := buf.Bytes()
count := 0
curr := 0
for curr < len(bufBytes) {
var delim int
// read until the next newline or the end of the string
relDelim := bytes.IndexRune(bufBytes[curr:], '\n')
if relDelim != -1 {
count += 1 // new line found, add it to the count
delim = curr + relDelim
} else {
delim = len(bufBytes) // no new line found, read rest of text
}
str := string(bufBytes[curr:delim])
if lineWidth := terminal.StringWidth(str); lineWidth > w {
// account for word wrapping
count += lineWidth / w
if (lineWidth % w) == 0 {
// content whose width is exactly a multiplier of available width should not
// count as having wrapped on the last line
count -= 1
}
}
curr = delim + 1
}
return count
}

@ -0,0 +1,329 @@
package survey
import (
"errors"
"fmt"
"git.yaojiankang.top/hupeh/gcz/survey/core"
"git.yaojiankang.top/hupeh/gcz/survey/terminal"
)
/*
Select is a prompt that presents a list of various options to the user
for them to select using the arrow keys and enter. Response type is a string.
color := ""
prompt := &survey.Select{
Message: "Choose a color:",
Options: []string{"red", "blue", "green"},
}
survey.AskOne(prompt, &color)
*/
type Select struct {
Renderer
Message string
Options []string
Default interface{}
Help string
PageSize int
VimMode bool
FilterMessage string
Filter func(filter string, value string, index int) bool
Description func(value string, index int) string
filter string
selectedIndex int
showingHelp bool
}
// SelectTemplateData is the data available to the templates when processing
type SelectTemplateData struct {
Select
PageEntries []core.OptionAnswer
SelectedIndex int
Answer string
ShowAnswer bool
ShowHelp bool
Description func(value string, index int) string
Config *PromptConfig
// These fields are used when rendering an individual option
CurrentOpt core.OptionAnswer
CurrentIndex int
}
// IterateOption sets CurrentOpt and CurrentIndex appropriately so a select option can be rendered individually
func (s SelectTemplateData) IterateOption(ix int, opt core.OptionAnswer) interface{} {
copy := s
copy.CurrentIndex = ix
copy.CurrentOpt = opt
return copy
}
func (s SelectTemplateData) GetDescription(opt core.OptionAnswer) string {
if s.Description == nil {
return ""
}
return s.Description(opt.Value, opt.Index)
}
var SelectQuestionTemplate = `
{{- define "option"}}
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
{{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{end}}
{{- color "reset"}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
{{- else}}
{{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
{{- "\n"}}
{{- range $ix, $option := .PageEntries}}
{{- template "option" $.IterateOption $ix $option}}
{{- end}}
{{- end}}`
// OnChange is called on every keypress.
func (s *Select) OnChange(key rune, config *PromptConfig) bool {
options := s.filterOptions(config)
oldFilter := s.filter
// if the user pressed the enter key and the index is a valid option
if key == terminal.KeyEnter || key == '\n' {
// if the selected index is a valid option
if len(options) > 0 && s.selectedIndex < len(options) {
// we're done (stop prompting the user)
return true
}
// we're not done (keep prompting)
return false
// if the user pressed the up arrow or 'k' to emulate vim
} else if (key == terminal.KeyArrowUp || (s.VimMode && key == 'k')) && len(options) > 0 {
// if we are at the top of the list
if s.selectedIndex == 0 {
// start from the button
s.selectedIndex = len(options) - 1
} else {
// otherwise we are not at the top of the list so decrement the selected index
s.selectedIndex--
}
// if the user pressed down or 'j' to emulate vim
} else if (key == terminal.KeyTab || key == terminal.KeyArrowDown || (s.VimMode && key == 'j')) && len(options) > 0 {
// if we are at the bottom of the list
if s.selectedIndex == len(options)-1 {
// start from the top
s.selectedIndex = 0
} else {
// increment the selected index
s.selectedIndex++
}
// only show the help message if we have one
} else if string(key) == config.HelpInput && s.Help != "" {
s.showingHelp = true
// if the user wants to toggle vim mode on/off
} else if key == terminal.KeyEscape {
s.VimMode = !s.VimMode
// if the user hits any of the keys that clear the filter
} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
s.filter = ""
// if the user is deleting a character in the filter
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
// if there is content in the filter to delete
if s.filter != "" {
runeFilter := []rune(s.filter)
// subtract a line from the current filter
s.filter = string(runeFilter[0 : len(runeFilter)-1])
// we removed the last value in the filter
}
} else if key >= terminal.KeySpace {
s.filter += string(key)
// make sure vim mode is disabled
s.VimMode = false
}
s.FilterMessage = ""
if s.filter != "" {
s.FilterMessage = " " + s.filter
}
if oldFilter != s.filter {
// filter changed
options = s.filterOptions(config)
if len(options) > 0 && len(options) <= s.selectedIndex {
s.selectedIndex = len(options) - 1
}
}
// figure out the options and index to render
// figure out the page size
pageSize := s.PageSize
// if we dont have a specific one
if pageSize == 0 {
// grab the global value
pageSize = config.PageSize
}
// TODO if we have started filtering and were looking at the end of a list
// and we have modified the filter then we should move the page back!
opts, idx := paginate(pageSize, options, s.selectedIndex)
tmplData := SelectTemplateData{
Select: *s,
SelectedIndex: idx,
ShowHelp: s.showingHelp,
Description: s.Description,
PageEntries: opts,
Config: config,
}
// render the options
_ = s.RenderWithCursorOffset(SelectQuestionTemplate, tmplData, opts, idx)
// keep prompting
return false
}
func (s *Select) filterOptions(config *PromptConfig) []core.OptionAnswer {
// the filtered list
answers := []core.OptionAnswer{}
// if there is no filter applied
if s.filter == "" {
return core.OptionAnswerList(s.Options)
}
// the filter to apply
filter := s.Filter
if filter == nil {
filter = config.Filter
}
for i, opt := range s.Options {
// i the filter says to include the option
if filter(s.filter, opt, i) {
answers = append(answers, core.OptionAnswer{
Index: i,
Value: opt,
})
}
}
// return the list of answers
return answers
}
func (s *Select) Prompt(config *PromptConfig) (interface{}, error) {
// if there are no options to render
if len(s.Options) == 0 {
// we failed
return "", errors.New("please provide options to select from")
}
s.selectedIndex = 0
if s.Default != nil {
switch defaultValue := s.Default.(type) {
case string:
var found bool
for i, opt := range s.Options {
if opt == defaultValue {
s.selectedIndex = i
found = true
}
}
if !found {
return "", fmt.Errorf("default value %q not found in options", defaultValue)
}
case int:
if defaultValue >= len(s.Options) {
return "", fmt.Errorf("default index %d exceeds the number of options", defaultValue)
}
s.selectedIndex = defaultValue
default:
return "", errors.New("default value of select must be an int or string")
}
}
// figure out the page size
pageSize := s.PageSize
// if we dont have a specific one
if pageSize == 0 {
// grab the global value
pageSize = config.PageSize
}
// figure out the options and index to render
opts, idx := paginate(pageSize, core.OptionAnswerList(s.Options), s.selectedIndex)
cursor := s.NewCursor()
cursor.Save() // for proper cursor placement during selection
cursor.Hide() // hide the cursor
defer cursor.Show() // show the cursor when we're done
defer cursor.Restore() // clear any accessibility offsetting on exit
tmplData := SelectTemplateData{
Select: *s,
SelectedIndex: idx,
Description: s.Description,
ShowHelp: s.showingHelp,
PageEntries: opts,
Config: config,
}
// ask the question
err := s.RenderWithCursorOffset(SelectQuestionTemplate, tmplData, opts, idx)
if err != nil {
return "", err
}
rr := s.NewRuneReader()
_ = rr.SetTermMode()
defer func() {
_ = rr.RestoreTermMode()
}()
// start waiting for input
for {
r, _, err := rr.ReadRune()
if err != nil {
return "", err
}
if r == terminal.KeyInterrupt {
return "", terminal.InterruptErr
}
if r == terminal.KeyEndTransmission {
break
}
if s.OnChange(r, config) {
break
}
}
options := s.filterOptions(config)
s.filter = ""
s.FilterMessage = ""
if s.selectedIndex < len(options) {
return options[s.selectedIndex], err
}
return options[0], err
}
func (s *Select) Cleanup(config *PromptConfig, val interface{}) error {
cursor := s.NewCursor()
cursor.Restore()
return s.Render(
SelectQuestionTemplate,
SelectTemplateData{
Select: *s,
Answer: val.(core.OptionAnswer).Value,
ShowAnswer: true,
Description: s.Description,
Config: config,
},
)
}

@ -0,0 +1,474 @@
package survey
import (
"bytes"
"errors"
"io"
"os"
"strings"
"unicode/utf8"
"git.yaojiankang.top/hupeh/gcz/survey/core"
"git.yaojiankang.top/hupeh/gcz/survey/terminal"
)
// DefaultAskOptions is the default options on ask, using the OS stdio.
func defaultAskOptions() *AskOptions {
return &AskOptions{
Stdio: terminal.Stdio{
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
},
PromptConfig: PromptConfig{
PageSize: 7,
HelpInput: "?",
SuggestInput: "tab",
Icons: IconSet{
Error: Icon{
Text: "X",
Format: "red",
},
Help: Icon{
Text: "?",
Format: "cyan",
},
Question: Icon{
Text: "?",
Format: "green+hb",
},
MarkedOption: Icon{
Text: "[x]",
Format: "green",
},
UnmarkedOption: Icon{
Text: "[ ]",
Format: "default+hb",
},
SelectFocus: Icon{
Text: ">",
Format: "cyan+b",
},
},
Filter: func(filter string, value string, index int) (include bool) {
filter = strings.ToLower(filter)
// include this option if it matches
return strings.Contains(strings.ToLower(value), filter)
},
KeepFilter: false,
ShowCursor: false,
RemoveSelectAll: false,
RemoveSelectNone: false,
HideCharacter: '*',
},
}
}
func defaultPromptConfig() *PromptConfig {
return &defaultAskOptions().PromptConfig
}
func defaultIcons() *IconSet {
return &defaultPromptConfig().Icons
}
// OptionAnswer is an ergonomic alias for core.OptionAnswer
type OptionAnswer = core.OptionAnswer
// Icon holds the text and format to show for a particular icon
type Icon struct {
Text string
Format string
}
// IconSet holds the icons to use for various prompts
type IconSet struct {
HelpInput Icon
Error Icon
Help Icon
Question Icon
MarkedOption Icon
UnmarkedOption Icon
SelectFocus Icon
}
// Validator is a function passed to a Question after a user has provided a response.
// If the function returns an error, then the user will be prompted again for another
// response.
type Validator func(ans interface{}) error
// Transformer is a function passed to a Question after a user has provided a response.
// The function can be used to implement a custom logic that will result to return
// a different representation of the given answer.
//
// Look `TransformString`, `ToLower` `Title` and `ComposeTransformers` for more.
type Transformer func(ans interface{}) (newAns interface{})
// Question is the core data structure for a survey questionnaire.
type Question struct {
Name string
Prompt Prompt
Validate Validator
Transform Transformer
}
// PromptConfig holds the global configuration for a prompt
type PromptConfig struct {
PageSize int
Icons IconSet
HelpInput string
SuggestInput string
Filter func(filter string, option string, index int) bool
KeepFilter bool
ShowCursor bool
RemoveSelectAll bool
RemoveSelectNone bool
HideCharacter rune
}
// Prompt is the primary interface for the objects that can take user input
// and return a response.
type Prompt interface {
Prompt(config *PromptConfig) (interface{}, error)
Cleanup(*PromptConfig, interface{}) error
Error(*PromptConfig, error) error
}
// PromptAgainer Interface for Prompts that support prompting again after invalid input
type PromptAgainer interface {
PromptAgain(config *PromptConfig, invalid interface{}, err error) (interface{}, error)
}
// AskOpt allows setting optional ask options.
type AskOpt func(options *AskOptions) error
// AskOptions provides additional options on ask.
type AskOptions struct {
Stdio terminal.Stdio
Validators []Validator
PromptConfig PromptConfig
}
// WithStdio specifies the standard input, output and error files survey
// interacts with. By default, these are os.Stdin, os.Stdout, and os.Stderr.
func WithStdio(in terminal.FileReader, out terminal.FileWriter, err io.Writer) AskOpt {
return func(options *AskOptions) error {
options.Stdio.In = in
options.Stdio.Out = out
options.Stdio.Err = err
return nil
}
}
// WithFilter specifies the default filter to use when asking questions.
func WithFilter(filter func(filter string, value string, index int) (include bool)) AskOpt {
return func(options *AskOptions) error {
// save the filter internally
options.PromptConfig.Filter = filter
return nil
}
}
// WithKeepFilter sets the if the filter is kept after selections
func WithKeepFilter(KeepFilter bool) AskOpt {
return func(options *AskOptions) error {
// set the page size
options.PromptConfig.KeepFilter = KeepFilter
// nothing went wrong
return nil
}
}
// WithRemoveSelectAll remove the select all option in Multiselect
func WithRemoveSelectAll() AskOpt {
return func(options *AskOptions) error {
options.PromptConfig.RemoveSelectAll = true
return nil
}
}
// WithRemoveSelectNone remove the select none/unselect all in Multiselect
func WithRemoveSelectNone() AskOpt {
return func(options *AskOptions) error {
options.PromptConfig.RemoveSelectNone = true
return nil
}
}
// WithValidator specifies a validator to use while prompting the user
func WithValidator(v Validator) AskOpt {
return func(options *AskOptions) error {
// add the provided validator to the list
options.Validators = append(options.Validators, v)
// nothing went wrong
return nil
}
}
type wantsStdio interface {
WithStdio(terminal.Stdio)
}
// WithPageSize sets the default page size used by prompts
func WithPageSize(pageSize int) AskOpt {
return func(options *AskOptions) error {
// set the page size
options.PromptConfig.PageSize = pageSize
// nothing went wrong
return nil
}
}
// WithHelpInput changes the character that prompts look for to give the user helpful information.
func WithHelpInput(r rune) AskOpt {
return func(options *AskOptions) error {
// set the input character
options.PromptConfig.HelpInput = string(r)
// nothing went wrong
return nil
}
}
// WithIcons sets the icons that will be used when prompting the user
func WithIcons(setIcons func(*IconSet)) AskOpt {
return func(options *AskOptions) error {
// update the default icons with whatever the user says
setIcons(&options.PromptConfig.Icons)
// nothing went wrong
return nil
}
}
// WithShowCursor sets the show cursor behavior when prompting the user
func WithShowCursor(ShowCursor bool) AskOpt {
return func(options *AskOptions) error {
// set the page size
options.PromptConfig.ShowCursor = ShowCursor
// nothing went wrong
return nil
}
}
// WithHideCharacter sets the default character shown instead of the password for password inputs
func WithHideCharacter(char rune) AskOpt {
return func(options *AskOptions) error {
// set the hide character
options.PromptConfig.HideCharacter = char
// nothing went wrong
return nil
}
}
/*
AskOne performs the prompt for a single prompt and asks for validation if required.
Response types should be something that can be casted from the response type designated
in the documentation. For example:
name := ""
prompt := &survey.Input{
Message: "name",
}
survey.AskOne(prompt, &name)
*/
func AskOne(p Prompt, response interface{}, opts ...AskOpt) error {
err := Ask([]*Question{{Prompt: p}}, response, opts...)
if err != nil {
return err
}
return nil
}
/*
Ask performs the prompt loop, asking for validation when appropriate. The response
type can be one of two options. If a struct is passed, the answer will be written to
the field whose name matches the Name field on the corresponding question. Field types
should be something that can be casted from the response type designated in the
documentation. Note, a survey tag can also be used to identify a Otherwise, a
map[string]interface{} can be passed, responses will be written to the key with the
matching name. For example:
qs := []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{Message: "What is your name?"},
Validate: survey.Required,
Transform: survey.Title,
},
}
answers := struct{ Name string }{}
err := survey.Ask(qs, &answers)
*/
func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
// build up the configuration options
options := defaultAskOptions()
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(options); err != nil {
return err
}
}
// if we weren't passed a place to record the answers
if response == nil {
// we can't go any further
return errors.New("cannot call Ask() with a nil reference to record the answers")
}
validate := func(q *Question, val interface{}) error {
if q.Validate != nil {
if err := q.Validate(val); err != nil {
return err
}
}
for _, v := range options.Validators {
if err := v(val); err != nil {
return err
}
}
return nil
}
// go over every question
for _, q := range qs {
// If Prompt implements controllable stdio, pass in specified stdio.
if p, ok := q.Prompt.(wantsStdio); ok {
p.WithStdio(options.Stdio)
}
var ans interface{}
var validationErr error
// prompt and validation loop
for {
if validationErr != nil {
if err := q.Prompt.Error(&options.PromptConfig, validationErr); err != nil {
return err
}
}
var err error
if promptAgainer, ok := q.Prompt.(PromptAgainer); ok && validationErr != nil {
ans, err = promptAgainer.PromptAgain(&options.PromptConfig, ans, validationErr)
} else {
ans, err = q.Prompt.Prompt(&options.PromptConfig)
}
if err != nil {
return err
}
validationErr = validate(q, ans)
if validationErr == nil {
break
}
}
if q.Transform != nil {
// check if we have a transformer available, if so
// then try to acquire the new representation of the
// answer, if the resulting answer is not nil.
if newAns := q.Transform(ans); newAns != nil {
ans = newAns
}
}
// tell the prompt to cleanup with the validated value
if err := q.Prompt.Cleanup(&options.PromptConfig, ans); err != nil {
return err
}
// add it to the map
if err := core.WriteAnswer(response, q.Name, ans); err != nil {
return err
}
}
// return the response
return nil
}
// paginate returns a single page of choices given the page size, the total list of
// possible choices, and the current selected index in the total list.
func paginate(pageSize int, choices []core.OptionAnswer, sel int) ([]core.OptionAnswer, int) {
var start, end, cursor int
if len(choices) < pageSize {
// if we dont have enough options to fill a page
start = 0
end = len(choices)
cursor = sel
} else if sel < pageSize/2 {
// if we are in the first half page
start = 0
end = pageSize
cursor = sel
} else if len(choices)-sel-1 < pageSize/2 {
// if we are in the last half page
start = len(choices) - pageSize
end = len(choices)
cursor = sel - start
} else {
// somewhere in the middle
above := pageSize / 2
below := pageSize - above
cursor = pageSize / 2
start = sel - above
end = sel + below
}
// return the subset we care about and the index
return choices[start:end], cursor
}
type IterableOpts interface {
IterateOption(int, core.OptionAnswer) interface{}
}
func computeCursorOffset(tmpl string, data IterableOpts, opts []core.OptionAnswer, idx, tWidth int) int {
tmpls, err := core.GetTemplatePair(tmpl)
if err != nil {
return 0
}
t := tmpls[0]
renderOpt := func(ix int, opt core.OptionAnswer) string {
var buf bytes.Buffer
_ = t.ExecuteTemplate(&buf, "option", data.IterateOption(ix, opt))
return buf.String()
}
offset := len(opts) - idx
for i, o := range opts {
if i < idx {
continue
}
renderedOpt := renderOpt(i, o)
valWidth := utf8.RuneCount([]byte(renderedOpt))
if valWidth > tWidth {
splitCount := valWidth / tWidth
if valWidth%tWidth == 0 {
splitCount -= 1
}
offset += splitCount
}
}
return offset
}

@ -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:])
}

@ -0,0 +1,209 @@
//go:build !windows
// +build !windows
package terminal
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strconv"
)
var COORDINATE_SYSTEM_BEGIN Short = 1
var dsrPattern = regexp.MustCompile(`\x1b\[(\d+);(\d+)R$`)
type Cursor struct {
In FileReader
Out FileWriter
}
// Up moves the cursor n cells to up.
func (c *Cursor) Up(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dA", n)
return err
}
// Down moves the cursor n cells to down.
func (c *Cursor) Down(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dB", n)
return err
}
// Forward moves the cursor n cells to right.
func (c *Cursor) Forward(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dC", n)
return err
}
// Back moves the cursor n cells to left.
func (c *Cursor) Back(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dD", n)
return err
}
// NextLine moves cursor to beginning of the line n lines down.
func (c *Cursor) NextLine(n int) error {
if err := c.Down(1); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// PreviousLine moves cursor to beginning of the line n lines up.
func (c *Cursor) PreviousLine(n int) error {
if err := c.Up(1); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// HorizontalAbsolute moves cursor horizontally to x.
func (c *Cursor) HorizontalAbsolute(x int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dG", x)
return err
}
// Show shows the cursor.
func (c *Cursor) Show() error {
_, err := fmt.Fprint(c.Out, "\x1b[?25h")
return err
}
// Hide hide the cursor.
func (c *Cursor) Hide() error {
_, err := fmt.Fprint(c.Out, "\x1b[?25l")
return err
}
// move moves the cursor to a specific x,y location.
func (c *Cursor) move(x int, y int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
return err
}
// Save saves the current position
func (c *Cursor) Save() error {
_, err := fmt.Fprint(c.Out, "\x1b7")
return err
}
// Restore restores the saved position of the cursor
func (c *Cursor) Restore() error {
_, err := fmt.Fprint(c.Out, "\x1b8")
return err
}
// for comparability purposes between windows
// in unix we need to print out a new line on some terminals
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
if cur.Y == terminalSize.Y {
if _, err := fmt.Fprintln(c.Out); err != nil {
return err
}
}
return c.NextLine(1)
}
// Location returns the current location of the cursor in the terminal
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
// ANSI escape sequence for DSR - Device Status Report
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
if _, err := fmt.Fprint(c.Out, "\x1b[6n"); err != nil {
return nil, err
}
// There may be input in Stdin prior to CursorLocation so make sure we don't
// drop those bytes.
var loc []int
var match string
for loc == nil {
// Reports the cursor position (CPR) to the application as (as though typed at
// the keyboard) ESC[n;mR, where n is the row and m is the column.
reader := bufio.NewReader(c.In)
text, err := reader.ReadSlice(byte('R'))
if err != nil {
return nil, err
}
loc = dsrPattern.FindStringIndex(string(text))
if loc == nil {
// After reading slice to byte 'R', the bufio Reader may have read more
// bytes into its internal buffer which will be discarded on next ReadSlice.
// We create a temporary buffer to read the remaining buffered slice and
// write them to output buffer.
buffered := make([]byte, reader.Buffered())
_, err = io.ReadFull(reader, buffered)
if err != nil {
return nil, err
}
// Stdin contains R that doesn't match DSR, so pass the bytes along to
// output buffer.
buf.Write(text)
buf.Write(buffered)
} else {
// Write the non-matching leading bytes to output buffer.
buf.Write(text[:loc[0]])
// Save the matching bytes to extract the row and column of the cursor.
match = string(text[loc[0]:loc[1]])
}
}
matches := dsrPattern.FindStringSubmatch(string(match))
if len(matches) != 3 {
return nil, fmt.Errorf("incorrect number of matches: %d", len(matches))
}
col, err := strconv.Atoi(matches[2])
if err != nil {
return nil, err
}
row, err := strconv.Atoi(matches[1])
if err != nil {
return nil, err
}
return &Coord{Short(col), Short(row)}, nil
}
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
return cur.X == size.X
}
func (cur Coord) CursorIsAtLineBegin() bool {
return cur.X == COORDINATE_SYSTEM_BEGIN
}
// Size returns the height and width of the terminal.
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
// the general approach here is to move the cursor to the very bottom
// of the terminal, ask for the current location and then move the
// cursor back where we started
// hide the cursor (so it doesn't blink when getting the size of the terminal)
c.Hide()
defer c.Show()
// save the current location of the cursor
c.Save()
defer c.Restore()
// move the cursor to the very bottom of the terminal
c.move(999, 999)
// ask for the current location
bottom, err := c.Location(buf)
if err != nil {
return nil, err
}
// since the bottom was calculated in the lower right corner, it
// is the dimensions we are looking for
return bottom, nil
}

@ -0,0 +1,164 @@
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) error {
return c.cursorMove(0, n)
}
func (c *Cursor) Down(n int) error {
return c.cursorMove(0, -1*n)
}
func (c *Cursor) Forward(n int) error {
return c.cursorMove(n, 0)
}
func (c *Cursor) Back(n int) error {
return c.cursorMove(-1*n, 0)
}
// save the cursor location
func (c *Cursor) Save() error {
loc, err := c.Location(nil)
if err != nil {
return err
}
cursorLoc = *loc
return nil
}
func (c *Cursor) Restore() error {
handle := syscall.Handle(c.Out.Fd())
// restore it to the original position
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
return normalizeError(err)
}
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
return cur.X == size.X
}
func (cur Coord) CursorIsAtLineBegin() bool {
return cur.X == 0
}
func (c *Cursor) cursorMove(x int, y int) error {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
var cursor Coord
cursor.X = csbi.cursorPosition.X + Short(x)
cursor.Y = csbi.cursorPosition.Y + Short(y)
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
}
func (c *Cursor) NextLine(n int) error {
if err := c.Up(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
func (c *Cursor) PreviousLine(n int) error {
if err := c.Down(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// for comparability purposes between windows
// in windows we don't have to print out a new line
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
return c.NextLine(1)
}
func (c *Cursor) HorizontalAbsolute(x int) error {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
var cursor Coord
cursor.X = Short(x)
cursor.Y = csbi.cursorPosition.Y
if csbi.size.X < cursor.X {
cursor.X = csbi.size.X
}
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
}
func (c *Cursor) Show() error {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
return err
}
cci.visible = 1
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
return normalizeError(err)
}
func (c *Cursor) Hide() error {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
return err
}
cci.visible = 0
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
return normalizeError(err)
}
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return nil, err
}
return &csbi.cursorPosition, nil
}
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return nil, err
}
// windows' coordinate system begins at (0, 0)
csbi.size.X--
csbi.size.Y--
return &csbi.size, nil
}

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

@ -0,0 +1,13 @@
//go:build !windows
// +build !windows
package terminal
import (
"fmt"
)
func EraseLine(out FileWriter, mode EraseLineMode) error {
_, err := fmt.Fprintf(out, "\x1b[%dK", mode)
return err
}

@ -0,0 +1,31 @@
package terminal
import (
"syscall"
"unsafe"
)
func EraseLine(out FileWriter, mode EraseLineMode) error {
handle := syscall.Handle(out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
var w uint32
var x Short
cursor := csbi.cursorPosition
switch mode {
case ERASE_LINE_END:
x = csbi.size.X
case ERASE_LINE_START:
x = 0
case ERASE_LINE_ALL:
cursor.X = 0
x = csbi.size.X
}
_, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
return normalizeError(err)
}

@ -0,0 +1,10 @@
package terminal
import (
"errors"
)
var (
//lint:ignore ST1012 keeping old name for backwards compatibility
InterruptErr = errors.New("interrupt")
)

@ -0,0 +1,20 @@
//go:build !windows
// +build !windows
package terminal
import (
"io"
)
// NewAnsiStdout returns special stdout, which converts escape sequences to Windows API calls
// on Windows environment.
func NewAnsiStdout(out FileWriter) io.Writer {
return out
}
// NewAnsiStderr returns special stderr, which converts escape sequences to Windows API calls
// on Windows environment.
func NewAnsiStderr(out FileWriter) io.Writer {
return out
}

@ -0,0 +1,253 @@
package terminal
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/mattn/go-isatty"
)
const (
foregroundBlue = 0x1
foregroundGreen = 0x2
foregroundRed = 0x4
foregroundIntensity = 0x8
foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
backgroundBlue = 0x10
backgroundGreen = 0x20
backgroundRed = 0x40
backgroundIntensity = 0x80
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
)
type Writer struct {
out FileWriter
handle syscall.Handle
orgAttr word
}
func NewAnsiStdout(out FileWriter) io.Writer {
var csbi consoleScreenBufferInfo
if !isatty.IsTerminal(out.Fd()) {
return out
}
handle := syscall.Handle(out.Fd())
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
}
func NewAnsiStderr(out FileWriter) io.Writer {
var csbi consoleScreenBufferInfo
if !isatty.IsTerminal(out.Fd()) {
return out
}
handle := syscall.Handle(out.Fd())
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
}
func (w *Writer) Write(data []byte) (n int, err error) {
r := bytes.NewReader(data)
for {
var ch rune
var size int
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
return
}
n += size
switch ch {
case '\x1b':
size, err = w.handleEscape(r)
n += size
if err != nil {
return
}
default:
_, err = fmt.Fprint(w.out, string(ch))
if err != nil {
return
}
}
}
}
func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
buf := make([]byte, 0, 10)
buf = append(buf, "\x1b"...)
var ch rune
var size int
// Check '[' continues after \x1b
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
fmt.Fprint(w.out, string(buf))
return
}
n += size
if ch != '[' {
fmt.Fprint(w.out, string(buf))
return
}
// Parse escape code
var code rune
argBuf := make([]byte, 0, 10)
for {
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
fmt.Fprint(w.out, string(buf))
return
}
n += size
if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') {
code = ch
break
}
argBuf = append(argBuf, string(ch)...)
}
err = w.applyEscapeCode(buf, string(argBuf), code)
return
}
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) error {
c := &Cursor{Out: w.out}
switch arg + string(code) {
case "?25h":
return c.Show()
case "?25l":
return c.Hide()
}
if code >= 'A' && code <= 'G' {
if n, err := strconv.Atoi(arg); err == nil {
switch code {
case 'A':
return c.Up(n)
case 'B':
return c.Down(n)
case 'C':
return c.Forward(n)
case 'D':
return c.Back(n)
case 'E':
return c.NextLine(n)
case 'F':
return c.PreviousLine(n)
case 'G':
return c.HorizontalAbsolute(n)
}
}
}
switch code {
case 'm':
return w.applySelectGraphicRendition(arg)
default:
buf = append(buf, string(code)...)
_, err := fmt.Fprint(w.out, string(buf))
return err
}
}
// Original implementation: https://github.com/mattn/go-colorable
func (w *Writer) applySelectGraphicRendition(arg string) error {
if arg == "" {
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
return normalizeError(err)
}
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
attr := csbi.attributes
for _, param := range strings.Split(arg, ";") {
n, err := strconv.Atoi(param)
if err != nil {
continue
}
switch {
case n == 0 || n == 100:
attr = w.orgAttr
case 1 <= n && n <= 5:
attr |= foregroundIntensity
case 30 <= n && n <= 37:
attr = (attr & backgroundMask)
if (n-30)&1 != 0 {
attr |= foregroundRed
}
if (n-30)&2 != 0 {
attr |= foregroundGreen
}
if (n-30)&4 != 0 {
attr |= foregroundBlue
}
case 40 <= n && n <= 47:
attr = (attr & foregroundMask)
if (n-40)&1 != 0 {
attr |= backgroundRed
}
if (n-40)&2 != 0 {
attr |= backgroundGreen
}
if (n-40)&4 != 0 {
attr |= backgroundBlue
}
case 90 <= n && n <= 97:
attr = (attr & backgroundMask)
attr |= foregroundIntensity
if (n-90)&1 != 0 {
attr |= foregroundRed
}
if (n-90)&2 != 0 {
attr |= foregroundGreen
}
if (n-90)&4 != 0 {
attr |= foregroundBlue
}
case 100 <= n && n <= 107:
attr = (attr & foregroundMask)
attr |= backgroundIntensity
if (n-100)&1 != 0 {
attr |= backgroundRed
}
if (n-100)&2 != 0 {
attr |= backgroundGreen
}
if (n-100)&4 != 0 {
attr |= backgroundBlue
}
}
}
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
return normalizeError(err)
}
func normalizeError(err error) error {
if syserr, ok := err.(syscall.Errno); ok && syserr == 0 {
return nil
}
return err
}

@ -0,0 +1,417 @@
package terminal
import (
"fmt"
"unicode"
"golang.org/x/text/width"
)
type RuneReader struct {
stdio Stdio
state runeReaderState
}
func NewRuneReader(stdio Stdio) *RuneReader {
return &RuneReader{
stdio: stdio,
state: newRuneReaderState(stdio.In),
}
}
func (rr *RuneReader) printChar(char rune, mask rune) error {
// if we don't need to mask the input
if mask == 0 {
// just print the character the user pressed
_, err := fmt.Fprintf(rr.stdio.Out, "%c", char)
return err
}
// otherwise print the mask we were given
_, err := fmt.Fprintf(rr.stdio.Out, "%c", mask)
return err
}
type OnRuneFn func(rune, []rune) ([]rune, bool, error)
func (rr *RuneReader) ReadLine(mask rune, onRunes ...OnRuneFn) ([]rune, error) {
return rr.ReadLineWithDefault(mask, []rune{}, onRunes...)
}
func (rr *RuneReader) ReadLineWithDefault(mask rune, d []rune, onRunes ...OnRuneFn) ([]rune, error) {
line := []rune{}
// we only care about horizontal displacements from the origin so start counting at 0
index := 0
cursor := &Cursor{
In: rr.stdio.In,
Out: rr.stdio.Out,
}
onRune := func(r rune, line []rune) ([]rune, bool, error) {
return line, false, nil
}
// if the user pressed a key the caller was interested in capturing
if len(onRunes) > 0 {
onRune = onRunes[0]
}
// we get the terminal width and height (if resized after this point the property might become invalid)
terminalSize, _ := cursor.Size(rr.Buffer())
// we set the current location of the cursor once
cursorCurrent, _ := cursor.Location(rr.Buffer())
increment := func() {
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
cursorCurrent.Y++
} else {
cursorCurrent.X++
}
}
decrement := func() {
if cursorCurrent.CursorIsAtLineBegin() {
cursorCurrent.X = terminalSize.X
cursorCurrent.Y--
} else {
cursorCurrent.X--
}
}
if len(d) > 0 {
index = len(d)
if _, err := fmt.Fprint(rr.stdio.Out, string(d)); err != nil {
return d, err
}
line = d
for range d {
increment()
}
}
for {
// wait for some input
r, _, err := rr.ReadRune()
if err != nil {
return line, err
}
if l, stop, err := onRune(r, line); stop || err != nil {
return l, err
}
// if the user pressed enter or some other newline/termination like ctrl+d
if r == '\r' || r == '\n' || r == KeyEndTransmission {
// delete what's printed out on the console screen (cleanup)
for index > 0 {
if cursorCurrent.CursorIsAtLineBegin() {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
} else {
cursor.Back(1)
}
decrement()
index--
}
// move the cursor the a new line
cursor.MoveNextLine(cursorCurrent, terminalSize)
// we're done processing the input
return line, nil
}
// if the user interrupts (ie with ctrl+c)
if r == KeyInterrupt {
// go to the beginning of the next line
if _, err := fmt.Fprint(rr.stdio.Out, "\r\n"); err != nil {
return line, err
}
// we're done processing the input, and treat interrupt like an error
return line, InterruptErr
}
// allow for backspace/delete editing of inputs
if r == KeyBackspace || r == KeyDelete {
// and we're not at the beginning of the line
if index > 0 && len(line) > 0 {
// if we are at the end of the word
if index == len(line) {
// just remove the last letter from the internal representation
// also count the number of cells the rune before the cursor occupied
cells := runeWidth(line[len(line)-1])
line = line[:len(line)-1]
// go back one
if cursorCurrent.X == 1 {
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
} else {
cursor.Back(cells)
}
// clear the rest of the line
EraseLine(rr.stdio.Out, ERASE_LINE_END)
} else {
// we need to remove a character from the middle of the word
cells := runeWidth(line[index-1])
// remove the current index from the list
line = append(line[:index-1], line[index:]...)
// save the current position of the cursor, as we have to move the cursor one back to erase the current symbol
// and then move the cursor for each symbol in line[index-1:] to print it out, afterwards we want to restore
// the cursor to its previous location.
cursor.Save()
// clear the rest of the line
cursor.Back(cells)
// print what comes after
for _, char := range line[index-1:] {
//Erase symbols which are left over from older print
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// print characters to the new line appropriately
if err := rr.printChar(char, mask); err != nil {
return line, err
}
}
// erase what's left over from last print
if cursorCurrent.Y < terminalSize.Y {
cursor.NextLine(1)
EraseLine(rr.stdio.Out, ERASE_LINE_END)
}
// restore cursor
cursor.Restore()
if cursorCurrent.CursorIsAtLineBegin() {
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
} else {
cursor.Back(cells)
}
}
// decrement the index
index--
decrement()
} else {
// otherwise the user pressed backspace while at the beginning of the line
_ = soundBell(rr.stdio.Out)
}
// we're done processing this key
continue
}
// if the left arrow is pressed
if r == KeyArrowLeft {
// if we have space to the left
if index > 0 {
//move the cursor to the prev line if necessary
if cursorCurrent.CursorIsAtLineBegin() {
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
} else {
cursor.Back(runeWidth(line[index-1]))
}
//decrement the index
index--
decrement()
} else {
// otherwise we are at the beginning of where we started reading lines
// sound the bell
_ = soundBell(rr.stdio.Out)
}
// we're done processing this key press
continue
}
// if the right arrow is pressed
if r == KeyArrowRight {
// if we have space to the right
if index < len(line) {
// move the cursor to the next line if necessary
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursor.NextLine(1)
} else {
cursor.Forward(runeWidth(line[index]))
}
index++
increment()
} else {
// otherwise we are at the end of the word and can't go past
// sound the bell
_ = soundBell(rr.stdio.Out)
}
// we're done processing this key press
continue
}
// the user pressed one of the special keys
if r == SpecialKeyHome {
for index > 0 {
if cursorCurrent.CursorIsAtLineBegin() {
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
cursorCurrent.Y--
cursorCurrent.X = terminalSize.X
} else {
cursor.Back(runeWidth(line[index-1]))
cursorCurrent.X -= Short(runeWidth(line[index-1]))
}
index--
}
continue
// user pressed end
} else if r == SpecialKeyEnd {
for index != len(line) {
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursor.NextLine(1)
cursorCurrent.Y++
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
} else {
cursor.Forward(runeWidth(line[index]))
cursorCurrent.X += Short(runeWidth(line[index]))
}
index++
}
continue
// user pressed forward delete key
} else if r == SpecialKeyDelete {
// if index at the end of the line nothing to delete
if index != len(line) {
// save the current position of the cursor, as we have to erase the current symbol
// and then move the cursor for each symbol in line[index:] to print it out, afterwards we want to restore
// the cursor to its previous location.
cursor.Save()
// remove the symbol after the cursor
line = append(line[:index], line[index+1:]...)
// print the updated line
for _, char := range line[index:] {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// print out the character
if err := rr.printChar(char, mask); err != nil {
return line, err
}
}
// erase what's left on last line
if cursorCurrent.Y < terminalSize.Y {
cursor.NextLine(1)
EraseLine(rr.stdio.Out, ERASE_LINE_END)
}
// restore cursor
cursor.Restore()
if len(line) == 0 || index == len(line) {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
}
}
continue
}
// if the letter is another escape sequence
if unicode.IsControl(r) || r == IgnoreKey {
// ignore it
continue
}
// the user pressed a regular key
// if we are at the end of the line
if index == len(line) {
// just append the character at the end of the line
line = append(line, r)
// save the location of the cursor
index++
increment()
// print out the character
if err := rr.printChar(r, mask); err != nil {
return line, err
}
} else {
// we are in the middle of the word so we need to insert the character the user pressed
line = append(line[:index], append([]rune{r}, line[index:]...)...)
// save the current position of the cursor, as we have to move the cursor back to erase the current symbol
// and then move for each symbol in line[index:] to print it out, afterwards we want to restore
// cursor's location to its previous one.
cursor.Save()
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// remove the symbol after the cursor
// print the updated line
for _, char := range line[index:] {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// print out the character
if err := rr.printChar(char, mask); err != nil {
return line, err
}
increment()
}
// if we are at the last line, we want to visually insert a new line and append to it.
if cursorCurrent.CursorIsAtLineEnd(terminalSize) && cursorCurrent.Y == terminalSize.Y {
// add a new line to the terminal
if _, err := fmt.Fprintln(rr.stdio.Out); err != nil {
return line, err
}
// restore the position of the cursor horizontally
cursor.Restore()
// restore the position of the cursor vertically
cursor.PreviousLine(1)
} else {
// restore cursor
cursor.Restore()
}
// check if cursor needs to move to next line
cursorCurrent, _ = cursor.Location(rr.Buffer())
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursor.NextLine(1)
} else {
cursor.Forward(runeWidth(r))
}
// increment the index
index++
increment()
}
}
}
// runeWidth returns the number of columns spanned by a rune when printed to the terminal
func runeWidth(r rune) int {
switch width.LookupRune(r).Kind() {
case width.EastAsianWide, width.EastAsianFullwidth:
return 2
}
if !unicode.IsPrint(r) {
return 0
}
return 1
}
// isAnsiMarker returns if a rune denotes the start of an ANSI sequence
func isAnsiMarker(r rune) bool {
return r == '\x1B'
}
// isAnsiTerminator returns if a rune denotes the end of an ANSI sequence
func isAnsiTerminator(r rune) bool {
return (r >= 0x40 && r <= 0x5a) || (r == 0x5e) || (r >= 0x60 && r <= 0x7e)
}
// StringWidth returns the visible width of a string when printed to the terminal
func StringWidth(str string) int {
w := 0
ansi := false
for _, r := range str {
// increase width only when outside of ANSI escape sequences
if ansi || isAnsiMarker(r) {
ansi = !isAnsiTerminator(r)
} else {
w += runeWidth(r)
}
}
return w
}

@ -0,0 +1,14 @@
// copied from: https://github.com/golang/crypto/blob/master/ssh/terminal/util_bsd.go
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
// +build darwin dragonfly freebsd netbsd openbsd
package terminal
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
const ioctlWriteTermios = syscall.TIOCSETA

@ -0,0 +1,14 @@
// copied from https://github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && !ppc64le
// +build linux,!ppc64le
package terminal
// These constants are declared here, rather than importing
// them from the syscall package as some syscall packages, even
// on linux, for example gccgo, do not declare them.
const ioctlReadTermios = 0x5401 // syscall.TCGETS
const ioctlWriteTermios = 0x5402 // syscall.TCSETS

@ -0,0 +1,132 @@
//go:build !windows
// +build !windows
// The terminal mode manipulation code is derived heavily from:
// https://github.com/golang/crypto/blob/master/ssh/terminal/util.go:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package terminal
import (
"bufio"
"bytes"
"fmt"
"syscall"
"unsafe"
)
const (
normalKeypad = '['
applicationKeypad = 'O'
)
type runeReaderState struct {
term syscall.Termios
reader *bufio.Reader
buf *bytes.Buffer
}
func newRuneReaderState(input FileReader) runeReaderState {
buf := new(bytes.Buffer)
return runeReaderState{
reader: bufio.NewReader(&BufferedReader{
In: input,
Buffer: buf,
}),
buf: buf,
}
}
func (rr *RuneReader) Buffer() *bytes.Buffer {
return rr.state.buf
}
// For reading runes we just want to disable echo.
func (rr *RuneReader) SetTermMode() error {
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 {
return err
}
newState := rr.state.term
newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG
// Because we are clearing canonical mode, we need to ensure VMIN & VTIME are
// set to the values we expect. This combination puts things in standard
// "blocking read" mode (see termios(3)).
newState.Cc[syscall.VMIN] = 1
newState.Cc[syscall.VTIME] = 0
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 {
return err
}
return nil
}
func (rr *RuneReader) RestoreTermMode() error {
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 {
return err
}
return nil
}
// ReadRune Parse escape sequences such as ESC [ A for arrow keys.
// See https://vt100.net/docs/vt102-ug/appendixc.html
func (rr *RuneReader) ReadRune() (rune, int, error) {
r, size, err := rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
if r != KeyEscape {
return r, size, err
}
if rr.state.reader.Buffered() == 0 {
// no more characters so must be `Esc` key
return KeyEscape, 1, nil
}
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
// ESC O ... or ESC [ ...?
if r != normalKeypad && r != applicationKeypad {
return r, size, fmt.Errorf("unexpected escape sequence from terminal: %q", []rune{KeyEscape, r})
}
keypad := r
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
switch r {
case 'A': // ESC [ A or ESC O A
return KeyArrowUp, 1, nil
case 'B': // ESC [ B or ESC O B
return KeyArrowDown, 1, nil
case 'C': // ESC [ C or ESC O C
return KeyArrowRight, 1, nil
case 'D': // ESC [ D or ESC O D
return KeyArrowLeft, 1, nil
case 'F': // ESC [ F or ESC O F
return SpecialKeyEnd, 1, nil
case 'H': // ESC [ H or ESC O H
return SpecialKeyHome, 1, nil
case '3': // ESC [ 3
if keypad == normalKeypad {
// discard the following '~' key from buffer
_, _ = rr.state.reader.Discard(1)
return SpecialKeyDelete, 1, nil
}
}
// discard the following '~' key from buffer
_, _ = rr.state.reader.Discard(1)
return IgnoreKey, 1, nil
}

@ -0,0 +1,8 @@
//go:build ppc64le && linux
// +build ppc64le,linux
package terminal
// Used syscall numbers from https://github.com/golang/go/blob/master/src/syscall/ztypes_linux_ppc64le.go
const ioctlReadTermios = 0x402c7413 // syscall.TCGETS
const ioctlWriteTermios = 0x802c7414 // syscall.TCSETS

@ -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
}
}

@ -0,0 +1,32 @@
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'
KeyTab = '\t'
)
func soundBell(out io.Writer) error {
_, err := fmt.Fprint(out, "\a")
return err
}

@ -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
}

@ -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
}

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

@ -0,0 +1,82 @@
package survey
import (
"reflect"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// TransformString returns a `Transformer` based on the "f"
// function which accepts a string representation of the answer
// and returns a new one, transformed, answer.
// Take for example the functions inside the std `strings` package,
// they can be converted to a compatible `Transformer` by using this function,
// i.e: `TransformString(strings.Title)`, `TransformString(strings.ToUpper)`.
//
// Note that `TransformString` is just a helper, `Transformer` can be used
// to transform any type of answer.
func TransformString(f func(s string) string) Transformer {
return func(ans interface{}) interface{} {
// if the answer value passed in is the zero value of the appropriate type
if isZero(reflect.ValueOf(ans)) {
// skip this `Transformer` by returning a zero value of string.
// The original answer will be not affected,
// see survey.go#L125.
// A zero value of string should be returned to be handled by
// next Transformer in a composed Tranformer,
// see tranform.go#L75
return ""
}
// "ans" is never nil here, so we don't have to check that
// see survey.go#L338 for more.
// Make sure that the the answer's value was a typeof string.
s, ok := ans.(string)
if !ok {
return ""
}
return f(s)
}
}
// ToLower is a `Transformer`.
// It receives an answer value
// and returns a copy of the "ans"
// with all Unicode letters mapped to their lower case.
//
// Note that if "ans" is not a string then it will
// return a nil value, meaning that the above answer
// will not be affected by this call at all.
func ToLower(ans interface{}) interface{} {
transformer := TransformString(strings.ToLower)
return transformer(ans)
}
// Title is a `Transformer`.
// It receives an answer value
// and returns a copy of the "ans"
// with all Unicode letters that begin words
// mapped to their title case.
//
// Note that if "ans" is not a string then it will
// return a nil value, meaning that the above answer
// will not be affected by this call at all.
func Title(ans interface{}) interface{} {
transformer := TransformString(cases.Title(language.English).String)
return transformer(ans)
}
// ComposeTransformers is a variadic function used to create one transformer from many.
func ComposeTransformers(transformers ...Transformer) Transformer {
// return a transformer that calls each one sequentially
return func(ans interface{}) interface{} {
// execute each transformer
for _, t := range transformers {
ans = t(ans)
}
return ans
}
}

@ -0,0 +1,128 @@
package survey
import (
"errors"
"fmt"
"reflect"
"git.yaojiankang.top/hupeh/gcz/survey/core"
)
// Required does not allow an empty value
func Required(val interface{}) error {
// the reflect value of the result
value := reflect.ValueOf(val)
// if the value passed in is the zero value of the appropriate type
if isZero(value) && value.Kind() != reflect.Bool {
//lint:ignore ST1005 this error message should render as capitalized
return errors.New("Value is required")
}
return nil
}
// MaxLength requires that the string is no longer than the specified value
func MaxLength(length int) Validator {
// return a validator that checks the length of the string
return func(val interface{}) error {
if str, ok := val.(string); ok {
// if the string is longer than the given value
if len([]rune(str)) > length {
// yell loudly
return fmt.Errorf("value is too long. Max length is %v", length)
}
} else {
// otherwise we cannot convert the value into a string and cannot enforce length
return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name())
}
// the input is fine
return nil
}
}
// MinLength requires that the string is longer or equal in length to the specified value
func MinLength(length int) Validator {
// return a validator that checks the length of the string
return func(val interface{}) error {
if str, ok := val.(string); ok {
// if the string is shorter than the given value
if len([]rune(str)) < length {
// yell loudly
return fmt.Errorf("value is too short. Min length is %v", length)
}
} else {
// otherwise we cannot convert the value into a string and cannot enforce length
return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name())
}
// the input is fine
return nil
}
}
// MaxItems requires that the list is no longer than the specified value
func MaxItems(numberItems int) Validator {
// return a validator that checks the length of the list
return func(val interface{}) error {
if list, ok := val.([]core.OptionAnswer); ok {
// if the list is longer than the given value
if len(list) > numberItems {
// yell loudly
return fmt.Errorf("value is too long. Max items is %v", numberItems)
}
} else {
// otherwise we cannot convert the value into a list of answer and cannot enforce length
return fmt.Errorf("cannot impose the length on something other than a list of answers")
}
// the input is fine
return nil
}
}
// MinItems requires that the list is longer or equal in length to the specified value
func MinItems(numberItems int) Validator {
// return a validator that checks the length of the list
return func(val interface{}) error {
if list, ok := val.([]core.OptionAnswer); ok {
// if the list is shorter than the given value
if len(list) < numberItems {
// yell loudly
return fmt.Errorf("value is too short. Min items is %v", numberItems)
}
} else {
// otherwise we cannot convert the value into a list of answer and cannot enforce length
return fmt.Errorf("cannot impose the length on something other than a list of answers")
}
// the input is fine
return nil
}
}
// ComposeValidators is a variadic function used to create one validator from many.
func ComposeValidators(validators ...Validator) Validator {
// return a validator that calls each one sequentially
return func(val interface{}) error {
// execute each validator
for _, validator := range validators {
// if the answer's value is not valid
if err := validator(val); err != nil {
// return the error
return err
}
}
// we passed all validators, the answer is valid
return nil
}
}
// isZero returns true if the passed value is the zero object
func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Slice, reflect.Map:
return v.Len() == 0
}
// compare the types directly with more general coverage
return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
}
Loading…
Cancel
Save