picker.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. package picker
  2. import (
  3. "github.com/ayn2op/tview"
  4. "github.com/gdamore/tcell/v3"
  5. "github.com/sahilm/fuzzy"
  6. )
  7. type (
  8. SelectedFunc func(item Item)
  9. CancelFunc func()
  10. )
  11. type Picker struct {
  12. *tview.Flex
  13. input *tview.InputField
  14. list *tview.List
  15. onSelected SelectedFunc
  16. onCancel CancelFunc
  17. keyMap *KeyMap
  18. items Items
  19. filtered Items
  20. }
  21. func New() *Picker {
  22. p := &Picker{
  23. Flex: tview.NewFlex(),
  24. input: tview.NewInputField(),
  25. list: tview.NewList(),
  26. }
  27. // Show a horizontal bottom border to visually separate input from list.
  28. var borderSet tview.BorderSet
  29. borderSet.Bottom = tview.BoxDrawingsLightHorizontal
  30. borderSet.BottomLeft = borderSet.Bottom
  31. borderSet.BottomRight = borderSet.Bottom
  32. p.input.
  33. SetChangedFunc(p.onInputChanged).
  34. SetLabel("> ").
  35. SetBorders(tview.BordersBottom).
  36. SetBorderSet(borderSet).
  37. SetBorderStyle(tcell.StyleDefault.Dim(true)).
  38. SetInputCapture(p.onInputCapture)
  39. p.
  40. SetDirection(tview.FlexRow).
  41. // bottom border + value
  42. AddItem(p.input, 2, 0, true).
  43. AddItem(p.list, 0, 1, false)
  44. p.Update()
  45. return p
  46. }
  47. func (p *Picker) setFilteredItems(filtered Items) {
  48. p.filtered = filtered
  49. p.list.SetBuilder(func(index int, cursor int) tview.ListItem {
  50. if index < 0 || index >= len(p.filtered) {
  51. return nil
  52. }
  53. style := tcell.StyleDefault
  54. if index == cursor {
  55. style = style.Reverse(true)
  56. }
  57. return tview.NewTextView().
  58. SetScrollable(false).
  59. SetWrap(false).
  60. SetWordWrap(false).
  61. SetTextStyle(style).
  62. SetLines([]tview.Line{{{Text: p.filtered[index].Text, Style: style}}})
  63. })
  64. if len(filtered) == 0 {
  65. p.list.SetCursor(-1)
  66. } else {
  67. p.list.SetCursor(0)
  68. }
  69. }
  70. func (p *Picker) SetKeyMap(keyMap *KeyMap) {
  71. p.keyMap = keyMap
  72. }
  73. // SetScrollBarVisibility sets when the picker's list scrollBar is rendered.
  74. func (p *Picker) SetScrollBarVisibility(visibility tview.ScrollBarVisibility) {
  75. p.list.SetScrollBarVisibility(visibility)
  76. }
  77. // SetScrollBar sets the scrollBar primitive used by the picker's list.
  78. func (p *Picker) SetScrollBar(scrollBar *tview.ScrollBar) {
  79. p.list.SetScrollBar(scrollBar)
  80. }
  81. func (p *Picker) SetSelectedFunc(onSelected SelectedFunc) {
  82. p.onSelected = onSelected
  83. }
  84. func (p *Picker) SetCancelFunc(onCancel CancelFunc) {
  85. p.onCancel = onCancel
  86. }
  87. func (p *Picker) ClearInput() {
  88. p.input.SetText("")
  89. }
  90. func (p *Picker) ClearList() {
  91. p.filtered = nil
  92. p.list.Clear()
  93. }
  94. func (p *Picker) ClearItems() {
  95. p.items = nil
  96. p.filtered = nil
  97. }
  98. func (p *Picker) AddItem(item Item) {
  99. p.items = append(p.items, item)
  100. }
  101. func (p *Picker) Update() {
  102. p.ClearInput()
  103. p.onInputChanged("")
  104. }
  105. func (p *Picker) onListSelected(index int) {
  106. if p.onSelected != nil {
  107. if index >= 0 && index < len(p.filtered) {
  108. item := p.filtered[index]
  109. p.onSelected(item)
  110. }
  111. }
  112. }
  113. func (p *Picker) onInputChanged(text string) {
  114. var fuzzied Items
  115. if text == "" {
  116. fuzzied = append(fuzzied, p.items...)
  117. } else {
  118. matches := fuzzy.FindFrom(text, p.items)
  119. for _, match := range matches {
  120. fuzzied = append(fuzzied, p.items[match.Index])
  121. }
  122. }
  123. p.setFilteredItems(fuzzied)
  124. }
  125. func (p *Picker) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
  126. if p.keyMap == nil {
  127. return nil
  128. }
  129. handler := p.list.InputHandler()
  130. switch event.Name() {
  131. case p.keyMap.Up:
  132. handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
  133. return nil
  134. case p.keyMap.Down:
  135. handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil)
  136. return nil
  137. case p.keyMap.Top:
  138. handler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil)
  139. return nil
  140. case p.keyMap.Bottom:
  141. handler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil)
  142. case p.keyMap.Select:
  143. p.onListSelected(p.list.Cursor())
  144. return nil
  145. case p.keyMap.Cancel:
  146. if p.onCancel != nil {
  147. p.onCancel()
  148. }
  149. return nil
  150. }
  151. return event
  152. }