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 ref *ggit.Reference
49}
50
51// New returns a new Repo.
52func New(common common.Common, rs git.GitRepoSource) *Repo {
53 sb := statusbar.New(common)
54 tb := tabs.New(common, []string{"Readme", "Files", "Commits", "Branches", "Tags"})
55 readme := code.New(common, "", "")
56 readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
57 log := NewLog(common)
58 r := &Repo{
59 common: common,
60 rs: rs,
61 tabs: tb,
62 statusbar: sb,
63 readme: readme,
64 log: log,
65 }
66 return r
67}
68
69// SetSize implements common.Component.
70func (r *Repo) SetSize(width, height int) {
71 r.common.SetSize(width, height)
72 hm := r.common.Styles.RepoBody.GetVerticalFrameSize() +
73 r.common.Styles.RepoHeader.GetHeight() +
74 r.common.Styles.RepoHeader.GetVerticalFrameSize() +
75 r.common.Styles.StatusBar.GetHeight() +
76 r.common.Styles.Tabs.GetHeight() +
77 r.common.Styles.Tabs.GetVerticalFrameSize()
78 r.tabs.SetSize(width, height-hm)
79 r.statusbar.SetSize(width, height-hm)
80 r.readme.SetSize(width, height-hm)
81 r.log.SetSize(width, height-hm)
82}
83
84// ShortHelp implements help.KeyMap.
85func (r *Repo) ShortHelp() []key.Binding {
86 b := make([]key.Binding, 0)
87 tab := r.common.KeyMap.Section
88 tab.SetHelp("tab", "switch tab")
89 back := r.common.KeyMap.Back
90 back.SetHelp("esc", "repos")
91 b = append(b, back)
92 b = append(b, tab)
93 switch r.activeTab {
94 case readmeTab:
95 b = append(b, r.common.KeyMap.UpDown)
96 case commitsTab:
97 b = append(b, r.log.ShortHelp()...)
98 }
99 return b
100}
101
102// FullHelp implements help.KeyMap.
103func (r *Repo) FullHelp() [][]key.Binding {
104 b := make([][]key.Binding, 0)
105 return b
106}
107
108// Init implements tea.View.
109func (r *Repo) Init() tea.Cmd {
110 return nil
111}
112
113// Update implements tea.Model.
114func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
115 cmds := make([]tea.Cmd, 0)
116 switch msg := msg.(type) {
117 case selector.SelectMsg:
118 switch msg.IdentifiableItem.(type) {
119 case selection.Item:
120 r.selectedItem = msg.IdentifiableItem.(selection.Item)
121 }
122 case RepoMsg:
123 r.activeTab = 0
124 r.selectedRepo = git.GitRepo(msg)
125 r.readme.GotoTop()
126 cmds = append(cmds,
127 r.tabs.Init(),
128 r.updateReadmeCmd,
129 r.updateRefCmd,
130 )
131 // Pass msg to log.
132 l, cmd := r.log.Update(msg)
133 r.log = l.(*Log)
134 if cmd != nil {
135 cmds = append(cmds, cmd)
136 }
137 case RefMsg:
138 r.ref = msg
139 cmds = append(cmds,
140 r.updateStatusBarCmd,
141 r.log.Init(),
142 )
143 // Pass msg to log.
144 l, cmd := r.log.Update(msg)
145 r.log = l.(*Log)
146 if cmd != nil {
147 cmds = append(cmds, cmd)
148 }
149 case tabs.ActiveTabMsg:
150 r.activeTab = tab(msg)
151 if r.selectedRepo != nil {
152 cmds = append(cmds, r.updateStatusBarCmd)
153 }
154 case tea.KeyMsg, tea.MouseMsg:
155 if r.selectedRepo != nil {
156 cmds = append(cmds, r.updateStatusBarCmd)
157 }
158 case LogCountMsg, LogItemsMsg:
159 l, cmd := r.log.Update(msg)
160 r.log = l.(*Log)
161 if cmd != nil {
162 cmds = append(cmds, cmd)
163 }
164 case UpdateStatusBarMsg:
165 cmds = append(cmds, r.updateStatusBarCmd)
166 case tea.WindowSizeMsg:
167 b, cmd := r.readme.Update(msg)
168 r.readme = b.(*code.Code)
169 if cmd != nil {
170 cmds = append(cmds, cmd)
171 }
172 l, cmd := r.log.Update(msg)
173 r.log = l.(*Log)
174 if cmd != nil {
175 cmds = append(cmds, cmd)
176 }
177 }
178 t, cmd := r.tabs.Update(msg)
179 r.tabs = t.(*tabs.Tabs)
180 if cmd != nil {
181 cmds = append(cmds, cmd)
182 }
183 s, cmd := r.statusbar.Update(msg)
184 r.statusbar = s.(*statusbar.StatusBar)
185 if cmd != nil {
186 cmds = append(cmds, cmd)
187 }
188 switch r.activeTab {
189 case readmeTab:
190 b, cmd := r.readme.Update(msg)
191 r.readme = b.(*code.Code)
192 if cmd != nil {
193 cmds = append(cmds, cmd)
194 }
195 case filesTab:
196 case commitsTab:
197 l, cmd := r.log.Update(msg)
198 r.log = l.(*Log)
199 if cmd != nil {
200 cmds = append(cmds, cmd)
201 }
202 case branchesTab:
203 case tagsTab:
204 }
205 return r, tea.Batch(cmds...)
206}
207
208// View implements tea.Model.
209func (r *Repo) View() string {
210 s := r.common.Styles.Repo.Copy().
211 Width(r.common.Width).
212 Height(r.common.Height)
213 repoBodyStyle := r.common.Styles.RepoBody.Copy()
214 hm := repoBodyStyle.GetVerticalFrameSize() +
215 r.common.Styles.RepoHeader.GetHeight() +
216 r.common.Styles.RepoHeader.GetVerticalFrameSize() +
217 r.common.Styles.StatusBar.GetHeight() +
218 r.common.Styles.Tabs.GetHeight() +
219 r.common.Styles.Tabs.GetVerticalFrameSize()
220 mainStyle := repoBodyStyle.
221 Height(r.common.Height - hm)
222 main := mainStyle.Render("")
223 switch r.activeTab {
224 case readmeTab:
225 main = mainStyle.Render(r.readme.View())
226 case filesTab:
227 case commitsTab:
228 main = mainStyle.Render(r.log.View())
229 }
230 view := lipgloss.JoinVertical(lipgloss.Top,
231 r.headerView(),
232 r.tabs.View(),
233 main,
234 r.statusbar.View(),
235 )
236 return s.Render(view)
237}
238
239func (r *Repo) headerView() string {
240 if r.selectedRepo == nil {
241 return ""
242 }
243 name := r.common.Styles.RepoHeaderName.Render(r.selectedItem.Title())
244 // TODO move this into a style.
245 url := lipgloss.NewStyle().MarginLeft(2).Render(r.selectedItem.URL())
246 desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedItem.Description())
247 style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
248 return style.Render(
249 lipgloss.JoinVertical(lipgloss.Top,
250 lipgloss.JoinHorizontal(lipgloss.Left,
251 name,
252 url,
253 ),
254 desc,
255 ),
256 )
257}
258
259func (r *Repo) setRepoCmd(repo string) tea.Cmd {
260 return func() tea.Msg {
261 for _, r := range r.rs.AllRepos() {
262 if r.Name() == repo {
263 return RepoMsg(r)
264 }
265 }
266 return common.ErrorMsg(git.ErrMissingRepo)
267 }
268}
269
270func (r *Repo) updateStatusBarCmd() tea.Msg {
271 info := ""
272 switch r.activeTab {
273 case readmeTab:
274 info = fmt.Sprintf("%.f%%", r.readme.ScrollPercent()*100)
275 case commitsTab:
276 info = r.log.StatusBarInfo()
277 }
278 return statusbar.StatusBarMsg{
279 Key: r.selectedRepo.Name(),
280 Value: "",
281 Info: info,
282 Branch: fmt.Sprintf(" %s", r.ref.Name().Short()),
283 }
284}
285
286func (r *Repo) updateReadmeCmd() tea.Msg {
287 if r.selectedRepo == nil {
288 return common.ErrorCmd(git.ErrMissingRepo)
289 }
290 rm, rp := r.selectedRepo.Readme()
291 return r.readme.SetContent(rm, rp)
292}
293
294func (r *Repo) updateRefCmd() tea.Msg {
295 head, err := r.selectedRepo.HEAD()
296 if err != nil {
297 return common.ErrorMsg(err)
298 }
299 return RefMsg(head)
300}
301
302func updateStatusBarCmd() tea.Msg {
303 return UpdateStatusBarMsg{}
304}