picker.go 4.0 KB

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