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