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