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/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/session"
 17)
 18
 19type tab int
 20
 21const (
 22	readmeTab tab = iota
 23	filesTab
 24	commitsTab
 25	branchesTab
 26	tagsTab
 27)
 28
 29// UpdateStatusBarMsg updates the status bar.
 30type UpdateStatusBarMsg struct{}
 31
 32// RepoMsg is a message that contains a git.Repository.
 33type RepoMsg git.GitRepo
 34
 35// RefMsg is a message that contains a git.Reference.
 36type RefMsg *ggit.Reference
 37
 38// Repo is a view for a git repository.
 39type Repo struct {
 40	s            session.Session
 41	common       common.Common
 42	rs           git.GitRepoSource
 43	selectedRepo git.GitRepo
 44	activeTab    tab
 45	tabs         *tabs.Tabs
 46	statusbar    *statusbar.StatusBar
 47	boxes        []common.Component
 48	ref          *ggit.Reference
 49}
 50
 51// New returns a new Repo.
 52func New(s session.Session, c common.Common) *Repo {
 53	sb := statusbar.New(c)
 54	// Tabs must match the order of tab constants above.
 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	// Make sure the order matches the order of tab constants above.
 63	boxes := []common.Component{
 64		readme,
 65		files,
 66		log,
 67		branches,
 68		tags,
 69	}
 70	r := &Repo{
 71		s:         s,
 72		common:    c,
 73		rs:        s.Source(),
 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[r.activeTab].(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				k.HalfPageDown,
132				k.HalfPageUp,
133			},
134			{
135				k.Down,
136				k.Up,
137			},
138		}...)
139	default:
140		b = append(b, r.boxes[r.activeTab].(help.KeyMap).FullHelp()...)
141	}
142	return b
143}
144
145// Init implements tea.View.
146func (r *Repo) Init() tea.Cmd {
147	return tea.Batch(
148		r.tabs.Init(),
149		r.statusbar.Init(),
150	)
151}
152
153// Update implements tea.Model.
154func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
155	cmds := make([]tea.Cmd, 0)
156	switch msg := msg.(type) {
157	case RepoMsg:
158		r.activeTab = 0
159		r.selectedRepo = git.GitRepo(msg)
160		r.boxes[readmeTab].(*code.Code).GotoTop()
161		cmds = append(cmds,
162			r.tabs.Init(),
163			r.updateReadmeCmd,
164			r.updateRefCmd,
165			r.updateModels(msg),
166		)
167	case RefMsg:
168		r.ref = msg
169		for _, b := range r.boxes {
170			cmds = append(cmds, b.Init())
171		}
172		cmds = append(cmds,
173			r.updateStatusBarCmd,
174			r.updateModels(msg),
175		)
176	case tabs.SelectTabMsg:
177		r.activeTab = tab(msg)
178		t, cmd := r.tabs.Update(msg)
179		r.tabs = t.(*tabs.Tabs)
180		if cmd != nil {
181			cmds = append(cmds, cmd)
182		}
183	case tabs.ActiveTabMsg:
184		r.activeTab = tab(msg)
185		if r.selectedRepo != nil {
186			cmds = append(cmds, r.updateStatusBarCmd)
187		}
188	case tea.KeyMsg, tea.MouseMsg:
189		t, cmd := r.tabs.Update(msg)
190		r.tabs = t.(*tabs.Tabs)
191		if cmd != nil {
192			cmds = append(cmds, cmd)
193		}
194		if r.selectedRepo != nil {
195			cmds = append(cmds, r.updateStatusBarCmd)
196		}
197	case FileItemsMsg:
198		f, cmd := r.boxes[filesTab].Update(msg)
199		r.boxes[filesTab] = f.(*Files)
200		if cmd != nil {
201			cmds = append(cmds, cmd)
202		}
203	case LogCountMsg, LogItemsMsg:
204		l, cmd := r.boxes[commitsTab].Update(msg)
205		r.boxes[commitsTab] = l.(*Log)
206		if cmd != nil {
207			cmds = append(cmds, cmd)
208		}
209	case RefItemsMsg:
210		switch msg.prefix {
211		case ggit.RefsHeads:
212			b, cmd := r.boxes[branchesTab].Update(msg)
213			r.boxes[branchesTab] = b.(*Refs)
214			if cmd != nil {
215				cmds = append(cmds, cmd)
216			}
217		case ggit.RefsTags:
218			t, cmd := r.boxes[tagsTab].Update(msg)
219			r.boxes[tagsTab] = t.(*Refs)
220			if cmd != nil {
221				cmds = append(cmds, cmd)
222			}
223		}
224	case UpdateStatusBarMsg:
225		cmds = append(cmds, r.updateStatusBarCmd)
226	case tea.WindowSizeMsg:
227		b, cmd := r.boxes[readmeTab].Update(msg)
228		r.boxes[readmeTab] = b.(*code.Code)
229		if cmd != nil {
230			cmds = append(cmds, cmd)
231		}
232		cmds = append(cmds, r.updateModels(msg))
233	}
234	s, cmd := r.statusbar.Update(msg)
235	r.statusbar = s.(*statusbar.StatusBar)
236	if cmd != nil {
237		cmds = append(cmds, cmd)
238	}
239	m, cmd := r.boxes[r.activeTab].Update(msg)
240	r.boxes[r.activeTab] = m.(common.Component)
241	if cmd != nil {
242		cmds = append(cmds, cmd)
243	}
244	return r, tea.Batch(cmds...)
245}
246
247// View implements tea.Model.
248func (r *Repo) View() string {
249	s := r.common.Styles.Repo.Copy().
250		Width(r.common.Width).
251		Height(r.common.Height)
252	repoBodyStyle := r.common.Styles.RepoBody.Copy()
253	hm := repoBodyStyle.GetVerticalFrameSize() +
254		r.common.Styles.RepoHeader.GetHeight() +
255		r.common.Styles.RepoHeader.GetVerticalFrameSize() +
256		r.common.Styles.StatusBar.GetHeight() +
257		r.common.Styles.Tabs.GetHeight() +
258		r.common.Styles.Tabs.GetVerticalFrameSize()
259	mainStyle := repoBodyStyle.
260		Height(r.common.Height - hm)
261	main := r.boxes[r.activeTab].View()
262	view := lipgloss.JoinVertical(lipgloss.Top,
263		r.headerView(),
264		r.tabs.View(),
265		mainStyle.Render(main),
266		r.statusbar.View(),
267	)
268	return s.Render(view)
269}
270
271func (r *Repo) headerView() string {
272	if r.selectedRepo == nil {
273		return ""
274	}
275	cfg := r.s.Config()
276	name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name())
277	url := git.RepoURL(cfg.Host, cfg.Port, r.selectedRepo.Repo())
278	// TODO move this into a style.
279	url = lipgloss.NewStyle().
280		MarginLeft(1).
281		Foreground(lipgloss.Color("168")).
282		Width(r.common.Width - lipgloss.Width(name) - 1).
283		Align(lipgloss.Right).
284		Render(url)
285	desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedRepo.Description())
286	style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
287	return style.Render(
288		lipgloss.JoinVertical(lipgloss.Top,
289			lipgloss.JoinHorizontal(lipgloss.Left,
290				name,
291				url,
292			),
293			desc,
294		),
295	)
296}
297
298func (r *Repo) updateStatusBarCmd() tea.Msg {
299	var info, value string
300	switch r.activeTab {
301	case readmeTab:
302		info = fmt.Sprintf("☰ %.f%%", r.boxes[readmeTab].(*code.Code).ScrollPercent()*100)
303	default:
304		value = r.boxes[r.activeTab].(statusbar.Model).StatusBarValue()
305		info = r.boxes[r.activeTab].(statusbar.Model).StatusBarInfo()
306	}
307	return statusbar.StatusBarMsg{
308		Key:    r.selectedRepo.Name(),
309		Value:  value,
310		Info:   info,
311		Branch: fmt.Sprintf(" %s", r.ref.Name().Short()),
312	}
313}
314
315func (r *Repo) updateReadmeCmd() tea.Msg {
316	if r.selectedRepo == nil {
317		return common.ErrorCmd(git.ErrMissingRepo)
318	}
319	rm, rp := r.selectedRepo.Readme()
320	return r.boxes[readmeTab].(*code.Code).SetContent(rm, rp)
321}
322
323func (r *Repo) updateRefCmd() tea.Msg {
324	head, err := r.selectedRepo.HEAD()
325	if err != nil {
326		return common.ErrorMsg(err)
327	}
328	return RefMsg(head)
329}
330
331func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
332	cmds := make([]tea.Cmd, 0)
333	for i, b := range r.boxes {
334		m, cmd := b.Update(msg)
335		r.boxes[i] = m.(common.Component)
336		if cmd != nil {
337			cmds = append(cmds, cmd)
338		}
339	}
340	return tea.Batch(cmds...)
341}
342
343func updateStatusBarCmd() tea.Msg {
344	return UpdateStatusBarMsg{}
345}