mirror of
https://github.com/manifoldco/promptui.git
synced 2026-03-14 22:35:53 +01:00
Copy promptui from manifoldco/torus-cli
This commit is contained in:
parent
4aa287a08e
commit
7ff5ef96cc
8 changed files with 660 additions and 0 deletions
73
codes.go
Normal file
73
codes.go
Normal 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
32
codes_test.go
Normal 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
223
prompt.go
Normal 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
43
prompt_test.go
Normal 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
44
promptui.go
Normal 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
209
select.go
Normal 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
19
styles.go
Normal 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
17
styles_windows.go
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue