picker.go 3.9 KB

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