1package repo
2
3import (
4 "errors"
5 "fmt"
6 "sort"
7 "strings"
8
9 "github.com/charmbracelet/bubbles/key"
10 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/soft-serve/git"
12 ggit "github.com/charmbracelet/soft-serve/git"
13 "github.com/charmbracelet/soft-serve/server/backend"
14 "github.com/charmbracelet/soft-serve/ui/common"
15 "github.com/charmbracelet/soft-serve/ui/components/selector"
16 "github.com/charmbracelet/soft-serve/ui/components/tabs"
17)
18
19var (
20 errNoRef = errors.New("no reference specified")
21)
22
23// RefMsg is a message that contains a git.Reference.
24type RefMsg *ggit.Reference
25
26// RefItemsMsg is a message that contains a list of RefItem.
27type RefItemsMsg struct {
28 prefix string
29 items []selector.IdentifiableItem
30}
31
32// Refs is a component that displays a list of references.
33type Refs struct {
34 common common.Common
35 selector *selector.Selector
36 repo backend.Repository
37 ref *git.Reference
38 activeRef *git.Reference
39 refPrefix string
40}
41
42// NewRefs creates a new Refs component.
43func NewRefs(common common.Common, refPrefix string) *Refs {
44 r := &Refs{
45 common: common,
46 refPrefix: refPrefix,
47 }
48 s := selector.New(common, []selector.IdentifiableItem{}, RefItemDelegate{&common})
49 s.SetShowFilter(false)
50 s.SetShowHelp(false)
51 s.SetShowPagination(false)
52 s.SetShowStatusBar(false)
53 s.SetShowTitle(false)
54 s.SetFilteringEnabled(false)
55 s.DisableQuitKeybindings()
56 r.selector = s
57 return r
58}
59
60// SetSize implements common.Component.
61func (r *Refs) SetSize(width, height int) {
62 r.common.SetSize(width, height)
63 r.selector.SetSize(width, height)
64}
65
66// ShortHelp implements help.KeyMap.
67func (r *Refs) ShortHelp() []key.Binding {
68 copyKey := r.common.KeyMap.Copy
69 copyKey.SetHelp("c", "copy ref")
70 k := r.selector.KeyMap
71 return []key.Binding{
72 r.common.KeyMap.SelectItem,
73 k.CursorUp,
74 k.CursorDown,
75 copyKey,
76 }
77}
78
79// FullHelp implements help.KeyMap.
80func (r *Refs) FullHelp() [][]key.Binding {
81 copyKey := r.common.KeyMap.Copy
82 copyKey.SetHelp("c", "copy ref")
83 k := r.selector.KeyMap
84 return [][]key.Binding{
85 {r.common.KeyMap.SelectItem},
86 {
87 k.CursorUp,
88 k.CursorDown,
89 k.NextPage,
90 k.PrevPage,
91 },
92 {
93 k.GoToStart,
94 k.GoToEnd,
95 copyKey,
96 },
97 }
98}
99
100// Init implements tea.Model.
101func (r *Refs) Init() tea.Cmd {
102 return r.updateItemsCmd
103}
104
105// Update implements tea.Model.
106func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107 cmds := make([]tea.Cmd, 0)
108 switch msg := msg.(type) {
109 case RepoMsg:
110 r.selector.Select(0)
111 r.repo = msg
112 case RefMsg:
113 r.ref = msg
114 cmds = append(cmds, r.Init())
115 case RefItemsMsg:
116 if r.refPrefix == msg.prefix {
117 cmds = append(cmds, r.selector.SetItems(msg.items))
118 i := r.selector.SelectedItem()
119 if i != nil {
120 r.activeRef = i.(RefItem).Reference
121 }
122 }
123 case selector.ActiveMsg:
124 switch sel := msg.IdentifiableItem.(type) {
125 case RefItem:
126 r.activeRef = sel.Reference
127 }
128 cmds = append(cmds, updateStatusBarCmd)
129 case selector.SelectMsg:
130 switch i := msg.IdentifiableItem.(type) {
131 case RefItem:
132 cmds = append(cmds,
133 switchRefCmd(i.Reference),
134 tabs.SelectTabCmd(int(filesTab)),
135 )
136 }
137 case tea.KeyMsg:
138 switch {
139 case key.Matches(msg, r.common.KeyMap.SelectItem):
140 cmds = append(cmds, r.selector.SelectItem)
141 }
142 case EmptyRepoMsg:
143 r.ref = nil
144 cmds = append(cmds, r.setItems([]selector.IdentifiableItem{}))
145 }
146 m, cmd := r.selector.Update(msg)
147 r.selector = m.(*selector.Selector)
148 if cmd != nil {
149 cmds = append(cmds, cmd)
150 }
151 return r, tea.Batch(cmds...)
152}
153
154// View implements tea.Model.
155func (r *Refs) View() string {
156 return r.selector.View()
157}
158
159// StatusBarValue implements statusbar.StatusBar.
160func (r *Refs) StatusBarValue() string {
161 if r.activeRef == nil {
162 return ""
163 }
164 return r.activeRef.Name().String()
165}
166
167// StatusBarInfo implements statusbar.StatusBar.
168func (r *Refs) StatusBarInfo() string {
169 totalPages := r.selector.TotalPages()
170 if totalPages > 1 {
171 return fmt.Sprintf("p. %d/%d", r.selector.Page()+1, totalPages)
172 }
173 return ""
174}
175
176func (r *Refs) updateItemsCmd() tea.Msg {
177 its := make(RefItems, 0)
178 rr, err := r.repo.Open()
179 if err != nil {
180 return common.ErrorMsg(err)
181 }
182 refs, err := rr.References()
183 if err != nil {
184 r.common.Logger.Debugf("ui: error getting references: %v", err)
185 return common.ErrorMsg(err)
186 }
187 for _, ref := range refs {
188 if strings.HasPrefix(ref.Name().String(), r.refPrefix) {
189 its = append(its, RefItem{Reference: ref})
190 }
191 }
192 sort.Sort(its)
193 items := make([]selector.IdentifiableItem, len(its))
194 for i, it := range its {
195 items[i] = it
196 }
197 return RefItemsMsg{
198 items: items,
199 prefix: r.refPrefix,
200 }
201}
202
203func (r *Refs) setItems(items []selector.IdentifiableItem) tea.Cmd {
204 return func() tea.Msg {
205 return RefItemsMsg{
206 items: items,
207 prefix: r.refPrefix,
208 }
209 }
210}
211
212func switchRefCmd(ref *ggit.Reference) tea.Cmd {
213 return func() tea.Msg {
214 return RefMsg(ref)
215 }
216}
217
218// UpdateRefCmd gets the repository's HEAD reference and sends a RefMsg.
219func UpdateRefCmd(repo backend.Repository) tea.Cmd {
220 return func() tea.Msg {
221 r, err := repo.Open()
222 if err != nil {
223 return common.ErrorMsg(err)
224 }
225 bs, _ := r.Branches()
226 if len(bs) == 0 {
227 return EmptyRepoMsg{}
228 }
229 ref, err := r.HEAD()
230 if err != nil {
231 return common.ErrorMsg(err)
232 }
233 return RefMsg(ref)
234 }
235}