multiselect.go 6.5 KB

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