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 },
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 tea.Batch(
150 r.tabs.Init(),
151 r.statusbar.Init(),
152 )
153}
154
155// Update implements tea.Model.
156func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
157 cmds := make([]tea.Cmd, 0)
158 switch msg := msg.(type) {
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 cfg := r.s.Config()
278 name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name())
279 url := git.RepoURL(cfg.Host, cfg.Port, r.selectedRepo.Repo())
280 // TODO move this into a style.
281 url = lipgloss.NewStyle().
282 MarginLeft(1).
283 Foreground(lipgloss.Color("168")).
284 Width(r.common.Width - lipgloss.Width(name) - 1).
285 Align(lipgloss.Right).
286 Render(url)
287 desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedRepo.Description())
288 style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
289 return style.Render(
290 lipgloss.JoinVertical(lipgloss.Top,
291 lipgloss.JoinHorizontal(lipgloss.Left,
292 name,
293 url,
294 ),
295 desc,
296 ),
297 )
298}
299
300func (r *Repo) updateStatusBarCmd() tea.Msg {
301 var info, value string
302 switch r.activeTab {
303 case readmeTab:
304 info = fmt.Sprintf("☰ %.f%%", r.boxes[readmeTab].(*code.Code).ScrollPercent()*100)
305 default:
306 value = r.boxes[r.activeTab].(statusbar.Model).StatusBarValue()
307 info = r.boxes[r.activeTab].(statusbar.Model).StatusBarInfo()
308 }
309 return statusbar.StatusBarMsg{
310 Key: r.selectedRepo.Name(),
311 Value: value,
312 Info: info,
313 Branch: fmt.Sprintf(" %s", r.ref.Name().Short()),
314 }
315}
316
317func (r *Repo) updateReadmeCmd() tea.Msg {
318 if r.selectedRepo == nil {
319 return common.ErrorCmd(git.ErrMissingRepo)
320 }
321 rm, rp := r.selectedRepo.Readme()
322 return r.boxes[readmeTab].(*code.Code).SetContent(rm, rp)
323}
324
325func (r *Repo) updateRefCmd() tea.Msg {
326 head, err := r.selectedRepo.HEAD()
327 if err != nil {
328 return common.ErrorMsg(err)
329 }
330 return RefMsg(head)
331}
332
333func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
334 cmds := make([]tea.Cmd, 0)
335 for i, b := range r.boxes {
336 m, cmd := b.Update(msg)
337 r.boxes[i] = m.(common.Component)
338 if cmd != nil {
339 cmds = append(cmds, cmd)
340 }
341 }
342 return tea.Batch(cmds...)
343}
344
345func updateStatusBarCmd() tea.Msg {
346 return UpdateStatusBarMsg{}
347}