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