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