Commited dependencies
This commit is contained in:
500
vendor/github.com/manifoldco/promptui/select.go
generated
vendored
Normal file
500
vendor/github.com/manifoldco/promptui/select.go
generated
vendored
Normal file
@@ -0,0 +1,500 @@
|
||||
package promptui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/juju/ansiterm"
|
||||
"github.com/manifoldco/promptui/list"
|
||||
"github.com/manifoldco/promptui/screenbuf"
|
||||
)
|
||||
|
||||
// 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 is the value displayed on the command line prompt. It can be any
|
||||
// value one would pass to a text/template Execute(), including just a string.
|
||||
Label interface{}
|
||||
|
||||
// Items are the items to use in the list. It can be any slice type one would
|
||||
// pass to a text/template execute, including a string slice.
|
||||
Items interface{}
|
||||
|
||||
// Size is the number of items that should appear on the select before
|
||||
// scrolling. If it is 0, defaults to 5.
|
||||
Size int
|
||||
|
||||
// IsVimMode sets whether readline is using Vim mode.
|
||||
IsVimMode bool
|
||||
|
||||
// Templates can be used to customize the select output. If nil is passed, the
|
||||
// default templates are used.
|
||||
Templates *SelectTemplates
|
||||
|
||||
// Keys can be used to change movement and search keys.
|
||||
Keys *SelectKeys
|
||||
|
||||
// Searcher can be implemented to teach the select how to search for items.
|
||||
Searcher list.Searcher
|
||||
|
||||
label string
|
||||
|
||||
list *list.List
|
||||
}
|
||||
|
||||
// SelectKeys defines which keys can be used for movement and search.
|
||||
type SelectKeys struct {
|
||||
Next Key // Next defaults to down arrow key
|
||||
Prev Key // Prev defaults to up arrow key
|
||||
PageUp Key // PageUp defaults to left arrow key
|
||||
PageDown Key // PageDown defaults to right arrow key
|
||||
Search Key // Search defaults to '/' key
|
||||
}
|
||||
|
||||
// Key defines a keyboard code and a display representation for the help
|
||||
// Check https://github.com/chzyer/readline for a list of codes
|
||||
type Key struct {
|
||||
Code rune
|
||||
Display string
|
||||
}
|
||||
|
||||
// SelectTemplates allow a select prompt to be customized following stdlib
|
||||
// text/template syntax. If any field is blank a default template is used.
|
||||
type SelectTemplates struct {
|
||||
// Active is a text/template for the label.
|
||||
Label string
|
||||
|
||||
// Active is a text/template for when an item is current active.
|
||||
Active string
|
||||
|
||||
// Inactive is a text/template for when an item is not current active.
|
||||
Inactive string
|
||||
|
||||
// Selected is a text/template for when an item was successfully selected.
|
||||
Selected string
|
||||
|
||||
// Details is a text/template for when an item current active to show
|
||||
// additional information. It can have multiple lines.
|
||||
Details string
|
||||
|
||||
// Help is a text/template for displaying instructions at the top. By default
|
||||
// it shows keys for movement and search.
|
||||
Help string
|
||||
|
||||
// FuncMap is a map of helpers for the templates. If nil, the default helpers
|
||||
// are used.
|
||||
FuncMap template.FuncMap
|
||||
|
||||
label *template.Template
|
||||
active *template.Template
|
||||
inactive *template.Template
|
||||
selected *template.Template
|
||||
details *template.Template
|
||||
help *template.Template
|
||||
}
|
||||
|
||||
// Run runs the Select list. It returns the index of the selected element,
|
||||
// and its value.
|
||||
func (s *Select) Run() (int, string, error) {
|
||||
if s.Size == 0 {
|
||||
s.Size = 5
|
||||
}
|
||||
|
||||
l, err := list.New(s.Items, s.Size)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
l.Searcher = s.Searcher
|
||||
|
||||
s.list = l
|
||||
|
||||
s.setKeys()
|
||||
|
||||
err = s.prepareTemplates()
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
c.HistoryLimit = -1
|
||||
c.UniqueEditLine = true
|
||||
|
||||
rl, err := readline.NewEx(c)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
rl.Write([]byte(hideCursor))
|
||||
sb := screenbuf.New(rl)
|
||||
|
||||
var searchInput []rune
|
||||
canSearch := s.Searcher != nil
|
||||
searchMode := false
|
||||
|
||||
c.SetListener(func(line []rune, pos int, key rune) ([]rune, int, bool) {
|
||||
switch {
|
||||
case key == readline.CharEnter:
|
||||
return nil, 0, true
|
||||
case key == s.Keys.Next.Code || (key == 'j' && !searchMode):
|
||||
s.list.Next()
|
||||
case key == s.Keys.Prev.Code || (key == 'k' && !searchMode):
|
||||
s.list.Prev()
|
||||
case key == s.Keys.Search.Code:
|
||||
if !canSearch {
|
||||
break
|
||||
}
|
||||
|
||||
if searchMode {
|
||||
searchMode = false
|
||||
searchInput = nil
|
||||
s.list.CancelSearch()
|
||||
} else {
|
||||
searchMode = true
|
||||
}
|
||||
case key == readline.CharBackspace:
|
||||
if !canSearch || !searchMode {
|
||||
break
|
||||
}
|
||||
|
||||
if len(searchInput) > 1 {
|
||||
searchInput = searchInput[:len(searchInput)-1]
|
||||
s.list.Search(string(searchInput))
|
||||
} else {
|
||||
searchInput = nil
|
||||
s.list.CancelSearch()
|
||||
}
|
||||
case key == s.Keys.PageUp.Code || (key == 'h' && !searchMode):
|
||||
s.list.PageUp()
|
||||
case key == s.Keys.PageDown.Code || (key == 'l' && !searchMode):
|
||||
s.list.PageDown()
|
||||
default:
|
||||
if canSearch && searchMode {
|
||||
searchInput = append(searchInput, line...)
|
||||
s.list.Search(string(searchInput))
|
||||
}
|
||||
}
|
||||
|
||||
if searchMode {
|
||||
header := fmt.Sprintf("Search: %s%s", string(searchInput), cursor)
|
||||
sb.WriteString(header)
|
||||
} else {
|
||||
help := s.renderHelp(canSearch)
|
||||
sb.Write(help)
|
||||
}
|
||||
|
||||
label := render(s.Templates.label, s.Label)
|
||||
sb.Write(label)
|
||||
|
||||
items, idx := s.list.Items()
|
||||
last := len(items) - 1
|
||||
|
||||
for i, item := range items {
|
||||
page := " "
|
||||
|
||||
switch i {
|
||||
case 0:
|
||||
if s.list.CanPageUp() {
|
||||
page = "↑"
|
||||
} else {
|
||||
page = string(top)
|
||||
}
|
||||
case last:
|
||||
if s.list.CanPageDown() {
|
||||
page = "↓"
|
||||
}
|
||||
}
|
||||
|
||||
output := []byte(page + " ")
|
||||
|
||||
if i == idx {
|
||||
output = append(output, render(s.Templates.active, item)...)
|
||||
} else {
|
||||
output = append(output, render(s.Templates.inactive, item)...)
|
||||
}
|
||||
|
||||
sb.Write(output)
|
||||
}
|
||||
|
||||
if idx == list.NotFound {
|
||||
sb.WriteString("")
|
||||
sb.WriteString("No results")
|
||||
} else {
|
||||
active := items[idx]
|
||||
|
||||
details := s.renderDetails(active)
|
||||
for _, d := range details {
|
||||
sb.Write(d)
|
||||
}
|
||||
}
|
||||
|
||||
sb.Flush()
|
||||
|
||||
return nil, 0, true
|
||||
})
|
||||
|
||||
for {
|
||||
_, err = rl.Readline()
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case err == readline.ErrInterrupt, err.Error() == "Interrupt":
|
||||
err = ErrInterrupt
|
||||
case err == io.EOF:
|
||||
err = ErrEOF
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
_, idx := s.list.Items()
|
||||
if idx != list.NotFound {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "Interrupt" {
|
||||
err = ErrInterrupt
|
||||
}
|
||||
sb.Reset()
|
||||
sb.WriteString("")
|
||||
sb.Flush()
|
||||
rl.Write([]byte(showCursor))
|
||||
rl.Close()
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
items, idx := s.list.Items()
|
||||
item := items[idx]
|
||||
|
||||
output := render(s.Templates.selected, item)
|
||||
|
||||
sb.Reset()
|
||||
sb.Write(output)
|
||||
sb.Flush()
|
||||
rl.Write([]byte(showCursor))
|
||||
rl.Close()
|
||||
|
||||
return s.list.Index(), fmt.Sprintf("%v", item), err
|
||||
}
|
||||
|
||||
func (s *Select) prepareTemplates() error {
|
||||
tpls := s.Templates
|
||||
if tpls == nil {
|
||||
tpls = &SelectTemplates{}
|
||||
}
|
||||
|
||||
if tpls.FuncMap == nil {
|
||||
tpls.FuncMap = FuncMap
|
||||
}
|
||||
|
||||
if tpls.Label == "" {
|
||||
tpls.Label = fmt.Sprintf("%s {{.}}: ", IconInitial)
|
||||
}
|
||||
|
||||
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpls.label = tpl
|
||||
|
||||
if tpls.Active == "" {
|
||||
tpls.Active = fmt.Sprintf("%s {{ . | underline }}", IconSelect)
|
||||
}
|
||||
|
||||
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Active)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpls.active = tpl
|
||||
|
||||
if tpls.Inactive == "" {
|
||||
tpls.Inactive = " {{.}}"
|
||||
}
|
||||
|
||||
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Inactive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpls.inactive = tpl
|
||||
|
||||
if tpls.Selected == "" {
|
||||
tpls.Selected = fmt.Sprintf(`{{ "%s" | green }} {{ . | faint }}`, IconGood)
|
||||
}
|
||||
|
||||
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Selected)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tpls.selected = tpl
|
||||
|
||||
if tpls.Details != "" {
|
||||
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Details)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpls.details = tpl
|
||||
}
|
||||
|
||||
if tpls.Help == "" {
|
||||
tpls.Help = fmt.Sprintf(`{{ "Use the arrow keys to navigate:" | faint }} {{ .NextKey | faint }} ` +
|
||||
`{{ .PrevKey | faint }} {{ .PageDownKey | faint }} {{ .PageUpKey | faint }} ` +
|
||||
`{{ if .Search }} {{ "and" | faint }} {{ .SearchKey | faint }} {{ "toggles search" | faint }}{{ end }}`)
|
||||
}
|
||||
|
||||
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Help)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpls.help = tpl
|
||||
|
||||
s.Templates = tpls
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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...)
|
||||
|
||||
list, err := list.New(newItems, 5)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
s := Select{
|
||||
Label: sa.Label,
|
||||
Items: newItems,
|
||||
IsVimMode: sa.IsVimMode,
|
||||
Size: 5,
|
||||
list: list,
|
||||
}
|
||||
s.setKeys()
|
||||
|
||||
err = s.prepareTemplates()
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Select) setKeys() {
|
||||
if s.Keys != nil {
|
||||
return
|
||||
}
|
||||
s.Keys = &SelectKeys{
|
||||
Prev: Key{Code: readline.CharPrev, Display: "↑"},
|
||||
Next: Key{Code: readline.CharNext, Display: "↓"},
|
||||
PageUp: Key{Code: readline.CharBackward, Display: "←"},
|
||||
PageDown: Key{Code: readline.CharForward, Display: "→"},
|
||||
Search: Key{Code: '/', Display: "/"},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Select) renderDetails(item interface{}) [][]byte {
|
||||
if s.Templates.details == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
w := ansiterm.NewTabWriter(&buf, 0, 0, 8, ' ', 0)
|
||||
|
||||
err := s.Templates.details.Execute(w, item)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "%v", item)
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
|
||||
output := buf.Bytes()
|
||||
|
||||
return bytes.Split(output, []byte("\n"))
|
||||
}
|
||||
|
||||
func (s *Select) renderHelp(b bool) []byte {
|
||||
keys := struct {
|
||||
NextKey string
|
||||
PrevKey string
|
||||
PageDownKey string
|
||||
PageUpKey string
|
||||
Search bool
|
||||
SearchKey string
|
||||
}{
|
||||
NextKey: s.Keys.Next.Display,
|
||||
PrevKey: s.Keys.Prev.Display,
|
||||
PageDownKey: s.Keys.PageDown.Display,
|
||||
PageUpKey: s.Keys.PageUp.Display,
|
||||
SearchKey: s.Keys.Search.Display,
|
||||
Search: b,
|
||||
}
|
||||
|
||||
return render(s.Templates.help, keys)
|
||||
}
|
||||
|
||||
func render(tpl *template.Template, data interface{}) []byte {
|
||||
var buf bytes.Buffer
|
||||
err := tpl.Execute(&buf, data)
|
||||
if err != nil {
|
||||
return []byte(fmt.Sprintf("%v", data))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
Reference in New Issue
Block a user