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}