Copy promptui from manifoldco/torus-cli

This commit is contained in:
Luiz Branco 2017-10-16 13:48:23 -04:00
commit 7ff5ef96cc
8 changed files with 660 additions and 0 deletions

73
codes.go Normal file
View file

@ -0,0 +1,73 @@
package promptui
import (
"fmt"
"strconv"
"strings"
)
const esc = "\033["
type attribute int
// Forground weight/decoration attributes.
const (
reset attribute = iota
FGBold
FGFaint
FGItalic
FGUnderline
)
// Forground color attributes
const (
FGBlack attribute = iota + 30
FGRed
FGGreen
FGYellow
FGBlue
FGMagenta
FGCyan
FGWhite
)
// ResetCode is the character code used to reset the terminal formatting
var ResetCode = fmt.Sprintf("%s%dm", esc, reset)
var (
hideCursor = esc + "?25l"
showCursor = esc + "?25h"
clearLine = esc + "2K"
)
func upLine(n uint) string {
return movementCode(n, 'A')
}
func downLine(n uint) string {
return movementCode(n, 'B')
}
func movementCode(n uint, code rune) string {
return esc + strconv.FormatUint(uint64(n), 10) + string(code)
}
// Styler returns a func that applies the attributes given in the Styler call
// to the provided string.
func Styler(attrs ...attribute) func(string) string {
attrstrs := make([]string, len(attrs))
for i, v := range attrs {
attrstrs[i] = strconv.Itoa(int(v))
}
seq := strings.Join(attrstrs, ";")
return func(s string) string {
end := ""
if !strings.HasSuffix(s, ResetCode) {
end = ResetCode
}
return fmt.Sprintf("%s%sm%s%s", esc, seq, s, end)
}
}

32
codes_test.go Normal file
View file

@ -0,0 +1,32 @@
package promptui
import "testing"
func TestStyler(t *testing.T) {
t.Run("renders a single code", func(t *testing.T) {
red := Styler(FGRed)("hi")
expected := "\033[31mhi\033[0m"
if red != expected {
t.Errorf("style did not match: %s != %s", red, expected)
}
})
t.Run("combines multiple codes", func(t *testing.T) {
boldRed := Styler(FGRed, FGBold)("hi")
expected := "\033[31;1mhi\033[0m"
if boldRed != expected {
t.Errorf("style did not match: %s != %s", boldRed, expected)
}
})
t.Run("should not repeat reset codes for nested styles", func(t *testing.T) {
red := Styler(FGRed)("hi")
boldRed := Styler(FGBold)(red)
expected := "\033[1m\033[31mhi\033[0m"
if boldRed != expected {
t.Errorf("style did not match: %s != %s", boldRed, expected)
}
})
}

223
prompt.go Normal file
View file

@ -0,0 +1,223 @@
package promptui
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"github.com/chzyer/readline"
)
// Prompt represents a single line text field input.
type Prompt struct {
Label string // Label is the value displayed on the command line prompt
Default string // Default is the initial value to populate in the prompt
// Validate is optional. If set, this function is used to validate the input
// after each character entry.
Validate ValidateFunc
// If mask is set, this value is displayed instead of the actual input
// characters.
Mask rune
IsConfirm bool
IsVimMode bool
Preamble *string
// Indent will be placed before the prompt's state symbol
Indent string
stdin io.Reader
stdout io.Writer
}
// Run runs the prompt, returning the validated input.
func (p *Prompt) Run() (string, error) {
c := &readline.Config{}
err := c.Init()
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
}
if p.Preamble != nil {
fmt.Println(*p.Preamble)
}
suggestedAnswer := ""
punctuation := ":"
if p.IsConfirm {
punctuation = "?"
answers := "y/N"
if strings.ToLower(p.Default) == "y" {
answers = "Y/n"
}
suggestedAnswer = " " + faint("["+answers+"]")
p.Default = ""
}
state := IconInitial
prompt := p.Label + punctuation + suggestedAnswer + " "
c.Prompt = p.Indent + bold(state) + " " + bold(prompt)
c.HistoryLimit = -1
c.UniqueEditLine = true
firstListen := true
wroteErr := false
caughtup := true
var out string
if p.Default != "" {
caughtup = false
out = p.Default
c.Stdin = io.MultiReader(bytes.NewBuffer([]byte(out)), os.Stdin)
}
rl, err := readline.NewEx(c)
if err != nil {
return "", err
}
validFn := func(x string) error {
return nil
}
if p.Validate != nil {
validFn = p.Validate
}
c.SetListener(func(line []rune, pos int, key rune) ([]rune, int, bool) {
if key == readline.CharEnter {
return nil, 0, false
}
if firstListen {
firstListen = false
return nil, 0, false
}
if !caughtup && out != "" {
if string(line) == out {
caughtup = true
}
if wroteErr {
return nil, 0, false
}
}
err := validFn(string(line))
if err != nil {
if _, ok := err.(*ValidationError); ok {
state = IconBad
} else {
rl.Close()
return nil, 0, false
}
} else {
state = IconGood
if p.IsConfirm {
state = IconInitial
}
}
rl.SetPrompt(p.Indent + bold(state) + " " + bold(prompt))
rl.Refresh()
wroteErr = false
return nil, 0, false
})
for {
out, err = rl.Readline()
var msg string
valid := true
oerr := validFn(out)
if oerr != nil {
if verr, ok := oerr.(*ValidationError); ok {
msg = verr.msg
valid = false
state = IconBad
} else {
return "", oerr
}
}
if valid {
state = IconGood
break
}
if err != nil {
switch err {
case readline.ErrInterrupt:
err = ErrInterrupt
case io.EOF:
err = ErrEOF
}
break
}
caughtup = false
c.Stdin = io.MultiReader(bytes.NewBuffer([]byte(out)), os.Stdin)
rl, _ = readline.NewEx(c)
firstListen = true
wroteErr = true
rl.SetPrompt("\n" + red(">> ") + msg + upLine(1) + "\r" + p.Indent + bold(state) + " " + bold(prompt))
rl.Refresh()
}
if wroteErr {
rl.Write([]byte(downLine(1) + clearLine + upLine(1) + "\r"))
}
if err != nil {
if err.Error() == "Interrupt" {
err = ErrInterrupt
}
rl.Write([]byte("\n"))
return "", err
}
echo := out
if p.Mask != 0 {
echo = strings.Repeat(string(p.Mask), len(echo))
}
if p.IsConfirm {
if strings.ToLower(echo) != "y" {
state = IconBad
err = ErrAbort
} else {
state = IconGood
}
}
rl.Write([]byte(p.Indent + state + " " + prompt + faint(echo) + "\n"))
return out, err
}

43
prompt_test.go Normal file
View file

@ -0,0 +1,43 @@
package promptui
import (
"bytes"
"testing"
)
func outputTest(mask rune, input, displayed, output, def string) func(t *testing.T) {
return func(t *testing.T) {
in := bytes.Buffer{}
out := bytes.Buffer{}
p := Prompt{
Label: "test",
Default: def,
Mask: mask,
stdin: &in,
stdout: &out,
}
in.Write([]byte(input + "\n"))
res, err := p.Run()
if err != nil {
t.Errorf("error during prompt: %s", err)
}
if res != output {
t.Errorf("wrong result: %s != %s", res, output)
}
expected := "\033[32m✔\033[0m test: \033[2m" + displayed + "\033[0m\n"
if !bytes.Equal(out.Bytes(), []byte(expected)) {
t.Errorf("wrong output: %s != %s", out.Bytes(), expected)
}
}
}
func TestPrompt(t *testing.T) {
t.Run("can read input", outputTest(0x0, "hi", "hi", "hi", ""))
t.Run("displays masked values", outputTest('*', "hi", "**", "hi", ""))
t.Run("can use a default", outputTest(0x0, "", "hi", "hi", "hi"))
}

44
promptui.go Normal file
View file

@ -0,0 +1,44 @@
// Package promptui provides ui elements for the command line prompt.
package promptui
import "errors"
// ErrEOF is returned from prompts when EOF is encountered.
var ErrEOF = errors.New("^D")
// ErrInterrupt is returned from prompts when an interrupt (ctrl-c) is
// encountered.
var ErrInterrupt = errors.New("^C")
// ErrAbort is returned when confirm prompts are supplied "n"
var ErrAbort = errors.New("")
// ValidateFunc validates the given input. It should return a ValidationError
// if the input is not valid.
type ValidateFunc func(string) error
// ValidationError is the class of errors resulting from invalid inputs,
// returned from a ValidateFunc.
type ValidationError struct {
msg string
}
// Error implements the error interface for ValidationError.
func (v *ValidationError) Error() string {
return v.msg
}
// NewValidationError creates a new validation error with the given message.
func NewValidationError(msg string) *ValidationError {
return &ValidationError{msg: msg}
}
// SuccessfulValue returns a value as if it were entered via prompt, valid
func SuccessfulValue(label, value string) string {
return IconGood + " " + label + ": " + faint(value)
}
// FailedValue returns a value as if it were entered via prompt, invalid
func FailedValue(label, value string) string {
return IconBad + " " + label + ": " + faint(value)
}

209
select.go Normal file
View file

@ -0,0 +1,209 @@
package promptui
import (
"bytes"
"io"
"os"
"strings"
"github.com/chzyer/readline"
)
// SelectedAdd is returned from SelectWithAdd when add is selected.
const SelectedAdd = -1
// Select represents a list for selecting a single item
type Select struct {
Label string // Label is the value displayed on the command line prompt.
Items []string // Items are the items to use in the list.
IsVimMode bool // Whether readline is using Vim mode.
}
// Run runs the Select list. It returns the index of the selected element,
// and its value.
func (s *Select) Run() (int, string, error) {
return s.innerRun(0, ' ')
}
func (s *Select) innerRun(starting int, top rune) (int, string, error) {
stdin := readline.NewCancelableStdin(os.Stdin)
c := &readline.Config{}
err := c.Init()
if err != nil {
return 0, "", err
}
c.Stdin = stdin
if s.IsVimMode {
c.VimMode = true
}
prompt := s.Label + ": "
c.HistoryLimit = -1
c.UniqueEditLine = true
start := 0
end := 4
if len(s.Items) <= end {
end = len(s.Items) - 1
}
selected := starting
rl, err := readline.NewEx(c)
if err != nil {
return 0, "", err
}
rl.Write([]byte(hideCursor))
rl.Write([]byte(strings.Repeat("\n", end-start+1)))
counter := 0
rl.Operation.ExitVimInsertMode() // Never use insert mode for selects
c.SetListener(func(line []rune, pos int, key rune) ([]rune, int, bool) {
if rl.Operation.IsEnableVimMode() {
rl.Operation.ExitVimInsertMode()
// Remap j and k for down/up selections immediately after an
// `i` press
switch key {
case 'j':
key = readline.CharNext
case 'k':
key = readline.CharPrev
}
}
switch key {
case readline.CharEnter:
return nil, 0, true
case readline.CharNext:
switch selected {
case len(s.Items) - 1:
case end:
start++
end++
fallthrough
default:
selected++
}
case readline.CharPrev:
switch selected {
case 0:
case start:
start--
end--
fallthrough
default:
selected--
}
}
list := make([]string, end-start+1)
for i := start; i <= end; i++ {
page := ' '
selection := " "
item := s.Items[i]
switch i {
case 0:
page = top
case len(s.Items) - 1:
case start:
page = '↑'
case end:
page = '↓'
}
if i == selected {
selection = "▸"
item = underlined(item)
}
list[i-start] = clearLine + "\r" + string(page) + " " + selection + " " + item
}
prefix := ""
prefix += upLine(uint(len(list))) + "\r" + clearLine
p := prefix + bold(IconInitial) + " " + bold(prompt) + downLine(1) + strings.Join(list, downLine(1))
rl.SetPrompt(p)
rl.Refresh()
counter++
return nil, 0, true
})
_, err = rl.Readline()
rl.Close()
if err != nil {
switch {
case err == readline.ErrInterrupt, err.Error() == "Interrupt":
err = ErrInterrupt
case err == io.EOF:
err = ErrEOF
}
rl.Write([]byte("\n"))
rl.Write([]byte(showCursor))
rl.Refresh()
return 0, "", err
}
rl.Write(bytes.Repeat([]byte(clearLine+upLine(1)), end-start+1))
rl.Write([]byte("\r"))
out := s.Items[selected]
rl.Write([]byte(IconGood + " " + prompt + faint(out) + "\n"))
rl.Write([]byte(showCursor))
return selected, out, err
}
// SelectWithAdd represents a list for selecting a single item, or selecting
// a newly created item.
type SelectWithAdd struct {
Label string // Label is the value displayed on the command line prompt.
Items []string // Items are the items to use in the list.
AddLabel string // The label used in the item list for creating a new item.
// Validate is optional. If set, this function is used to validate the input
// after each character entry.
Validate ValidateFunc
IsVimMode bool // Whether readline is using Vim mode.
}
// Run runs the Select list. It returns the index of the selected element,
// and its value. If a new element is created, -1 is returned as the index.
func (sa *SelectWithAdd) Run() (int, string, error) {
if len(sa.Items) > 0 {
newItems := append([]string{sa.AddLabel}, sa.Items...)
s := Select{
Label: sa.Label,
Items: newItems,
IsVimMode: sa.IsVimMode,
}
selected, value, err := s.innerRun(1, '+')
if err != nil || selected != 0 {
return selected - 1, value, err
}
// XXX run through terminal for windows
os.Stdout.Write([]byte(upLine(1) + "\r" + clearLine))
}
p := Prompt{
Label: sa.AddLabel,
Validate: sa.Validate,
IsVimMode: sa.IsVimMode,
}
value, err := p.Run()
return SelectedAdd, value, err
}

19
styles.go Normal file
View file

@ -0,0 +1,19 @@
// +build !windows
package promptui
var (
bold = Styler(FGBold)
faint = Styler(FGFaint)
underlined = Styler(FGUnderline)
)
// Icons used for displaying prompts or status
var (
IconInitial = Styler(FGBlue)("?")
IconGood = Styler(FGGreen)("✔")
IconWarn = Styler(FGYellow)("⚠")
IconBad = Styler(FGRed)("✗")
)
var red = Styler(FGBold, FGRed)

17
styles_windows.go Normal file
View file

@ -0,0 +1,17 @@
package promptui
var (
bold = Styler(FGBold)
faint = Styler(FGFaint)
underlined = Styler(FGUnderline)
)
// Icons used for displaying prompts or status
var (
IconInitial = Styler(FGBlue)("?")
IconGood = Styler(FGGreen)("v")
IconWarn = Styler(FGYellow)("!")
IconBad = Styler(FGRed)("x")
)
var red = Styler(FGBold, FGRed)