refs.go

  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}