repo.go

  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	// Tabs must match the order of tab constants above.
 56	tb := tabs.New(c, []string{"Readme", "Files", "Commits", "Branches", "Tags"})
 57	readme := code.New(c, "", "")
 58	readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.")
 59	log := NewLog(c)
 60	files := NewFiles(c)
 61	branches := NewRefs(c, ggit.RefsHeads)
 62	tags := NewRefs(c, ggit.RefsTags)
 63	// Make sure the order matches the order of tab constants above.
 64	boxes := []common.Component{
 65		readme,
 66		files,
 67		log,
 68		branches,
 69		tags,
 70	}
 71	r := &Repo{
 72		common:    c,
 73		rs:        rs,
 74		tabs:      tb,
 75		statusbar: sb,
 76		boxes:     boxes,
 77	}
 78	return r
 79}
 80
 81// SetSize implements common.Component.
 82func (r *Repo) SetSize(width, height int) {
 83	r.common.SetSize(width, height)
 84	hm := r.common.Styles.RepoBody.GetVerticalFrameSize() +
 85		r.common.Styles.RepoHeader.GetHeight() +
 86		r.common.Styles.RepoHeader.GetVerticalFrameSize() +
 87		r.common.Styles.StatusBar.GetHeight() +
 88		r.common.Styles.Tabs.GetHeight() +
 89		r.common.Styles.Tabs.GetVerticalFrameSize()
 90	r.tabs.SetSize(width, height-hm)
 91	r.statusbar.SetSize(width, height-hm)
 92	for _, b := range r.boxes {
 93		b.SetSize(width, height-hm)
 94	}
 95}
 96
 97func (r *Repo) commonHelp() []key.Binding {
 98	b := make([]key.Binding, 0)
 99	back := r.common.KeyMap.Back
100	back.SetHelp("esc", "back to menu")
101	tab := r.common.KeyMap.Section
102	tab.SetHelp("tab", "switch tab")
103	b = append(b, back)
104	b = append(b, tab)
105	return b
106}
107
108// ShortHelp implements help.KeyMap.
109func (r *Repo) ShortHelp() []key.Binding {
110	b := r.commonHelp()
111	switch r.activeTab {
112	case readmeTab:
113		b = append(b, r.common.KeyMap.UpDown)
114	default:
115		b = append(b, r.boxes[commitsTab].(help.KeyMap).ShortHelp()...)
116	}
117	return b
118}
119
120// FullHelp implements help.KeyMap.
121func (r *Repo) FullHelp() [][]key.Binding {
122	b := make([][]key.Binding, 0)
123	b = append(b, r.commonHelp())
124	switch r.activeTab {
125	case readmeTab:
126		k := r.boxes[readmeTab].(*code.Code).KeyMap
127		b = append(b, [][]key.Binding{
128			{
129				k.PageDown,
130				k.PageUp,
131			},
132			{
133				k.HalfPageDown,
134				k.HalfPageUp,
135			},
136			{
137				k.Down,
138				k.Up,
139			},
140		}...)
141	default:
142		b = append(b, r.boxes[r.activeTab].(help.KeyMap).FullHelp()...)
143	}
144	return b
145}
146
147// Init implements tea.View.
148func (r *Repo) Init() tea.Cmd {
149	cmds := make([]tea.Cmd, 0)
150	cmds = append(cmds,
151		r.tabs.Init(),
152		r.statusbar.Init(),
153	)
154	return tea.Batch(cmds...)
155}
156
157// Update implements tea.Model.
158func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
159	cmds := make([]tea.Cmd, 0)
160	switch msg := msg.(type) {
161	case selector.SelectMsg:
162		switch msg.IdentifiableItem.(type) {
163		case selection.Item:
164			r.selectedItem = msg.IdentifiableItem.(selection.Item)
165		}
166	case RepoMsg:
167		r.activeTab = 0
168		r.selectedRepo = git.GitRepo(msg)
169		r.boxes[readmeTab].(*code.Code).GotoTop()
170		cmds = append(cmds,
171			r.tabs.Init(),
172			r.updateReadmeCmd,
173			r.updateRefCmd,
174			r.updateModels(msg),
175		)
176	case RefMsg:
177		r.ref = msg
178		for _, b := range r.boxes {
179			cmds = append(cmds, b.Init())
180		}
181		cmds = append(cmds,
182			r.updateStatusBarCmd,
183			r.updateModels(msg),
184		)
185	case tabs.SelectTabMsg:
186		r.activeTab = tab(msg)
187		t, cmd := r.tabs.Update(msg)
188		r.tabs = t.(*tabs.Tabs)
189		if cmd != nil {
190			cmds = append(cmds, cmd)
191		}
192	case tabs.ActiveTabMsg:
193		r.activeTab = tab(msg)
194		if r.selectedRepo != nil {
195			cmds = append(cmds, r.updateStatusBarCmd)
196		}
197	case tea.KeyMsg, tea.MouseMsg:
198		t, cmd := r.tabs.Update(msg)
199		r.tabs = t.(*tabs.Tabs)
200		if cmd != nil {
201			cmds = append(cmds, cmd)
202		}
203		if r.selectedRepo != nil {
204			cmds = append(cmds, r.updateStatusBarCmd)
205		}
206	case FileItemsMsg:
207		f, cmd := r.boxes[filesTab].Update(msg)
208		r.boxes[filesTab] = f.(*Files)
209		if cmd != nil {
210			cmds = append(cmds, cmd)
211		}
212	case LogCountMsg, LogItemsMsg:
213		l, cmd := r.boxes[commitsTab].Update(msg)
214		r.boxes[commitsTab] = l.(*Log)
215		if cmd != nil {
216			cmds = append(cmds, cmd)
217		}
218	case RefItemsMsg:
219		switch msg.prefix {
220		case ggit.RefsHeads:
221			b, cmd := r.boxes[branchesTab].Update(msg)
222			r.boxes[branchesTab] = b.(*Refs)
223			if cmd != nil {
224				cmds = append(cmds, cmd)
225			}
226		case ggit.RefsTags:
227			t, cmd := r.boxes[tagsTab].Update(msg)
228			r.boxes[tagsTab] = t.(*Refs)
229			if cmd != nil {
230				cmds = append(cmds, cmd)
231			}
232		}
233	case UpdateStatusBarMsg:
234		cmds = append(cmds, r.updateStatusBarCmd)
235	case tea.WindowSizeMsg:
236		b, cmd := r.boxes[readmeTab].Update(msg)
237		r.boxes[readmeTab] = b.(*code.Code)
238		if cmd != nil {
239			cmds = append(cmds, cmd)
240		}
241		cmds = append(cmds, r.updateModels(msg))
242	}
243	s, cmd := r.statusbar.Update(msg)
244	r.statusbar = s.(*statusbar.StatusBar)
245	if cmd != nil {
246		cmds = append(cmds, cmd)
247	}
248	m, cmd := r.boxes[r.activeTab].Update(msg)
249	r.boxes[r.activeTab] = m.(common.Component)
250	if cmd != nil {
251		cmds = append(cmds, cmd)
252	}
253	return r, tea.Batch(cmds...)
254}
255
256// View implements tea.Model.
257func (r *Repo) View() string {
258	s := r.common.Styles.Repo.Copy().
259		Width(r.common.Width).
260		Height(r.common.Height)
261	repoBodyStyle := r.common.Styles.RepoBody.Copy()
262	hm := repoBodyStyle.GetVerticalFrameSize() +
263		r.common.Styles.RepoHeader.GetHeight() +
264		r.common.Styles.RepoHeader.GetVerticalFrameSize() +
265		r.common.Styles.StatusBar.GetHeight() +
266		r.common.Styles.Tabs.GetHeight() +
267		r.common.Styles.Tabs.GetVerticalFrameSize()
268	mainStyle := repoBodyStyle.
269		Height(r.common.Height - hm)
270	main := r.boxes[r.activeTab].View()
271	view := lipgloss.JoinVertical(lipgloss.Top,
272		r.headerView(),
273		r.tabs.View(),
274		mainStyle.Render(main),
275		r.statusbar.View(),
276	)
277	return s.Render(view)
278}
279
280func (r *Repo) headerView() string {
281	if r.selectedRepo == nil {
282		return ""
283	}
284	name := r.common.Styles.RepoHeaderName.Render(r.selectedItem.Title())
285	// TODO move this into a style.
286	url := lipgloss.NewStyle().
287		MarginLeft(1).
288		Width(r.common.Width - lipgloss.Width(name) - 1).
289		Align(lipgloss.Right).
290		Render(r.selectedItem.URL())
291	desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedItem.Description())
292	style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
293	return style.Render(
294		lipgloss.JoinVertical(lipgloss.Top,
295			lipgloss.JoinHorizontal(lipgloss.Left,
296				name,
297				url,
298			),
299			desc,
300		),
301	)
302}
303
304func (r *Repo) setRepoCmd(repo string) tea.Cmd {
305	return func() tea.Msg {
306		for _, r := range r.rs.AllRepos() {
307			if r.Name() == repo {
308				return RepoMsg(r)
309			}
310		}
311		return common.ErrorMsg(git.ErrMissingRepo)
312	}
313}
314
315func (r *Repo) updateStatusBarCmd() tea.Msg {
316	var info, value string
317	switch r.activeTab {
318	case readmeTab:
319		info = fmt.Sprintf("%.f%%", r.boxes[readmeTab].(*code.Code).ScrollPercent()*100)
320	default:
321		value = r.boxes[r.activeTab].(statusbar.Model).StatusBarValue()
322		info = r.boxes[r.activeTab].(statusbar.Model).StatusBarInfo()
323	}
324	return statusbar.StatusBarMsg{
325		Key:    r.selectedRepo.Name(),
326		Value:  value,
327		Info:   info,
328		Branch: fmt.Sprintf(" %s", r.ref.Name().Short()),
329	}
330}
331
332func (r *Repo) updateReadmeCmd() tea.Msg {
333	if r.selectedRepo == nil {
334		return common.ErrorCmd(git.ErrMissingRepo)
335	}
336	rm, rp := r.selectedRepo.Readme()
337	return r.boxes[readmeTab].(*code.Code).SetContent(rm, rp)
338}
339
340func (r *Repo) updateRefCmd() tea.Msg {
341	head, err := r.selectedRepo.HEAD()
342	if err != nil {
343		return common.ErrorMsg(err)
344	}
345	return RefMsg(head)
346}
347
348func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
349	cmds := make([]tea.Cmd, 0)
350	for i, b := range r.boxes {
351		m, cmd := b.Update(msg)
352		r.boxes[i] = m.(common.Component)
353		if cmd != nil {
354			cmds = append(cmds, cmd)
355		}
356	}
357	return tea.Batch(cmds...)
358}
359
360func updateStatusBarCmd() tea.Msg {
361	return UpdateStatusBarMsg{}
362}