1package repo
2
3import (
4 "fmt"
5
6 "github.com/charmbracelet/bubbles/help"
7 "github.com/charmbracelet/bubbles/key"
8 "github.com/charmbracelet/bubbles/spinner"
9 tea "github.com/charmbracelet/bubbletea"
10 "github.com/charmbracelet/lipgloss"
11 "github.com/charmbracelet/soft-serve/config"
12 ggit "github.com/charmbracelet/soft-serve/git"
13 "github.com/charmbracelet/soft-serve/ui/common"
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)
18
19type state int
20
21const (
22 loadingState state = iota
23 loadedState
24)
25
26type tab int
27
28const (
29 readmeTab tab = iota
30 filesTab
31 commitsTab
32 branchesTab
33 tagsTab
34 lastTab
35)
36
37func (t tab) String() string {
38 return []string{
39 "Readme",
40 "Files",
41 "Commits",
42 "Branches",
43 "Tags",
44 }[t]
45}
46
47// UpdateStatusBarMsg updates the status bar.
48type UpdateStatusBarMsg struct{}
49
50// RepoMsg is a message that contains a git.Repository.
51type RepoMsg git.GitRepo
52
53// RefMsg is a message that contains a git.Reference.
54type RefMsg *ggit.Reference
55
56// Repo is a view for a git repository.
57type Repo struct {
58 common common.Common
59 cfg *config.Config
60 selectedRepo git.GitRepo
61 activeTab tab
62 tabs *tabs.Tabs
63 statusbar *statusbar.StatusBar
64 boxes []common.Component
65 ref *ggit.Reference
66}
67
68// New returns a new Repo.
69func New(cfg *config.Config, c common.Common) *Repo {
70 sb := statusbar.New(c)
71 ts := make([]string, lastTab)
72 // Tabs must match the order of tab constants above.
73 for i, t := range []tab{readmeTab, filesTab, commitsTab, branchesTab, tagsTab} {
74 ts[i] = t.String()
75 }
76 tb := tabs.New(c, ts)
77 readme := NewReadme(c)
78 log := NewLog(c)
79 files := NewFiles(c)
80 branches := NewRefs(c, ggit.RefsHeads)
81 tags := NewRefs(c, ggit.RefsTags)
82 // Make sure the order matches the order of tab constants above.
83 boxes := []common.Component{
84 readme,
85 files,
86 log,
87 branches,
88 tags,
89 }
90 r := &Repo{
91 cfg: cfg,
92 common: c,
93 tabs: tb,
94 statusbar: sb,
95 boxes: boxes,
96 }
97 return r
98}
99
100// SetSize implements common.Component.
101func (r *Repo) SetSize(width, height int) {
102 r.common.SetSize(width, height)
103 hm := r.common.Styles.RepoBody.GetVerticalFrameSize() +
104 r.common.Styles.RepoHeader.GetHeight() +
105 r.common.Styles.RepoHeader.GetVerticalFrameSize() +
106 r.common.Styles.StatusBar.GetHeight() +
107 r.common.Styles.Tabs.GetHeight() +
108 r.common.Styles.Tabs.GetVerticalFrameSize()
109 r.tabs.SetSize(width, height-hm)
110 r.statusbar.SetSize(width, height-hm)
111 for _, b := range r.boxes {
112 b.SetSize(width, height-hm)
113 }
114}
115
116func (r *Repo) commonHelp() []key.Binding {
117 b := make([]key.Binding, 0)
118 back := r.common.KeyMap.Back
119 back.SetHelp("esc", "back to menu")
120 tab := r.common.KeyMap.Section
121 tab.SetHelp("tab", "switch tab")
122 b = append(b, back)
123 b = append(b, tab)
124 return b
125}
126
127// ShortHelp implements help.KeyMap.
128func (r *Repo) ShortHelp() []key.Binding {
129 b := r.commonHelp()
130 b = append(b, r.boxes[r.activeTab].(help.KeyMap).ShortHelp()...)
131 return b
132}
133
134// FullHelp implements help.KeyMap.
135func (r *Repo) FullHelp() [][]key.Binding {
136 b := make([][]key.Binding, 0)
137 b = append(b, r.commonHelp())
138 b = append(b, r.boxes[r.activeTab].(help.KeyMap).FullHelp()...)
139 return b
140}
141
142// Init implements tea.View.
143func (r *Repo) Init() tea.Cmd {
144 return tea.Batch(
145 r.tabs.Init(),
146 r.statusbar.Init(),
147 )
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 RepoMsg:
155 r.activeTab = 0
156 r.selectedRepo = git.GitRepo(msg)
157 cmds = append(cmds,
158 r.tabs.Init(),
159 r.updateRefCmd,
160 r.updateModels(msg),
161 )
162 case RefMsg:
163 r.ref = msg
164 for _, b := range r.boxes {
165 cmds = append(cmds, b.Init())
166 }
167 cmds = append(cmds,
168 r.updateStatusBarCmd,
169 r.updateModels(msg),
170 )
171 case tabs.SelectTabMsg:
172 r.activeTab = tab(msg)
173 t, cmd := r.tabs.Update(msg)
174 r.tabs = t.(*tabs.Tabs)
175 if cmd != nil {
176 cmds = append(cmds, cmd)
177 }
178 case tabs.ActiveTabMsg:
179 r.activeTab = tab(msg)
180 if r.selectedRepo != nil {
181 cmds = append(cmds,
182 r.updateStatusBarCmd,
183 )
184 }
185 case tea.KeyMsg, tea.MouseMsg:
186 t, cmd := r.tabs.Update(msg)
187 r.tabs = t.(*tabs.Tabs)
188 if cmd != nil {
189 cmds = append(cmds, cmd)
190 }
191 if r.selectedRepo != nil {
192 cmds = append(cmds, r.updateStatusBarCmd)
193 }
194 case ReadmeMsg:
195 case FileItemsMsg:
196 f, cmd := r.boxes[filesTab].Update(msg)
197 r.boxes[filesTab] = f.(*Files)
198 if cmd != nil {
199 cmds = append(cmds, cmd)
200 }
201 // The Log bubble is the only bubble that uses a spinner, so this is fine
202 // for now. We need to pass the TickMsg to the Log bubble when the Log is
203 // loading but not the current selected tab so that the spinner works.
204 case LogCountMsg, LogItemsMsg, spinner.TickMsg:
205 l, cmd := r.boxes[commitsTab].Update(msg)
206 r.boxes[commitsTab] = l.(*Log)
207 if cmd != nil {
208 cmds = append(cmds, cmd)
209 }
210 case RefItemsMsg:
211 switch msg.prefix {
212 case ggit.RefsHeads:
213 b, cmd := r.boxes[branchesTab].Update(msg)
214 r.boxes[branchesTab] = b.(*Refs)
215 if cmd != nil {
216 cmds = append(cmds, cmd)
217 }
218 case ggit.RefsTags:
219 t, cmd := r.boxes[tagsTab].Update(msg)
220 r.boxes[tagsTab] = t.(*Refs)
221 if cmd != nil {
222 cmds = append(cmds, cmd)
223 }
224 }
225 case UpdateStatusBarMsg:
226 cmds = append(cmds, r.updateStatusBarCmd)
227 case tea.WindowSizeMsg:
228 cmds = append(cmds, r.updateModels(msg))
229 }
230 s, cmd := r.statusbar.Update(msg)
231 r.statusbar = s.(*statusbar.StatusBar)
232 if cmd != nil {
233 cmds = append(cmds, cmd)
234 }
235 m, cmd := r.boxes[r.activeTab].Update(msg)
236 r.boxes[r.activeTab] = m.(common.Component)
237 if cmd != nil {
238 cmds = append(cmds, cmd)
239 }
240 return r, tea.Batch(cmds...)
241}
242
243// View implements tea.Model.
244func (r *Repo) View() string {
245 s := r.common.Styles.Repo.Copy().
246 Width(r.common.Width).
247 Height(r.common.Height)
248 repoBodyStyle := r.common.Styles.RepoBody.Copy()
249 hm := repoBodyStyle.GetVerticalFrameSize() +
250 r.common.Styles.RepoHeader.GetHeight() +
251 r.common.Styles.RepoHeader.GetVerticalFrameSize() +
252 r.common.Styles.StatusBar.GetHeight() +
253 r.common.Styles.Tabs.GetHeight() +
254 r.common.Styles.Tabs.GetVerticalFrameSize()
255 mainStyle := repoBodyStyle.
256 Height(r.common.Height - hm)
257 main := r.boxes[r.activeTab].View()
258 view := lipgloss.JoinVertical(lipgloss.Top,
259 r.headerView(),
260 r.tabs.View(),
261 mainStyle.Render(main),
262 r.statusbar.View(),
263 )
264 return s.Render(view)
265}
266
267func (r *Repo) headerView() string {
268 if r.selectedRepo == nil {
269 return ""
270 }
271 cfg := r.cfg
272 truncate := lipgloss.NewStyle().MaxWidth(r.common.Width)
273 name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name())
274 desc := r.selectedRepo.Description()
275 if desc == "" {
276 desc = name
277 name = ""
278 } else {
279 desc = r.common.Styles.RepoHeaderDesc.Render(desc)
280 }
281 // TODO move this into a style.
282 urlStyle := lipgloss.NewStyle().
283 MarginLeft(1).
284 Foreground(lipgloss.Color("168")).
285 Width(r.common.Width - lipgloss.Width(desc) - 1).
286 Align(lipgloss.Right)
287 url := git.RepoURL(cfg.Host, cfg.Port, r.selectedRepo.Repo())
288 url = common.TruncateString(url, r.common.Width-lipgloss.Width(desc)-1)
289 url = urlStyle.Render(url)
290 style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
291 return style.Render(
292 lipgloss.JoinVertical(lipgloss.Top,
293 truncate.Render(name),
294 truncate.Render(lipgloss.JoinHorizontal(lipgloss.Left,
295 desc,
296 url,
297 )),
298 ),
299 )
300}
301
302func (r *Repo) updateStatusBarCmd() tea.Msg {
303 if r.selectedRepo == nil {
304 return nil
305 }
306 value := r.boxes[r.activeTab].(statusbar.Model).StatusBarValue()
307 info := r.boxes[r.activeTab].(statusbar.Model).StatusBarInfo()
308 ref := ""
309 if r.ref != nil {
310 ref = r.ref.Name().Short()
311 }
312 return statusbar.StatusBarMsg{
313 Key: r.selectedRepo.Repo(),
314 Value: value,
315 Info: info,
316 Branch: fmt.Sprintf(" %s", ref),
317 }
318}
319
320func (r *Repo) updateRefCmd() tea.Msg {
321 if r.selectedRepo == nil {
322 return nil
323 }
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}