refs.go

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