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