1package repo
2
3import (
4 "fmt"
5
6 "github.com/charmbracelet/bubbles/help"
7 "github.com/charmbracelet/bubbles/key"
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 ggit "github.com/charmbracelet/soft-serve/git"
11 "github.com/charmbracelet/soft-serve/ui/common"
12 "github.com/charmbracelet/soft-serve/ui/components/code"
13 "github.com/charmbracelet/soft-serve/ui/components/selector"
14 "github.com/charmbracelet/soft-serve/ui/components/statusbar"
15 "github.com/charmbracelet/soft-serve/ui/components/tabs"
16 "github.com/charmbracelet/soft-serve/ui/git"
17 "github.com/charmbracelet/soft-serve/ui/pages/selection"
18)
19
20type tab int
21
22const (
23 readmeTab tab = iota
24 filesTab
25 commitsTab
26 branchesTab
27 tagsTab
28)
29
30// UpdateStatusBarMsg updates the status bar.
31type UpdateStatusBarMsg struct{}
32
33// RepoMsg is a message that contains a git.Repository.
34type RepoMsg git.GitRepo
35
36// RefMsg is a message that contains a git.Reference.
37type RefMsg *ggit.Reference
38
39// Repo is a view for a git repository.
40type Repo struct {
41 common common.Common
42 rs git.GitRepoSource
43 selectedRepo git.GitRepo
44 selectedItem selection.Item
45 activeTab tab
46 tabs *tabs.Tabs
47 statusbar *statusbar.StatusBar
48 boxes []common.Component
49 ref *ggit.Reference
50}
51
52// New returns a new Repo.
53func New(c common.Common, rs git.GitRepoSource) *Repo {
54 sb := statusbar.New(c)
55 // Tabs must match the order of tab constants above.
56 tb := tabs.New(c, []string{"Readme", "Files", "Commits", "Branches", "Tags"})
57 readme := code.New(c, "", "")
58 readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
59 log := NewLog(c)
60 files := NewFiles(c)
61 branches := NewRefs(c, ggit.RefsHeads)
62 tags := NewRefs(c, ggit.RefsTags)
63 // Make sure the order matches the order of tab constants above.
64 boxes := []common.Component{
65 readme,
66 files,
67 log,
68 branches,
69 tags,
70 }
71 r := &Repo{
72 common: c,
73 rs: rs,
74 tabs: tb,
75 statusbar: sb,
76 boxes: boxes,
77 }
78 return r
79}
80
81// SetSize implements common.Component.
82func (r *Repo) SetSize(width, height int) {
83 r.common.SetSize(width, height)
84 hm := r.common.Styles.RepoBody.GetVerticalFrameSize() +
85 r.common.Styles.RepoHeader.GetHeight() +
86 r.common.Styles.RepoHeader.GetVerticalFrameSize() +
87 r.common.Styles.StatusBar.GetHeight() +
88 r.common.Styles.Tabs.GetHeight() +
89 r.common.Styles.Tabs.GetVerticalFrameSize()
90 r.tabs.SetSize(width, height-hm)
91 r.statusbar.SetSize(width, height-hm)
92 for _, b := range r.boxes {
93 b.SetSize(width, height-hm)
94 }
95}
96
97func (r *Repo) commonHelp() []key.Binding {
98 b := make([]key.Binding, 0)
99 back := r.common.KeyMap.Back
100 back.SetHelp("esc", "back to menu")
101 tab := r.common.KeyMap.Section
102 tab.SetHelp("tab", "switch tab")
103 b = append(b, back)
104 b = append(b, tab)
105 return b
106}
107
108// ShortHelp implements help.KeyMap.
109func (r *Repo) ShortHelp() []key.Binding {
110 b := r.commonHelp()
111 switch r.activeTab {
112 case readmeTab:
113 b = append(b, r.common.KeyMap.UpDown)
114 default:
115 b = append(b, r.boxes[commitsTab].(help.KeyMap).ShortHelp()...)
116 }
117 return b
118}
119
120// FullHelp implements help.KeyMap.
121func (r *Repo) FullHelp() [][]key.Binding {
122 b := make([][]key.Binding, 0)
123 b = append(b, r.commonHelp())
124 switch r.activeTab {
125 case readmeTab:
126 k := r.boxes[readmeTab].(*code.Code).KeyMap
127 b = append(b, [][]key.Binding{
128 {
129 k.PageDown,
130 k.PageUp,
131 },
132 {
133 k.HalfPageDown,
134 k.HalfPageUp,
135 },
136 {
137 k.Down,
138 k.Up,
139 },
140 }...)
141 default:
142 b = append(b, r.boxes[r.activeTab].(help.KeyMap).FullHelp()...)
143 }
144 return b
145}
146
147// Init implements tea.View.
148func (r *Repo) Init() tea.Cmd {
149 return nil
150}
151
152// Update implements tea.Model.
153func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
154 cmds := make([]tea.Cmd, 0)
155 switch msg := msg.(type) {
156 case selector.SelectMsg:
157 switch msg.IdentifiableItem.(type) {
158 case selection.Item:
159 r.selectedItem = msg.IdentifiableItem.(selection.Item)
160 }
161 case RepoMsg:
162 r.activeTab = 0
163 r.selectedRepo = git.GitRepo(msg)
164 r.boxes[readmeTab].(*code.Code).GotoTop()
165 cmds = append(cmds,
166 r.tabs.Init(),
167 r.updateReadmeCmd,
168 r.updateRefCmd,
169 r.updateModels(msg),
170 )
171 case RefMsg:
172 r.ref = msg
173 for _, b := range r.boxes {
174 cmds = append(cmds, b.Init())
175 }
176 cmds = append(cmds,
177 r.updateStatusBarCmd,
178 r.updateModels(msg),
179 )
180 case tabs.SelectTabMsg:
181 r.activeTab = tab(msg)
182 t, cmd := r.tabs.Update(msg)
183 r.tabs = t.(*tabs.Tabs)
184 if cmd != nil {
185 cmds = append(cmds, cmd)
186 }
187 case tabs.ActiveTabMsg:
188 r.activeTab = tab(msg)
189 if r.selectedRepo != nil {
190 cmds = append(cmds, r.updateStatusBarCmd)
191 }
192 case tea.KeyMsg, tea.MouseMsg:
193 t, cmd := r.tabs.Update(msg)
194 r.tabs = t.(*tabs.Tabs)
195 if cmd != nil {
196 cmds = append(cmds, cmd)
197 }
198 if r.selectedRepo != nil {
199 cmds = append(cmds, r.updateStatusBarCmd)
200 }
201 case FileItemsMsg:
202 f, cmd := r.boxes[filesTab].Update(msg)
203 r.boxes[filesTab] = f.(*Files)
204 if cmd != nil {
205 cmds = append(cmds, cmd)
206 }
207 case LogCountMsg, LogItemsMsg:
208 l, cmd := r.boxes[commitsTab].Update(msg)
209 r.boxes[commitsTab] = l.(*Log)
210 if cmd != nil {
211 cmds = append(cmds, cmd)
212 }
213 case RefItemsMsg:
214 switch msg.prefix {
215 case ggit.RefsHeads:
216 b, cmd := r.boxes[branchesTab].Update(msg)
217 r.boxes[branchesTab] = b.(*Refs)
218 if cmd != nil {
219 cmds = append(cmds, cmd)
220 }
221 case ggit.RefsTags:
222 t, cmd := r.boxes[tagsTab].Update(msg)
223 r.boxes[tagsTab] = t.(*Refs)
224 if cmd != nil {
225 cmds = append(cmds, cmd)
226 }
227 }
228 case UpdateStatusBarMsg:
229 cmds = append(cmds, r.updateStatusBarCmd)
230 case tea.WindowSizeMsg:
231 b, cmd := r.boxes[readmeTab].Update(msg)
232 r.boxes[readmeTab] = b.(*code.Code)
233 if cmd != nil {
234 cmds = append(cmds, cmd)
235 }
236 cmds = append(cmds, r.updateModels(msg))
237 }
238 s, cmd := r.statusbar.Update(msg)
239 r.statusbar = s.(*statusbar.StatusBar)
240 if cmd != nil {
241 cmds = append(cmds, cmd)
242 }
243 m, cmd := r.boxes[r.activeTab].Update(msg)
244 r.boxes[r.activeTab] = m.(common.Component)
245 if cmd != nil {
246 cmds = append(cmds, cmd)
247 }
248 return r, tea.Batch(cmds...)
249}
250
251// View implements tea.Model.
252func (r *Repo) View() string {
253 s := r.common.Styles.Repo.Copy().
254 Width(r.common.Width).
255 Height(r.common.Height)
256 repoBodyStyle := r.common.Styles.RepoBody.Copy()
257 hm := repoBodyStyle.GetVerticalFrameSize() +
258 r.common.Styles.RepoHeader.GetHeight() +
259 r.common.Styles.RepoHeader.GetVerticalFrameSize() +
260 r.common.Styles.StatusBar.GetHeight() +
261 r.common.Styles.Tabs.GetHeight() +
262 r.common.Styles.Tabs.GetVerticalFrameSize()
263 mainStyle := repoBodyStyle.
264 Height(r.common.Height - hm)
265 main := r.boxes[r.activeTab].View()
266 view := lipgloss.JoinVertical(lipgloss.Top,
267 r.headerView(),
268 r.tabs.View(),
269 mainStyle.Render(main),
270 r.statusbar.View(),
271 )
272 return s.Render(view)
273}
274
275func (r *Repo) headerView() string {
276 if r.selectedRepo == nil {
277 return ""
278 }
279 name := r.common.Styles.RepoHeaderName.Render(r.selectedItem.Title())
280 // TODO move this into a style.
281 url := lipgloss.NewStyle().
282 MarginLeft(1).
283 Width(r.common.Width - lipgloss.Width(name) - 1).
284 Align(lipgloss.Right).
285 Render(r.selectedItem.URL())
286 desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedItem.Description())
287 style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
288 return style.Render(
289 lipgloss.JoinVertical(lipgloss.Top,
290 lipgloss.JoinHorizontal(lipgloss.Left,
291 name,
292 url,
293 ),
294 desc,
295 ),
296 )
297}
298
299func (r *Repo) setRepoCmd(repo string) tea.Cmd {
300 return func() tea.Msg {
301 for _, r := range r.rs.AllRepos() {
302 if r.Name() == repo {
303 return RepoMsg(r)
304 }
305 }
306 return common.ErrorMsg(git.ErrMissingRepo)
307 }
308}
309
310func (r *Repo) updateStatusBarCmd() tea.Msg {
311 var info, value string
312 switch r.activeTab {
313 case readmeTab:
314 info = fmt.Sprintf("%.f%%", r.boxes[readmeTab].(*code.Code).ScrollPercent()*100)
315 default:
316 value = r.boxes[r.activeTab].(statusbar.Model).StatusBarValue()
317 info = r.boxes[r.activeTab].(statusbar.Model).StatusBarInfo()
318 }
319 return statusbar.StatusBarMsg{
320 Key: r.selectedRepo.Name(),
321 Value: value,
322 Info: info,
323 Branch: fmt.Sprintf(" %s", r.ref.Name().Short()),
324 }
325}
326
327func (r *Repo) updateReadmeCmd() tea.Msg {
328 if r.selectedRepo == nil {
329 return common.ErrorCmd(git.ErrMissingRepo)
330 }
331 rm, rp := r.selectedRepo.Readme()
332 return r.boxes[readmeTab].(*code.Code).SetContent(rm, rp)
333}
334
335func (r *Repo) updateRefCmd() tea.Msg {
336 head, err := r.selectedRepo.HEAD()
337 if err != nil {
338 return common.ErrorMsg(err)
339 }
340 return RefMsg(head)
341}
342
343func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
344 cmds := make([]tea.Cmd, 0)
345 for i, b := range r.boxes {
346 m, cmd := b.Update(msg)
347 r.boxes[i] = m.(common.Component)
348 if cmd != nil {
349 cmds = append(cmds, cmd)
350 }
351 }
352 return tea.Batch(cmds...)
353}
354
355func updateStatusBarCmd() tea.Msg {
356 return UpdateStatusBarMsg{}
357}