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