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