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.KeyMsg:
80 switch {
81 case key.Matches(msg, s.common.Keymap.Select):
82 cmds = append(cmds, s.selectCmd)
83 }
84 case list.FilterMatchesMsg:
85 cmds = append(cmds, s.activeFilterCmd)
86 }
87 m, cmd := s.list.Update(msg)
88 s.list = m
89 if cmd != nil {
90 cmds = append(cmds, cmd)
91 }
92 // Track filter state and update active item when filter state changes.
93 filterState := s.list.FilterState()
94 if s.filterState != filterState {
95 cmds = append(cmds, s.activeFilterCmd)
96 }
97 s.filterState = filterState
98 // Send ActiveMsg when index change.
99 if s.active != s.list.Index() {
100 cmds = append(cmds, s.activeCmd)
101 }
102 s.active = s.list.Index()
103 return s, tea.Batch(cmds...)
104}
105
106// View implements tea.Model.
107func (s *Selector) View() string {
108 return s.list.View()
109}
110
111func (s *Selector) selectCmd() tea.Msg {
112 item := s.list.SelectedItem()
113 i, ok := item.(IdentifiableItem)
114 if !ok {
115 return SelectMsg("")
116 }
117 return SelectMsg(i.ID())
118}
119
120func (s *Selector) activeCmd() tea.Msg {
121 item := s.list.SelectedItem()
122 i, ok := item.(IdentifiableItem)
123 if !ok {
124 return ActiveMsg("")
125 }
126 return ActiveMsg(i.ID())
127}
128
129func (s *Selector) activeFilterCmd() tea.Msg {
130 // Here we use VisibleItems because when list.FilterMatchesMsg is sent,
131 // VisibleItems is the only way to get the list of filtered items. The list
132 // bubble should export something like list.FilterMatchesMsg.Items().
133 items := s.list.VisibleItems()
134 if len(items) == 0 {
135 return nil
136 }
137 item := items[0]
138 i, ok := item.(IdentifiableItem)
139 if !ok {
140 return nil
141 }
142 return ActiveMsg(i.ID())
143}