parent
ad146bfd79
commit
c3e0106b9b
@ -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…
Reference in new issue