multiselect.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. package survey
  2. import (
  3. "errors"
  4. "os"
  5. "strings"
  6. "github.com/kataras/survey/core"
  7. "github.com/kataras/survey/terminal"
  8. )
  9. /*
  10. MultiSelect is a prompt that presents a list of various options to the user
  11. for them to select using the arrow keys and enter. Response type is a slice of strings.
  12. days := []string{}
  13. prompt := &survey.MultiSelect{
  14. Message: "What days do you prefer:",
  15. Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
  16. }
  17. survey.AskOne(prompt, &days, nil)
  18. */
  19. type MultiSelect struct {
  20. core.Renderer
  21. Message string
  22. Options []string
  23. Default []string
  24. Help string
  25. PageSize int
  26. selectedIndex int
  27. checked map[string]bool
  28. showingHelp bool
  29. }
  30. // data available to the templates when processing
  31. type MultiSelectTemplateData struct {
  32. MultiSelect
  33. Answer string
  34. ShowAnswer bool
  35. Checked map[string]bool
  36. SelectedIndex int
  37. ShowHelp bool
  38. PageEntries []string
  39. }
  40. var MultiSelectQuestionTemplate = `
  41. {{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
  42. {{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}}
  43. {{- color "default+hb"}}{{ .Message }}{{color "reset"}}
  44. {{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
  45. {{- else }}
  46. {{- if and .Help (not .ShowHelp)}} {{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}}{{end}}
  47. {{- "\n"}}
  48. {{- range $ix, $option := .PageEntries}}
  49. {{- if eq $ix $.SelectedIndex}}{{color "cyan"}}{{ SelectFocusIcon }}{{color "reset"}}{{else}} {{end}}
  50. {{- if index $.Checked $option}}{{color "green"}} {{ MarkedOptionIcon }} {{else}}{{color "default+hb"}} {{ UnmarkedOptionIcon }} {{end}}
  51. {{- color "reset"}}
  52. {{- " "}}{{$option}}{{"\n"}}
  53. {{- end}}
  54. {{- end}}`
  55. // OnChange is called on every keypress.
  56. func (m *MultiSelect) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
  57. if key == terminal.KeyArrowUp {
  58. // if we are at the top of the list
  59. if m.selectedIndex == 0 {
  60. // go to the bottom
  61. m.selectedIndex = len(m.Options) - 1
  62. } else {
  63. // decrement the selected index
  64. m.selectedIndex--
  65. }
  66. } else if key == terminal.KeyArrowDown {
  67. // if we are at the bottom of the list
  68. if m.selectedIndex == len(m.Options)-1 {
  69. // start at the top
  70. m.selectedIndex = 0
  71. } else {
  72. // increment the selected index
  73. m.selectedIndex++
  74. }
  75. // if the user pressed down and there is room to move
  76. } else if key == terminal.KeySpace {
  77. if old, ok := m.checked[m.Options[m.selectedIndex]]; !ok {
  78. // otherwise just invert the current value
  79. m.checked[m.Options[m.selectedIndex]] = true
  80. } else {
  81. // otherwise just invert the current value
  82. m.checked[m.Options[m.selectedIndex]] = !old
  83. }
  84. // only show the help message if we have one to show
  85. } else if key == core.HelpInputRune && m.Help != "" {
  86. m.showingHelp = true
  87. }
  88. // paginate the options
  89. opts, idx := paginate(m.PageSize, m.Options, m.selectedIndex)
  90. // render the options
  91. m.Render(
  92. MultiSelectQuestionTemplate,
  93. MultiSelectTemplateData{
  94. MultiSelect: *m,
  95. SelectedIndex: idx,
  96. Checked: m.checked,
  97. ShowHelp: m.showingHelp,
  98. PageEntries: opts,
  99. },
  100. )
  101. // if we are not pressing ent
  102. return line, 0, true
  103. }
  104. func (m *MultiSelect) Prompt() (interface{}, error) {
  105. // compute the default state
  106. m.checked = make(map[string]bool)
  107. // if there is a default
  108. if len(m.Default) > 0 {
  109. for _, dflt := range m.Default {
  110. for _, opt := range m.Options {
  111. // if the option correponds to the default
  112. if opt == dflt {
  113. // we found our initial value
  114. m.checked[opt] = true
  115. // stop looking
  116. break
  117. }
  118. }
  119. }
  120. }
  121. // if there are no options to render
  122. if len(m.Options) == 0 {
  123. // we failed
  124. return "", errors.New("please provide options to select from")
  125. }
  126. // hide the cursor
  127. terminal.CursorHide()
  128. // show the cursor when we're done
  129. defer terminal.CursorShow()
  130. // paginate the options
  131. opts, idx := paginate(m.PageSize, m.Options, m.selectedIndex)
  132. // ask the question
  133. err := m.Render(
  134. MultiSelectQuestionTemplate,
  135. MultiSelectTemplateData{
  136. MultiSelect: *m,
  137. SelectedIndex: idx,
  138. Checked: m.checked,
  139. PageEntries: opts,
  140. },
  141. )
  142. if err != nil {
  143. return "", err
  144. }
  145. rr := terminal.NewRuneReader(os.Stdin)
  146. rr.SetTermMode()
  147. defer rr.RestoreTermMode()
  148. // start waiting for input
  149. for {
  150. r, _, _ := rr.ReadRune()
  151. if r == '\r' || r == '\n' {
  152. break
  153. }
  154. if r == terminal.KeyInterrupt {
  155. return "", terminal.InterruptErr
  156. }
  157. if r == terminal.KeyEndTransmission {
  158. break
  159. }
  160. m.OnChange(nil, 0, r)
  161. }
  162. answers := []string{}
  163. for _, option := range m.Options {
  164. if val, ok := m.checked[option]; ok && val {
  165. answers = append(answers, option)
  166. }
  167. }
  168. return answers, nil
  169. }
  170. // Cleanup removes the options section, and renders the ask like a normal question.
  171. func (m *MultiSelect) Cleanup(val interface{}) error {
  172. // execute the output summary template with the answer
  173. return m.Render(
  174. MultiSelectQuestionTemplate,
  175. MultiSelectTemplateData{
  176. MultiSelect: *m,
  177. SelectedIndex: m.selectedIndex,
  178. Checked: m.checked,
  179. Answer: strings.Join(val.([]string), ", "),
  180. ShowAnswer: true,
  181. },
  182. )
  183. }