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