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}