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	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}