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