|
@@ -0,0 +1,159 @@
|
|
|
|
|
+package picker
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "github.com/ayn2op/tview"
|
|
|
|
|
+ "github.com/ayn2op/tview/flex"
|
|
|
|
|
+ "github.com/ayn2op/tview/keybind"
|
|
|
|
|
+ "github.com/ayn2op/tview/list"
|
|
|
|
|
+ "github.com/gdamore/tcell/v3"
|
|
|
|
|
+ "github.com/sahilm/fuzzy"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// bottom border + value
|
|
|
|
|
+const inputHeight = 2
|
|
|
|
|
+
|
|
|
|
|
+type Model struct {
|
|
|
|
|
+ *flex.Model
|
|
|
|
|
+ input *tview.InputField
|
|
|
|
|
+ list *list.Model
|
|
|
|
|
+
|
|
|
|
|
+ keyMap *KeyMap
|
|
|
|
|
+
|
|
|
|
|
+ items Items
|
|
|
|
|
+ filtered Items
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func NewModel() *Model {
|
|
|
|
|
+ m := &Model{
|
|
|
|
|
+ Model: flex.NewModel(),
|
|
|
|
|
+ input: tview.NewInputField(),
|
|
|
|
|
+ list: list.NewModel(),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Show a horizontal bottom border to visually separate input from list.
|
|
|
|
|
+ var borderSet tview.BorderSet
|
|
|
|
|
+ borderSet.Bottom = tview.BoxDrawingsLightHorizontal
|
|
|
|
|
+ borderSet.BottomLeft = borderSet.Bottom
|
|
|
|
|
+ borderSet.BottomRight = borderSet.Bottom
|
|
|
|
|
+
|
|
|
|
|
+ m.input.
|
|
|
|
|
+ SetChangedFunc(m.onInputChanged).
|
|
|
|
|
+ SetLabel("> ").
|
|
|
|
|
+ SetBorders(tview.BordersBottom).
|
|
|
|
|
+ SetBorderSet(borderSet).
|
|
|
|
|
+ SetBorderStyle(tcell.StyleDefault.Dim(true))
|
|
|
|
|
+
|
|
|
|
|
+ m.
|
|
|
|
|
+ SetDirection(flex.DirectionRow).
|
|
|
|
|
+ AddItem(m.input, inputHeight, 0, true).
|
|
|
|
|
+ AddItem(m.list, 0, 1, false)
|
|
|
|
|
+
|
|
|
|
|
+ m.Update()
|
|
|
|
|
+ return m
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *Model) setFilteredItems(filtered Items) {
|
|
|
|
|
+ m.filtered = filtered
|
|
|
|
|
+
|
|
|
|
|
+ m.list.SetBuilder(func(index int, cursor int) list.Item {
|
|
|
|
|
+ if index < 0 || index >= len(m.filtered) {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+ style := tcell.StyleDefault
|
|
|
|
|
+ if index == cursor {
|
|
|
|
|
+ style = style.Reverse(true)
|
|
|
|
|
+ }
|
|
|
|
|
+ return tview.NewTextView().
|
|
|
|
|
+ SetScrollable(false).
|
|
|
|
|
+ SetWrap(false).
|
|
|
|
|
+ SetWordWrap(false).
|
|
|
|
|
+ SetTextStyle(style).
|
|
|
|
|
+ SetLines([]tview.Line{{{Text: m.filtered[index].Text, Style: style}}})
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if len(filtered) == 0 {
|
|
|
|
|
+ m.list.SetCursor(-1)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ m.list.SetCursor(0)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *Model) SetKeyMap(keyMap *KeyMap) {
|
|
|
|
|
+ m.keyMap = keyMap
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// SetScrollBarVisibility sets when the picker's list scrollBar is rendered.
|
|
|
|
|
+func (m *Model) SetScrollBarVisibility(visibility list.ScrollBarVisibility) {
|
|
|
|
|
+ m.list.SetScrollBarVisibility(visibility)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// SetScrollBar sets the scrollBar primitive used by the picker's list.
|
|
|
|
|
+func (m *Model) SetScrollBar(scrollBar *tview.ScrollBar) {
|
|
|
|
|
+ m.list.SetScrollBar(scrollBar)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *Model) ClearInput() {
|
|
|
|
|
+ m.input.SetText("")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *Model) ClearList() {
|
|
|
|
|
+ m.filtered = nil
|
|
|
|
|
+ m.list.Clear()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *Model) ClearItems() {
|
|
|
|
|
+ m.items = nil
|
|
|
|
|
+ m.filtered = nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *Model) AddItem(item Item) {
|
|
|
|
|
+ m.items = append(m.items, item)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *Model) Update() {
|
|
|
|
|
+ m.ClearInput()
|
|
|
|
|
+ m.onInputChanged("")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *Model) onInputChanged(text string) {
|
|
|
|
|
+ var fuzzied Items
|
|
|
|
|
+ if text == "" {
|
|
|
|
|
+ fuzzied = append(fuzzied, m.items...)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ matches := fuzzy.FindFrom(text, m.items)
|
|
|
|
|
+ for _, match := range matches {
|
|
|
|
|
+ fuzzied = append(fuzzied, m.items[match.Index])
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ m.setFilteredItems(fuzzied)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|
|
|
|
+ switch event := event.(type) {
|
|
|
|
|
+ case *tview.KeyEvent:
|
|
|
|
|
+ if m.keyMap != nil {
|
|
|
|
|
+ switch {
|
|
|
|
|
+ case keybind.Matches(event, m.keyMap.Up):
|
|
|
|
|
+ m.list.HandleEvent(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
|
|
|
|
|
+ return nil
|
|
|
|
|
+ case keybind.Matches(event, m.keyMap.Down):
|
|
|
|
|
+ m.list.HandleEvent(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
|
|
|
|
|
+ return nil
|
|
|
|
|
+ case keybind.Matches(event, m.keyMap.Top):
|
|
|
|
|
+ m.list.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
|
|
|
|
|
+ return nil
|
|
|
|
|
+ case keybind.Matches(event, m.keyMap.Bottom):
|
|
|
|
|
+ m.list.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
|
|
|
|
|
+ return nil
|
|
|
|
|
+
|
|
|
|
|
+ case keybind.Matches(event, m.keyMap.Select):
|
|
|
|
|
+ return m._select()
|
|
|
|
|
+ case keybind.Matches(event, m.keyMap.Cancel):
|
|
|
|
|
+ return cancel()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return m.Model.HandleEvent(event)
|
|
|
|
|
+ }
|
|
|
|
|
+ return m.Model.HandleEvent(event)
|
|
|
|
|
+}
|