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/server/proto"
13 "github.com/charmbracelet/soft-serve/server/ui/common"
14 "github.com/charmbracelet/soft-serve/server/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// TabName returns the name of the tab.
61func (r *Refs) TabName() string {
62 if r.refPrefix == git.RefsHeads {
63 return "Branches"
64 } else if r.refPrefix == git.RefsTags {
65 return "Tags"
66 }
67 return "Refs"
68}
69
70// SetSize implements common.Component.
71func (r *Refs) SetSize(width, height int) {
72 r.common.SetSize(width, height)
73 r.selector.SetSize(width, height)
74}
75
76// ShortHelp implements help.KeyMap.
77func (r *Refs) ShortHelp() []key.Binding {
78 copyKey := r.common.KeyMap.Copy
79 copyKey.SetHelp("c", "copy ref")
80 k := r.selector.KeyMap
81 return []key.Binding{
82 r.common.KeyMap.SelectItem,
83 k.CursorUp,
84 k.CursorDown,
85 copyKey,
86 }
87}
88
89// FullHelp implements help.KeyMap.
90func (r *Refs) FullHelp() [][]key.Binding {
91 copyKey := r.common.KeyMap.Copy
92 copyKey.SetHelp("c", "copy ref")
93 k := r.selector.KeyMap
94 return [][]key.Binding{
95 {r.common.KeyMap.SelectItem},
96 {
97 k.CursorUp,
98 k.CursorDown,
99 k.NextPage,
100 k.PrevPage,
101 },
102 {
103 k.GoToStart,
104 k.GoToEnd,
105 copyKey,
106 },
107 }
108}
109
110// Init implements tea.Model.
111func (r *Refs) Init() tea.Cmd {
112 r.isLoading = true
113 return tea.Batch(r.spinner.Tick, r.updateItemsCmd)
114}
115
116// Update implements tea.Model.
117func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
118 cmds := make([]tea.Cmd, 0)
119 switch msg := msg.(type) {
120 case RepoMsg:
121 r.selector.Select(0)
122 r.repo = msg
123 case RefMsg:
124 r.ref = msg
125 cmds = append(cmds, r.Init())
126 case tea.WindowSizeMsg:
127 r.SetSize(msg.Width, msg.Height)
128 case RefItemsMsg:
129 if r.refPrefix == msg.prefix {
130 cmds = append(cmds, r.selector.SetItems(msg.items))
131 i := r.selector.SelectedItem()
132 if i != nil {
133 r.activeRef = i.(RefItem).Reference
134 }
135 r.isLoading = false
136 }
137 case selector.ActiveMsg:
138 switch sel := msg.IdentifiableItem.(type) {
139 case RefItem:
140 r.activeRef = sel.Reference
141 }
142 case selector.SelectMsg:
143 switch i := msg.IdentifiableItem.(type) {
144 case RefItem:
145 cmds = append(cmds,
146 switchRefCmd(i.Reference),
147 switchTabCmd(&Files{}),
148 )
149 }
150 case tea.KeyMsg:
151 switch {
152 case key.Matches(msg, r.common.KeyMap.SelectItem):
153 cmds = append(cmds, r.selector.SelectItemCmd)
154 }
155 case EmptyRepoMsg:
156 r.ref = nil
157 cmds = append(cmds, r.setItems([]selector.IdentifiableItem{}))
158 case spinner.TickMsg:
159 if r.isLoading && r.spinner.ID() == msg.ID {
160 s, cmd := r.spinner.Update(msg)
161 if cmd != nil {
162 cmds = append(cmds, cmd)
163 }
164 r.spinner = s
165 }
166 }
167 m, cmd := r.selector.Update(msg)
168 r.selector = m.(*selector.Selector)
169 if cmd != nil {
170 cmds = append(cmds, cmd)
171 }
172 return r, tea.Batch(cmds...)
173}
174
175// View implements tea.Model.
176func (r *Refs) View() string {
177 if r.isLoading {
178 return renderLoading(r.common, r.spinner)
179 }
180 return r.selector.View()
181}
182
183// SpinnerID implements common.TabComponent.
184func (r *Refs) SpinnerID() int {
185 return r.spinner.ID()
186}
187
188// StatusBarValue implements statusbar.StatusBar.
189func (r *Refs) StatusBarValue() string {
190 if r.activeRef == nil {
191 return ""
192 }
193 return r.activeRef.Name().String()
194}
195
196// StatusBarInfo implements statusbar.StatusBar.
197func (r *Refs) StatusBarInfo() string {
198 totalPages := r.selector.TotalPages()
199 if totalPages <= 1 {
200 return "p. 1/1"
201 }
202 return fmt.Sprintf("p. %d/%d", r.selector.Page()+1, totalPages)
203}
204
205func (r *Refs) updateItemsCmd() tea.Msg {
206 its := make(RefItems, 0)
207 rr, err := r.repo.Open()
208 if err != nil {
209 return common.ErrorMsg(err)
210 }
211 refs, err := rr.References()
212 if err != nil {
213 r.common.Logger.Debugf("ui: error getting references: %v", err)
214 return common.ErrorMsg(err)
215 }
216 for _, ref := range refs {
217 if strings.HasPrefix(ref.Name().String(), r.refPrefix) {
218 refItem := RefItem{
219 Reference: ref,
220 }
221
222 if ref.IsTag() {
223 refItem.Tag, _ = rr.Tag(ref.Name().Short())
224 if refItem.Tag != nil {
225 refItem.Commit, _ = refItem.Tag.Commit()
226 }
227 } else {
228 refItem.Commit, _ = rr.CatFileCommit(ref.ID)
229 }
230 its = append(its, refItem)
231 }
232 }
233 sort.Sort(its)
234 items := make([]selector.IdentifiableItem, len(its))
235 for i, it := range its {
236 items[i] = it
237 }
238 return RefItemsMsg{
239 items: items,
240 prefix: r.refPrefix,
241 }
242}
243
244func (r *Refs) setItems(items []selector.IdentifiableItem) tea.Cmd {
245 return func() tea.Msg {
246 return RefItemsMsg{
247 items: items,
248 prefix: r.refPrefix,
249 }
250 }
251}
252
253func switchRefCmd(ref *git.Reference) tea.Cmd {
254 return func() tea.Msg {
255 return RefMsg(ref)
256 }
257}
258
259// UpdateRefCmd gets the repository's HEAD reference and sends a RefMsg.
260func UpdateRefCmd(repo proto.Repository) tea.Cmd {
261 return func() tea.Msg {
262 r, err := repo.Open()
263 if err != nil {
264 return common.ErrorMsg(err)
265 }
266 bs, _ := r.Branches()
267 if len(bs) == 0 {
268 return EmptyRepoMsg{}
269 }
270 ref, err := r.HEAD()
271 if err != nil {
272 return common.ErrorMsg(err)
273 }
274 return RefMsg(ref)
275 }
276}