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}