You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
251 lines
6.5 KiB
251 lines
6.5 KiB
package survey |
|
|
|
import ( |
|
"errors" |
|
"strings" |
|
|
|
"gopkg.in/AlecAivazis/survey.v1/core" |
|
"gopkg.in/AlecAivazis/survey.v1/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, nil) |
|
*/ |
|
type MultiSelect struct { |
|
core.Renderer |
|
Message string |
|
Options []string |
|
Default []string |
|
Help string |
|
PageSize int |
|
VimMode bool |
|
FilterMessage string |
|
filter string |
|
selectedIndex int |
|
checked map[string]bool |
|
showingHelp bool |
|
} |
|
|
|
// data available to the templates when processing |
|
type MultiSelectTemplateData struct { |
|
MultiSelect |
|
Answer string |
|
ShowAnswer bool |
|
Checked map[string]bool |
|
SelectedIndex int |
|
ShowHelp bool |
|
PageEntries []string |
|
} |
|
|
|
var MultiSelectQuestionTemplate = ` |
|
{{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} |
|
{{- color "green+hb"}}{{ QuestionIcon }} {{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)}}, {{ HelpInputRune }} for more help{{end}}]{{color "reset"}} |
|
{{- "\n"}} |
|
{{- range $ix, $option := .PageEntries}} |
|
{{- if eq $ix $.SelectedIndex}}{{color "cyan"}}{{ SelectFocusIcon }}{{color "reset"}}{{else}} {{end}} |
|
{{- if index $.Checked $option}}{{color "green"}} {{ MarkedOptionIcon }} {{else}}{{color "default+hb"}} {{ UnmarkedOptionIcon }} {{end}} |
|
{{- color "reset"}} |
|
{{- " "}}{{$option}}{{"\n"}} |
|
{{- end}} |
|
{{- end}}` |
|
|
|
// OnChange is called on every keypress. |
|
func (m *MultiSelect) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { |
|
options := m.filterOptions() |
|
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.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 { |
|
if m.selectedIndex < len(options) { |
|
if old, ok := m.checked[options[m.selectedIndex]]; !ok { |
|
// otherwise just invert the current value |
|
m.checked[options[m.selectedIndex]] = true |
|
} else { |
|
// otherwise just invert the current value |
|
m.checked[options[m.selectedIndex]] = !old |
|
} |
|
m.filter = "" |
|
} |
|
// only show the help message if we have one to show |
|
} else if key == core.HelpInputRune && 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 != "" { |
|
m.filter = m.filter[0 : len(m.filter)-1] |
|
} |
|
} else if key >= terminal.KeySpace { |
|
m.filter += string(key) |
|
m.VimMode = false |
|
} |
|
|
|
m.FilterMessage = "" |
|
if m.filter != "" { |
|
m.FilterMessage = " " + m.filter |
|
} |
|
if oldFilter != m.filter { |
|
// filter changed |
|
options = m.filterOptions() |
|
if len(options) > 0 && len(options) <= m.selectedIndex { |
|
m.selectedIndex = len(options) - 1 |
|
} |
|
} |
|
// paginate the options |
|
|
|
// 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(m.PageSize, options, m.selectedIndex) |
|
|
|
// render the options |
|
m.Render( |
|
MultiSelectQuestionTemplate, |
|
MultiSelectTemplateData{ |
|
MultiSelect: *m, |
|
SelectedIndex: idx, |
|
Checked: m.checked, |
|
ShowHelp: m.showingHelp, |
|
PageEntries: opts, |
|
}, |
|
) |
|
|
|
// if we are not pressing ent |
|
return line, 0, true |
|
} |
|
|
|
func (m *MultiSelect) filterOptions() []string { |
|
filter := strings.ToLower(m.filter) |
|
if filter == "" { |
|
return m.Options |
|
} |
|
answer := []string{} |
|
for _, o := range m.Options { |
|
if strings.Contains(strings.ToLower(o), filter) { |
|
answer = append(answer, o) |
|
} |
|
} |
|
return answer |
|
} |
|
|
|
func (m *MultiSelect) Prompt() (interface{}, error) { |
|
// compute the default state |
|
m.checked = make(map[string]bool) |
|
// if there is a default |
|
if len(m.Default) > 0 { |
|
for _, dflt := range m.Default { |
|
for _, opt := range m.Options { |
|
// if the option correponds to the default |
|
if opt == dflt { |
|
// we found our initial value |
|
m.checked[opt] = true |
|
// stop looking |
|
break |
|
} |
|
} |
|
} |
|
} |
|
|
|
// if there are no options to render |
|
if len(m.Options) == 0 { |
|
// we failed |
|
return "", errors.New("please provide options to select from") |
|
} |
|
|
|
// paginate the options |
|
opts, idx := paginate(m.PageSize, m.Options, m.selectedIndex) |
|
|
|
cursor := m.NewCursor() |
|
cursor.Hide() // hide the cursor |
|
defer cursor.Show() // show the cursor when we're done |
|
|
|
// ask the question |
|
err := m.Render( |
|
MultiSelectQuestionTemplate, |
|
MultiSelectTemplateData{ |
|
MultiSelect: *m, |
|
SelectedIndex: idx, |
|
Checked: m.checked, |
|
PageEntries: opts, |
|
}, |
|
) |
|
if err != nil { |
|
return "", err |
|
} |
|
|
|
rr := m.NewRuneReader() |
|
rr.SetTermMode() |
|
defer rr.RestoreTermMode() |
|
|
|
// start waiting for input |
|
for { |
|
r, _, _ := rr.ReadRune() |
|
if r == '\r' || r == '\n' { |
|
break |
|
} |
|
if r == terminal.KeyInterrupt { |
|
return "", terminal.InterruptErr |
|
} |
|
if r == terminal.KeyEndTransmission { |
|
break |
|
} |
|
m.OnChange(nil, 0, r) |
|
} |
|
m.filter = "" |
|
m.FilterMessage = "" |
|
|
|
answers := []string{} |
|
for _, option := range m.Options { |
|
if val, ok := m.checked[option]; ok && val { |
|
answers = append(answers, option) |
|
} |
|
} |
|
|
|
return answers, nil |
|
} |
|
|
|
// Cleanup removes the options section, and renders the ask like a normal question. |
|
func (m *MultiSelect) Cleanup(val interface{}) error { |
|
// execute the output summary template with the answer |
|
return m.Render( |
|
MultiSelectQuestionTemplate, |
|
MultiSelectTemplateData{ |
|
MultiSelect: *m, |
|
SelectedIndex: m.selectedIndex, |
|
Checked: m.checked, |
|
Answer: strings.Join(val.([]string), ", "), |
|
ShowAnswer: true, |
|
}, |
|
) |
|
}
|
|
|