1package selector
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 "github.com/charmbracelet/bubbles/list"
6 tea "github.com/charmbracelet/bubbletea"
7 "github.com/charmbracelet/soft-serve/ui/common"
8)
9
10// Selector is a list of items that can be selected.
11type Selector struct {
12 list list.Model
13 common common.Common
14 active int
15 filterState list.FilterState
16}
17
18// IdentifiableItem is an item that can be identified by a string and extends list.Item.
19type IdentifiableItem interface {
20 list.DefaultItem
21 ID() string
22}
23
24// SelectMsg is a message that is sent when an item is selected.
25type SelectMsg string
26
27// ActiveMsg is a message that is sent when an item is active but not selected.
28type ActiveMsg string
29
30// New creates a new selector.
31func New(common common.Common, items []IdentifiableItem, delegate list.ItemDelegate) *Selector {
32 itms := make([]list.Item, len(items))
33 for i, item := range items {
34 itms[i] = item
35 }
36 l := list.New(itms, delegate, common.Width, common.Height)
37 l.SetShowTitle(false)
38 l.SetShowHelp(false)
39 l.SetShowStatusBar(false)
40 l.DisableQuitKeybindings()
41 s := &Selector{
42 list: l,
43 common: common,
44 }
45 s.SetSize(common.Width, common.Height)
46 return s
47}
48
49// KeyMap returns the underlying list's keymap.
50func (s *Selector) KeyMap() list.KeyMap {
51 return s.list.KeyMap
52}
53
54// SetSize implements common.Component.
55func (s *Selector) SetSize(width, height int) {
56 s.common.SetSize(width, height)
57 s.list.SetSize(width, height)
58}
59
60// SetItems sets the items in the selector.
61func (s *Selector) SetItems(items []list.Item) tea.Cmd {
62 return s.list.SetItems(items)
63}
64
65// Index returns the index of the selected item.
66func (s *Selector) Index() int {
67 return s.list.Index()
68}
69
70// Init implements tea.Model.
71func (s *Selector) Init() tea.Cmd {
72 return s.activeCmd
73}
74
75// Update implements tea.Model.
76func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
77 cmds := make([]tea.Cmd, 0)
78 switch msg := msg.(type) {
79 case tea.MouseMsg:
80 switch msg.Type {
81 case tea.MouseWheelUp:
82 s.list.CursorUp()
83 case tea.MouseWheelDown:
84 s.list.CursorDown()
85 }
86 case tea.KeyMsg:
87 switch {
88 case key.Matches(msg, s.common.Keymap.Select):
89 cmds = append(cmds, s.selectCmd)
90 }
91 case list.FilterMatchesMsg:
92 cmds = append(cmds, s.activeFilterCmd)
93 }
94 m, cmd := s.list.Update(msg)
95 s.list = m
96 if cmd != nil {
97 cmds = append(cmds, cmd)
98 }
99 // Track filter state and update active item when filter state changes.
100 filterState := s.list.FilterState()
101 if s.filterState != filterState {
102 cmds = append(cmds, s.activeFilterCmd)
103 }
104 s.filterState = filterState
105 // Send ActiveMsg when index change.
106 if s.active != s.list.Index() {
107 cmds = append(cmds, s.activeCmd)
108 }
109 s.active = s.list.Index()
110 return s, tea.Batch(cmds...)
111}
112
113// View implements tea.Model.
114func (s *Selector) View() string {
115 return s.list.View()
116}
117
118func (s *Selector) selectCmd() tea.Msg {
119 item := s.list.SelectedItem()
120 i, ok := item.(IdentifiableItem)
121 if !ok {
122 return SelectMsg("")
123 }
124 return SelectMsg(i.ID())
125}
126
127func (s *Selector) activeCmd() tea.Msg {
128 item := s.list.SelectedItem()
129 i, ok := item.(IdentifiableItem)
130 if !ok {
131 return ActiveMsg("")
132 }
133 return ActiveMsg(i.ID())
134}
135
136func (s *Selector) activeFilterCmd() tea.Msg {
137 // Here we use VisibleItems because when list.FilterMatchesMsg is sent,
138 // VisibleItems is the only way to get the list of filtered items. The list
139 // bubble should export something like list.FilterMatchesMsg.Items().
140 items := s.list.VisibleItems()
141 if len(items) == 0 {
142 return nil
143 }
144 item := items[0]
145 i, ok := item.(IdentifiableItem)
146 if !ok {
147 return nil
148 }
149 return ActiveMsg(i.ID())
150}