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