1package repo23import (4 "fmt"5 "sort"6 "strings"78 "charm.land/bubbles/v2/key"9 "charm.land/bubbles/v2/spinner"10 tea "charm.land/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)1617// RefMsg is a message that contains a git.Reference.18type RefMsg *git.Reference1920// RefItemsMsg is a message that contains a list of RefItem.21type RefItemsMsg struct {22 prefix string23 items []selector.IdentifiableItem24}2526// Refs is a component that displays a list of references.27type Refs struct {28 common common.Common29 selector *selector.Selector30 repo proto.Repository31 ref *git.Reference32 activeRef *git.Reference33 refPrefix string34 spinner spinner.Model35 isLoading bool36}3738// 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 = s54 sp := spinner.New(spinner.WithSpinner(spinner.Dot),55 spinner.WithStyle(common.Styles.Spinner))56 r.spinner = sp57 return r58}5960// Path implements common.TabComponent.61func (r *Refs) Path() string {62 return ""63}6465// 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}7576// SetSize implements common.Component.77func (r *Refs) SetSize(width, height int) {78 r.common.SetSize(width, height)79 r.selector.SetSize(width, height)80}8182// ShortHelp implements help.KeyMap.83func (r *Refs) ShortHelp() []key.Binding {84 copyKey := r.common.KeyMap.Copy85 copyKey.SetHelp("c", "copy ref")86 k := r.selector.KeyMap87 return []key.Binding{88 r.common.KeyMap.SelectItem,89 k.CursorUp,90 k.CursorDown,91 copyKey,92 }93}9495// FullHelp implements help.KeyMap.96func (r *Refs) FullHelp() [][]key.Binding {97 copyKey := r.common.KeyMap.Copy98 copyKey.SetHelp("c", "copy ref")99 k := r.selector.KeyMap100 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}115116// Init implements tea.Model.117func (r *Refs) Init() tea.Cmd {118 r.isLoading = true119 return tea.Batch(r.spinner.Tick, r.updateItemsCmd)120}121122// Update implements tea.Model.123func (r *Refs) Update(msg tea.Msg) (common.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 = msg129 case RefMsg:130 r.ref = msg131 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).Reference140 }141 r.isLoading = false142 }143 case selector.ActiveMsg:144 switch sel := msg.IdentifiableItem.(type) {145 case RefItem:146 r.activeRef = sel.Reference147 }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 = nil163 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 = s171 }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}180181// 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}188189// SpinnerID implements common.TabComponent.190func (r *Refs) SpinnerID() int {191 return r.spinner.ID()192}193194// StatusBarValue implements statusbar.StatusBar.195func (r *Refs) StatusBarValue() string {196 if r.activeRef == nil {197 return ""198 }199 return r.activeRef.Name().String()200}201202// 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}210211func (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 }227228 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] = it243 }244 return RefItemsMsg{245 items: items,246 prefix: r.refPrefix,247 }248}249250func (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}258259func switchRefCmd(ref *git.Reference) tea.Cmd {260 return func() tea.Msg {261 return RefMsg(ref)262 }263}264265// 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}