repo.go

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