mirror of
https://github.com/rclone/rclone.git
synced 2025-12-23 11:43:50 +00:00
368 lines
8.5 KiB
Go
368 lines
8.5 KiB
Go
package promptui
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/chzyer/readline"
|
|
"github.com/manifoldco/promptui/screenbuf"
|
|
)
|
|
|
|
const cursor = "\u2588"
|
|
|
|
// Prompt represents a single line text field input with options for validation and input masks.
|
|
type Prompt struct {
|
|
// Label is the value displayed on the command line prompt.
|
|
//
|
|
// The value for Label can be a simple string or a struct that will need to be accessed by dot notation
|
|
// inside the templates. For example, `{{ .Name }}` will display the name property of a struct.
|
|
Label interface{}
|
|
|
|
// Default is the initial value for the prompt. This value will be displayed next to the prompt's label
|
|
// and the user will be able to view or change it depending on the options.
|
|
Default string
|
|
|
|
// AllowEdit lets the user edit the default value. If false, any key press
|
|
// other than <Enter> automatically clears the default value.
|
|
AllowEdit bool
|
|
|
|
// Validate is an optional function that fill be used against the entered value in the prompt to validate it.
|
|
Validate ValidateFunc
|
|
|
|
// Mask is an optional rune that sets which character to display instead of the entered characters. This
|
|
// allows hiding private information like passwords.
|
|
Mask rune
|
|
|
|
// Templates can be used to customize the prompt output. If nil is passed, the
|
|
// default templates are used. See the PromptTemplates docs for more info.
|
|
Templates *PromptTemplates
|
|
|
|
// IsConfirm makes the prompt ask for a yes or no ([Y/N]) question rather than request an input. When set,
|
|
// most properties related to input will be ignored.
|
|
IsConfirm bool
|
|
|
|
// IsVimMode enables vi-like movements (hjkl) and editing.
|
|
IsVimMode bool
|
|
|
|
stdin io.ReadCloser
|
|
stdout io.WriteCloser
|
|
}
|
|
|
|
// PromptTemplates allow a prompt to be customized following stdlib
|
|
// text/template syntax. Custom state, colors and background color are available for use inside
|
|
// the templates and are documented inside the Variable section of the docs.
|
|
//
|
|
// Examples
|
|
//
|
|
// text/templates use a special notation to display programmable content. Using the double bracket notation,
|
|
// the value can be printed with specific helper functions. For example
|
|
//
|
|
// This displays the value given to the template as pure, unstylized text.
|
|
// '{{ . }}'
|
|
//
|
|
// This displays the value colored in cyan
|
|
// '{{ . | cyan }}'
|
|
//
|
|
// This displays the value colored in red with a cyan background-color
|
|
// '{{ . | red | cyan }}'
|
|
//
|
|
// See the doc of text/template for more info: https://golang.org/pkg/text/template/
|
|
type PromptTemplates struct {
|
|
// Prompt is a text/template for the prompt label displayed on the left side of the prompt.
|
|
Prompt string
|
|
|
|
// Prompt is a text/template for the prompt label when IsConfirm is set as true.
|
|
Confirm string
|
|
|
|
// Valid is a text/template for the prompt label when the value entered is valid.
|
|
Valid string
|
|
|
|
// Invalid is a text/template for the prompt label when the value entered is invalid.
|
|
Invalid string
|
|
|
|
// Success is a text/template for the prompt label when the user has pressed entered and the value has been
|
|
// deemed valid by the validation function. The label will keep using this template even when the prompt ends
|
|
// inside the console.
|
|
Success string
|
|
|
|
// Prompt is a text/template for the prompt label when the value is invalid dur to an error triggered from
|
|
// the prompt's validation function.
|
|
ValidationError string
|
|
|
|
// FuncMap is a map of helper functions that can be used inside of templates according to the text/template
|
|
// documentation.
|
|
//
|
|
// By default, FuncMap contains the color functions used to color the text in templates. If FuncMap
|
|
// is overridden, the colors functions must be added in the override from promptui.FuncMap to work.
|
|
FuncMap template.FuncMap
|
|
|
|
prompt *template.Template
|
|
valid *template.Template
|
|
invalid *template.Template
|
|
validation *template.Template
|
|
success *template.Template
|
|
}
|
|
|
|
// Run executes the prompt. Its displays the label and default value if any, asking the user to enter a value.
|
|
// Run will keep the prompt alive until it has been canceled from the command prompt or it has received a valid
|
|
// value. It will return the value and an error if any occurred during the prompt's execution.
|
|
func (p *Prompt) Run() (string, error) {
|
|
c := &readline.Config{}
|
|
err := c.Init()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
err = p.prepareTemplates()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if p.stdin != nil {
|
|
c.Stdin = p.stdin
|
|
}
|
|
|
|
if p.stdout != nil {
|
|
c.Stdout = p.stdout
|
|
}
|
|
|
|
if p.Mask != 0 {
|
|
c.EnableMask = true
|
|
c.MaskRune = p.Mask
|
|
}
|
|
|
|
if p.IsVimMode {
|
|
c.VimMode = true
|
|
}
|
|
|
|
c.HistoryLimit = -1
|
|
c.UniqueEditLine = true
|
|
|
|
rl, err := readline.NewEx(c)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
rl.Write([]byte(hideCursor))
|
|
sb := screenbuf.New(rl)
|
|
|
|
validFn := func(x string) error {
|
|
return nil
|
|
}
|
|
|
|
if p.Validate != nil {
|
|
validFn = p.Validate
|
|
}
|
|
|
|
var inputErr error
|
|
input := p.Default
|
|
if p.IsConfirm {
|
|
input = ""
|
|
}
|
|
eraseDefault := input != "" && !p.AllowEdit
|
|
|
|
c.SetListener(func(line []rune, pos int, key rune) ([]rune, int, bool) {
|
|
if line != nil {
|
|
input += string(line)
|
|
}
|
|
|
|
switch key {
|
|
case 0: // empty
|
|
case KeyEnter:
|
|
return nil, 0, false
|
|
case KeyBackspace:
|
|
if eraseDefault {
|
|
eraseDefault = false
|
|
input = ""
|
|
}
|
|
if len(input) > 0 {
|
|
r := []rune(input)
|
|
input = string(r[:len(r)-1])
|
|
}
|
|
default:
|
|
if eraseDefault {
|
|
eraseDefault = false
|
|
input = string(line)
|
|
}
|
|
}
|
|
|
|
err := validFn(input)
|
|
var prompt []byte
|
|
|
|
if err != nil {
|
|
prompt = render(p.Templates.invalid, p.Label)
|
|
} else {
|
|
prompt = render(p.Templates.valid, p.Label)
|
|
if p.IsConfirm {
|
|
prompt = render(p.Templates.prompt, p.Label)
|
|
}
|
|
}
|
|
|
|
echo := input
|
|
if p.Mask != 0 {
|
|
echo = strings.Repeat(string(p.Mask), len(echo))
|
|
}
|
|
|
|
prompt = append(prompt, []byte(echo+cursor)...)
|
|
|
|
sb.Reset()
|
|
sb.Write(prompt)
|
|
|
|
if inputErr != nil {
|
|
validation := render(p.Templates.validation, inputErr)
|
|
sb.Write(validation)
|
|
inputErr = nil
|
|
}
|
|
|
|
sb.Flush()
|
|
|
|
return nil, 0, true
|
|
})
|
|
|
|
for {
|
|
_, err = rl.Readline()
|
|
|
|
inputErr = validFn(input)
|
|
if inputErr == nil {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
switch err {
|
|
case readline.ErrInterrupt:
|
|
err = ErrInterrupt
|
|
case io.EOF:
|
|
err = ErrEOF
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
if err.Error() == "Interrupt" {
|
|
err = ErrInterrupt
|
|
}
|
|
sb.Reset()
|
|
sb.WriteString("")
|
|
sb.Flush()
|
|
rl.Write([]byte(showCursor))
|
|
rl.Close()
|
|
return "", err
|
|
}
|
|
|
|
echo := input
|
|
if p.Mask != 0 {
|
|
echo = strings.Repeat(string(p.Mask), len(echo))
|
|
}
|
|
|
|
prompt := render(p.Templates.success, p.Label)
|
|
prompt = append(prompt, []byte(echo)...)
|
|
|
|
if p.IsConfirm {
|
|
lowerDefault := strings.ToLower(p.Default)
|
|
if strings.ToLower(echo) != "y" && (lowerDefault != "y" || (lowerDefault == "y" && echo != "")) {
|
|
prompt = render(p.Templates.invalid, p.Label)
|
|
err = ErrAbort
|
|
}
|
|
}
|
|
|
|
sb.Reset()
|
|
sb.Write(prompt)
|
|
sb.Flush()
|
|
rl.Write([]byte(showCursor))
|
|
rl.Close()
|
|
|
|
return input, err
|
|
}
|
|
|
|
func (p *Prompt) prepareTemplates() error {
|
|
tpls := p.Templates
|
|
if tpls == nil {
|
|
tpls = &PromptTemplates{}
|
|
}
|
|
|
|
if tpls.FuncMap == nil {
|
|
tpls.FuncMap = FuncMap
|
|
}
|
|
|
|
bold := Styler(FGBold)
|
|
|
|
if p.IsConfirm {
|
|
if tpls.Confirm == "" {
|
|
confirm := "y/N"
|
|
if strings.ToLower(p.Default) == "y" {
|
|
confirm = "Y/n"
|
|
}
|
|
tpls.Confirm = fmt.Sprintf(`{{ "%s" | bold }} {{ . | bold }}? {{ "[%s]" | faint }} `, IconInitial, confirm)
|
|
}
|
|
|
|
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Confirm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tpls.prompt = tpl
|
|
} else {
|
|
if tpls.Prompt == "" {
|
|
tpls.Prompt = fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconInitial), bold(":"))
|
|
}
|
|
|
|
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Prompt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tpls.prompt = tpl
|
|
}
|
|
|
|
if tpls.Valid == "" {
|
|
tpls.Valid = fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconGood), bold(":"))
|
|
}
|
|
|
|
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Valid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tpls.valid = tpl
|
|
|
|
if tpls.Invalid == "" {
|
|
tpls.Invalid = fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconBad), bold(":"))
|
|
}
|
|
|
|
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Invalid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tpls.invalid = tpl
|
|
|
|
if tpls.ValidationError == "" {
|
|
tpls.ValidationError = `{{ ">>" | red }} {{ . | red }}`
|
|
}
|
|
|
|
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.ValidationError)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tpls.validation = tpl
|
|
|
|
if tpls.Success == "" {
|
|
tpls.Success = `{{ . | faint }}`
|
|
}
|
|
|
|
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Success)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tpls.success = tpl
|
|
|
|
p.Templates = tpls
|
|
|
|
return nil
|
|
}
|