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/statusbar"
14 "github.com/charmbracelet/soft-serve/ui/components/tabs"
15 "github.com/charmbracelet/soft-serve/ui/git"
16 "github.com/charmbracelet/soft-serve/ui/session"
17)
18
19type tab int
20
21const (
22 readmeTab tab = iota
23 filesTab
24 commitsTab
25 branchesTab
26 tagsTab
27)
28
29// UpdateStatusBarMsg updates the status bar.
30type UpdateStatusBarMsg struct{}
31
32// RepoMsg is a message that contains a git.Repository.
33type RepoMsg git.GitRepo
34
35// RefMsg is a message that contains a git.Reference.
36type RefMsg *ggit.Reference
37
38// Repo is a view for a git repository.
39type Repo struct {
40 s session.Session
41 common common.Common
42 rs git.GitRepoSource
43 selectedRepo git.GitRepo
44 activeTab tab
45 tabs *tabs.Tabs
46 statusbar *statusbar.StatusBar
47 boxes []common.Component
48 ref *ggit.Reference
49}
50
51// New returns a new Repo.
52func New(s session.Session, c common.Common) *Repo {
53 sb := statusbar.New(c)
54 // Tabs must match the order of tab constants above.
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 // Make sure the order matches the order of tab constants above.
63 boxes := []common.Component{
64 readme,
65 files,
66 log,
67 branches,
68 tags,
69 }
70 r := &Repo{
71 s: s,
72 common: c,
73 rs: s.Source(),
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[r.activeTab].(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 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 tea.Batch(
148 r.tabs.Init(),
149 r.statusbar.Init(),
150 )
151}
152
153// Update implements tea.Model.
154func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
155 cmds := make([]tea.Cmd, 0)
156 switch msg := msg.(type) {
157 case RepoMsg:
158 r.activeTab = 0
159 r.selectedRepo = git.GitRepo(msg)
160 r.boxes[readmeTab].(*code.Code).GotoTop()
161 cmds = append(cmds,
162 r.tabs.Init(),
163 r.updateReadmeCmd,
164 r.updateRefCmd,
165 r.updateModels(msg),
166 )
167 case RefMsg:
168 r.ref = msg
169 for _, b := range r.boxes {
170 cmds = append(cmds, b.Init())
171 }
172 cmds = append(cmds,
173 r.updateStatusBarCmd,
174 r.updateModels(msg),
175 )
176 case tabs.SelectTabMsg:
177 r.activeTab = tab(msg)
178 t, cmd := r.tabs.Update(msg)
179 r.tabs = t.(*tabs.Tabs)
180 if cmd != nil {
181 cmds = append(cmds, cmd)
182 }
183 case tabs.ActiveTabMsg:
184 r.activeTab = tab(msg)
185 if r.selectedRepo != nil {
186 cmds = append(cmds, r.updateStatusBarCmd)
187 }
188 case tea.KeyMsg, tea.MouseMsg:
189 t, cmd := r.tabs.Update(msg)
190 r.tabs = t.(*tabs.Tabs)
191 if cmd != nil {
192 cmds = append(cmds, cmd)
193 }
194 if r.selectedRepo != nil {
195 cmds = append(cmds, r.updateStatusBarCmd)
196 }
197 case FileItemsMsg:
198 f, cmd := r.boxes[filesTab].Update(msg)
199 r.boxes[filesTab] = f.(*Files)
200 if cmd != nil {
201 cmds = append(cmds, cmd)
202 }
203 case LogCountMsg, LogItemsMsg:
204 l, cmd := r.boxes[commitsTab].Update(msg)
205 r.boxes[commitsTab] = l.(*Log)
206 if cmd != nil {
207 cmds = append(cmds, cmd)
208 }
209 case RefItemsMsg:
210 switch msg.prefix {
211 case ggit.RefsHeads:
212 b, cmd := r.boxes[branchesTab].Update(msg)
213 r.boxes[branchesTab] = b.(*Refs)
214 if cmd != nil {
215 cmds = append(cmds, cmd)
216 }
217 case ggit.RefsTags:
218 t, cmd := r.boxes[tagsTab].Update(msg)
219 r.boxes[tagsTab] = t.(*Refs)
220 if cmd != nil {
221 cmds = append(cmds, cmd)
222 }
223 }
224 case UpdateStatusBarMsg:
225 cmds = append(cmds, r.updateStatusBarCmd)
226 case tea.WindowSizeMsg:
227 b, cmd := r.boxes[readmeTab].Update(msg)
228 r.boxes[readmeTab] = b.(*code.Code)
229 if cmd != nil {
230 cmds = append(cmds, cmd)
231 }
232 cmds = append(cmds, r.updateModels(msg))
233 }
234 s, cmd := r.statusbar.Update(msg)
235 r.statusbar = s.(*statusbar.StatusBar)
236 if cmd != nil {
237 cmds = append(cmds, cmd)
238 }
239 m, cmd := r.boxes[r.activeTab].Update(msg)
240 r.boxes[r.activeTab] = m.(common.Component)
241 if cmd != nil {
242 cmds = append(cmds, cmd)
243 }
244 return r, tea.Batch(cmds...)
245}
246
247// View implements tea.Model.
248func (r *Repo) View() string {
249 s := r.common.Styles.Repo.Copy().
250 Width(r.common.Width).
251 Height(r.common.Height)
252 repoBodyStyle := r.common.Styles.RepoBody.Copy()
253 hm := repoBodyStyle.GetVerticalFrameSize() +
254 r.common.Styles.RepoHeader.GetHeight() +
255 r.common.Styles.RepoHeader.GetVerticalFrameSize() +
256 r.common.Styles.StatusBar.GetHeight() +
257 r.common.Styles.Tabs.GetHeight() +
258 r.common.Styles.Tabs.GetVerticalFrameSize()
259 mainStyle := repoBodyStyle.
260 Height(r.common.Height - hm)
261 main := r.boxes[r.activeTab].View()
262 view := lipgloss.JoinVertical(lipgloss.Top,
263 r.headerView(),
264 r.tabs.View(),
265 mainStyle.Render(main),
266 r.statusbar.View(),
267 )
268 return s.Render(view)
269}
270
271func (r *Repo) headerView() string {
272 if r.selectedRepo == nil {
273 return ""
274 }
275 cfg := r.s.Config()
276 name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name())
277 url := git.RepoURL(cfg.Host, cfg.Port, r.selectedRepo.Repo())
278 // TODO move this into a style.
279 url = lipgloss.NewStyle().
280 MarginLeft(1).
281 Foreground(lipgloss.Color("168")).
282 Width(r.common.Width - lipgloss.Width(name) - 1).
283 Align(lipgloss.Right).
284 Render(url)
285 desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedRepo.Description())
286 style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
287 return style.Render(
288 lipgloss.JoinVertical(lipgloss.Top,
289 lipgloss.JoinHorizontal(lipgloss.Left,
290 name,
291 url,
292 ),
293 desc,
294 ),
295 )
296}
297
298func (r *Repo) updateStatusBarCmd() tea.Msg {
299 var info, value string
300 switch r.activeTab {
301 case readmeTab:
302 info = fmt.Sprintf("☰ %.f%%", r.boxes[readmeTab].(*code.Code).ScrollPercent()*100)
303 default:
304 value = r.boxes[r.activeTab].(statusbar.Model).StatusBarValue()
305 info = r.boxes[r.activeTab].(statusbar.Model).StatusBarInfo()
306 }
307 return statusbar.StatusBarMsg{
308 Key: r.selectedRepo.Name(),
309 Value: value,
310 Info: info,
311 Branch: fmt.Sprintf(" %s", r.ref.Name().Short()),
312 }
313}
314
315func (r *Repo) updateReadmeCmd() tea.Msg {
316 if r.selectedRepo == nil {
317 return common.ErrorCmd(git.ErrMissingRepo)
318 }
319 rm, rp := r.selectedRepo.Readme()
320 return r.boxes[readmeTab].(*code.Code).SetContent(rm, rp)
321}
322
323func (r *Repo) updateRefCmd() tea.Msg {
324 head, err := r.selectedRepo.HEAD()
325 if err != nil {
326 return common.ErrorMsg(err)
327 }
328 return RefMsg(head)
329}
330
331func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
332 cmds := make([]tea.Cmd, 0)
333 for i, b := range r.boxes {
334 m, cmd := b.Update(msg)
335 r.boxes[i] = m.(common.Component)
336 if cmd != nil {
337 cmds = append(cmds, cmd)
338 }
339 }
340 return tea.Batch(cmds...)
341}
342
343func updateStatusBarCmd() tea.Msg {
344 return UpdateStatusBarMsg{}
345}