refs.go

  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}