refs.go

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