repo.go

  1package repo
  2
  3import (
  4	"fmt"
  5
  6	"github.com/charmbracelet/bubbles/help"
  7	"github.com/charmbracelet/bubbles/key"
  8	"github.com/charmbracelet/bubbles/spinner"
  9	tea "github.com/charmbracelet/bubbletea"
 10	"github.com/charmbracelet/lipgloss"
 11	"github.com/charmbracelet/soft-serve/config"
 12	ggit "github.com/charmbracelet/soft-serve/git"
 13	"github.com/charmbracelet/soft-serve/ui/common"
 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)
 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	common       common.Common
 41	cfg          *config.Config
 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(cfg *config.Config, rs git.GitRepoSource, 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 := NewReadme(c)
 57	log := NewLog(c)
 58	files := NewFiles(c)
 59	branches := NewRefs(c, ggit.RefsHeads)
 60	tags := NewRefs(c, ggit.RefsTags)
 61	// Make sure the order matches the order of tab constants above.
 62	boxes := []common.Component{
 63		readme,
 64		files,
 65		log,
 66		branches,
 67		tags,
 68	}
 69	r := &Repo{
 70		cfg:       cfg,
 71		common:    c,
 72		rs:        rs,
 73		tabs:      tb,
 74		statusbar: sb,
 75		boxes:     boxes,
 76	}
 77	return r
 78}
 79
 80// SetSize implements common.Component.
 81func (r *Repo) SetSize(width, height int) {
 82	r.common.SetSize(width, height)
 83	hm := r.common.Styles.RepoBody.GetVerticalFrameSize() +
 84		r.common.Styles.RepoHeader.GetHeight() +
 85		r.common.Styles.RepoHeader.GetVerticalFrameSize() +
 86		r.common.Styles.StatusBar.GetHeight() +
 87		r.common.Styles.Tabs.GetHeight() +
 88		r.common.Styles.Tabs.GetVerticalFrameSize()
 89	r.tabs.SetSize(width, height-hm)
 90	r.statusbar.SetSize(width, height-hm)
 91	for _, b := range r.boxes {
 92		b.SetSize(width, height-hm)
 93	}
 94}
 95
 96func (r *Repo) commonHelp() []key.Binding {
 97	b := make([]key.Binding, 0)
 98	back := r.common.KeyMap.Back
 99	back.SetHelp("esc", "back to menu")
100	tab := r.common.KeyMap.Section
101	tab.SetHelp("tab", "switch tab")
102	b = append(b, back)
103	b = append(b, tab)
104	return b
105}
106
107// ShortHelp implements help.KeyMap.
108func (r *Repo) ShortHelp() []key.Binding {
109	b := r.commonHelp()
110	b = append(b, r.boxes[r.activeTab].(help.KeyMap).ShortHelp()...)
111	return b
112}
113
114// FullHelp implements help.KeyMap.
115func (r *Repo) FullHelp() [][]key.Binding {
116	b := make([][]key.Binding, 0)
117	b = append(b, r.commonHelp())
118	b = append(b, r.boxes[r.activeTab].(help.KeyMap).FullHelp()...)
119	return b
120}
121
122// Init implements tea.View.
123func (r *Repo) Init() tea.Cmd {
124	return tea.Batch(
125		r.tabs.Init(),
126		r.statusbar.Init(),
127	)
128}
129
130// Update implements tea.Model.
131func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
132	cmds := make([]tea.Cmd, 0)
133	switch msg := msg.(type) {
134	case RepoMsg:
135		r.activeTab = 0
136		r.selectedRepo = git.GitRepo(msg)
137		cmds = append(cmds,
138			r.tabs.Init(),
139			r.updateRefCmd,
140			r.updateModels(msg),
141		)
142	case RefMsg:
143		r.ref = msg
144		for _, b := range r.boxes {
145			cmds = append(cmds, b.Init())
146		}
147		cmds = append(cmds,
148			r.updateStatusBarCmd,
149			r.updateModels(msg),
150		)
151	case tabs.SelectTabMsg:
152		r.activeTab = tab(msg)
153		t, cmd := r.tabs.Update(msg)
154		r.tabs = t.(*tabs.Tabs)
155		if cmd != nil {
156			cmds = append(cmds, cmd)
157		}
158	case tabs.ActiveTabMsg:
159		r.activeTab = tab(msg)
160		if r.selectedRepo != nil {
161			cmds = append(cmds, r.updateStatusBarCmd)
162		}
163	case tea.KeyMsg, tea.MouseMsg:
164		t, cmd := r.tabs.Update(msg)
165		r.tabs = t.(*tabs.Tabs)
166		if cmd != nil {
167			cmds = append(cmds, cmd)
168		}
169		if r.selectedRepo != nil {
170			cmds = append(cmds, r.updateStatusBarCmd)
171		}
172	case FileItemsMsg:
173		f, cmd := r.boxes[filesTab].Update(msg)
174		r.boxes[filesTab] = f.(*Files)
175		if cmd != nil {
176			cmds = append(cmds, cmd)
177		}
178	// The Log bubble is the only bubble that uses a spinner, so this is fine
179	// for now. We need to pass the TickMsg to the Log bubble when the Log is
180	// loading but not the current selected tab so that the spinner works.
181	case LogCountMsg, LogItemsMsg, spinner.TickMsg:
182		l, cmd := r.boxes[commitsTab].Update(msg)
183		r.boxes[commitsTab] = l.(*Log)
184		if cmd != nil {
185			cmds = append(cmds, cmd)
186		}
187	case RefItemsMsg:
188		switch msg.prefix {
189		case ggit.RefsHeads:
190			b, cmd := r.boxes[branchesTab].Update(msg)
191			r.boxes[branchesTab] = b.(*Refs)
192			if cmd != nil {
193				cmds = append(cmds, cmd)
194			}
195		case ggit.RefsTags:
196			t, cmd := r.boxes[tagsTab].Update(msg)
197			r.boxes[tagsTab] = t.(*Refs)
198			if cmd != nil {
199				cmds = append(cmds, cmd)
200			}
201		}
202	case UpdateStatusBarMsg:
203		cmds = append(cmds, r.updateStatusBarCmd)
204	case tea.WindowSizeMsg:
205		cmds = append(cmds, r.updateModels(msg))
206	}
207	s, cmd := r.statusbar.Update(msg)
208	r.statusbar = s.(*statusbar.StatusBar)
209	if cmd != nil {
210		cmds = append(cmds, cmd)
211	}
212	m, cmd := r.boxes[r.activeTab].Update(msg)
213	r.boxes[r.activeTab] = m.(common.Component)
214	if cmd != nil {
215		cmds = append(cmds, cmd)
216	}
217	return r, tea.Batch(cmds...)
218}
219
220// View implements tea.Model.
221func (r *Repo) View() string {
222	s := r.common.Styles.Repo.Copy().
223		Width(r.common.Width).
224		Height(r.common.Height)
225	repoBodyStyle := r.common.Styles.RepoBody.Copy()
226	hm := repoBodyStyle.GetVerticalFrameSize() +
227		r.common.Styles.RepoHeader.GetHeight() +
228		r.common.Styles.RepoHeader.GetVerticalFrameSize() +
229		r.common.Styles.StatusBar.GetHeight() +
230		r.common.Styles.Tabs.GetHeight() +
231		r.common.Styles.Tabs.GetVerticalFrameSize()
232	mainStyle := repoBodyStyle.
233		Height(r.common.Height - hm)
234	main := r.boxes[r.activeTab].View()
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	cfg := r.cfg
249	name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name())
250	url := git.RepoURL(cfg.Host, cfg.Port, r.selectedRepo.Repo())
251	// TODO move this into a style.
252	url = lipgloss.NewStyle().
253		MarginLeft(1).
254		Foreground(lipgloss.Color("168")).
255		Width(r.common.Width - lipgloss.Width(name) - 1).
256		Align(lipgloss.Right).
257		Render(url)
258	desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedRepo.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) updateStatusBarCmd() tea.Msg {
272	value := r.boxes[r.activeTab].(statusbar.Model).StatusBarValue()
273	info := r.boxes[r.activeTab].(statusbar.Model).StatusBarInfo()
274	return statusbar.StatusBarMsg{
275		Key:    r.selectedRepo.Name(),
276		Value:  value,
277		Info:   info,
278		Branch: fmt.Sprintf(" %s", r.ref.Name().Short()),
279	}
280}
281
282func (r *Repo) updateRefCmd() tea.Msg {
283	head, err := r.selectedRepo.HEAD()
284	if err != nil {
285		return common.ErrorMsg(err)
286	}
287	return RefMsg(head)
288}
289
290func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
291	cmds := make([]tea.Cmd, 0)
292	for i, b := range r.boxes {
293		m, cmd := b.Update(msg)
294		r.boxes[i] = m.(common.Component)
295		if cmd != nil {
296			cmds = append(cmds, cmd)
297		}
298	}
299	return tea.Batch(cmds...)
300}
301
302func updateStatusBarCmd() tea.Msg {
303	return UpdateStatusBarMsg{}
304}