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	"github.com/charmbracelet/soft-serve/config"
 11	ggit "github.com/charmbracelet/soft-serve/git"
 12	"github.com/charmbracelet/soft-serve/ui/common"
 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)
 17
 18type tab int
 19
 20const (
 21	readmeTab tab = iota
 22	filesTab
 23	commitsTab
 24	branchesTab
 25	tagsTab
 26)
 27
 28// UpdateStatusBarMsg updates the status bar.
 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	cfg          *config.Config
 41	rs           git.GitRepoSource
 42	selectedRepo git.GitRepo
 43	activeTab    tab
 44	tabs         *tabs.Tabs
 45	statusbar    *statusbar.StatusBar
 46	boxes        []common.Component
 47	ref          *ggit.Reference
 48}
 49
 50// New returns a new Repo.
 51func New(cfg *config.Config, rs git.GitRepoSource, c common.Common) *Repo {
 52	sb := statusbar.New(c)
 53	// Tabs must match the order of tab constants above.
 54	tb := tabs.New(c, []string{"Readme", "Files", "Commits", "Branches", "Tags"})
 55	readme := NewReadme(c)
 56	log := NewLog(c)
 57	files := NewFiles(c)
 58	branches := NewRefs(c, ggit.RefsHeads)
 59	tags := NewRefs(c, ggit.RefsTags)
 60	// Make sure the order matches the order of tab constants above.
 61	boxes := []common.Component{
 62		readme,
 63		files,
 64		log,
 65		branches,
 66		tags,
 67	}
 68	r := &Repo{
 69		cfg:       cfg,
 70		common:    c,
 71		rs:        rs,
 72		tabs:      tb,
 73		statusbar: sb,
 74		boxes:     boxes,
 75	}
 76	return r
 77}
 78
 79// SetSize implements common.Component.
 80func (r *Repo) SetSize(width, height int) {
 81	r.common.SetSize(width, height)
 82	hm := r.common.Styles.RepoBody.GetVerticalFrameSize() +
 83		r.common.Styles.RepoHeader.GetHeight() +
 84		r.common.Styles.RepoHeader.GetVerticalFrameSize() +
 85		r.common.Styles.StatusBar.GetHeight() +
 86		r.common.Styles.Tabs.GetHeight() +
 87		r.common.Styles.Tabs.GetVerticalFrameSize()
 88	r.tabs.SetSize(width, height-hm)
 89	r.statusbar.SetSize(width, height-hm)
 90	for _, b := range r.boxes {
 91		b.SetSize(width, height-hm)
 92	}
 93}
 94
 95func (r *Repo) commonHelp() []key.Binding {
 96	b := make([]key.Binding, 0)
 97	back := r.common.KeyMap.Back
 98	back.SetHelp("esc", "back to menu")
 99	tab := r.common.KeyMap.Section
100	tab.SetHelp("tab", "switch tab")
101	b = append(b, back)
102	b = append(b, tab)
103	return b
104}
105
106// ShortHelp implements help.KeyMap.
107func (r *Repo) ShortHelp() []key.Binding {
108	b := r.commonHelp()
109	b = append(b, r.boxes[r.activeTab].(help.KeyMap).ShortHelp()...)
110	return b
111}
112
113// FullHelp implements help.KeyMap.
114func (r *Repo) FullHelp() [][]key.Binding {
115	b := make([][]key.Binding, 0)
116	b = append(b, r.commonHelp())
117	b = append(b, r.boxes[r.activeTab].(help.KeyMap).FullHelp()...)
118	return b
119}
120
121// Init implements tea.View.
122func (r *Repo) Init() tea.Cmd {
123	return tea.Batch(
124		r.tabs.Init(),
125		r.statusbar.Init(),
126	)
127}
128
129// Update implements tea.Model.
130func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
131	cmds := make([]tea.Cmd, 0)
132	switch msg := msg.(type) {
133	case RepoMsg:
134		r.activeTab = 0
135		r.selectedRepo = git.GitRepo(msg)
136		cmds = append(cmds,
137			r.tabs.Init(),
138			r.updateRefCmd,
139			r.updateModels(msg),
140		)
141	case RefMsg:
142		r.ref = msg
143		for _, b := range r.boxes {
144			cmds = append(cmds, b.Init())
145		}
146		cmds = append(cmds,
147			r.updateStatusBarCmd,
148			r.updateModels(msg),
149		)
150	case tabs.SelectTabMsg:
151		r.activeTab = tab(msg)
152		t, cmd := r.tabs.Update(msg)
153		r.tabs = t.(*tabs.Tabs)
154		if cmd != nil {
155			cmds = append(cmds, cmd)
156		}
157	case tabs.ActiveTabMsg:
158		r.activeTab = tab(msg)
159		if r.selectedRepo != nil {
160			cmds = append(cmds, r.updateStatusBarCmd)
161		}
162	case tea.KeyMsg, tea.MouseMsg:
163		t, cmd := r.tabs.Update(msg)
164		r.tabs = t.(*tabs.Tabs)
165		if cmd != nil {
166			cmds = append(cmds, cmd)
167		}
168		if r.selectedRepo != nil {
169			cmds = append(cmds, r.updateStatusBarCmd)
170		}
171	case FileItemsMsg:
172		f, cmd := r.boxes[filesTab].Update(msg)
173		r.boxes[filesTab] = f.(*Files)
174		if cmd != nil {
175			cmds = append(cmds, cmd)
176		}
177	case LogCountMsg, LogItemsMsg:
178		l, cmd := r.boxes[commitsTab].Update(msg)
179		r.boxes[commitsTab] = l.(*Log)
180		if cmd != nil {
181			cmds = append(cmds, cmd)
182		}
183	case RefItemsMsg:
184		switch msg.prefix {
185		case ggit.RefsHeads:
186			b, cmd := r.boxes[branchesTab].Update(msg)
187			r.boxes[branchesTab] = b.(*Refs)
188			if cmd != nil {
189				cmds = append(cmds, cmd)
190			}
191		case ggit.RefsTags:
192			t, cmd := r.boxes[tagsTab].Update(msg)
193			r.boxes[tagsTab] = t.(*Refs)
194			if cmd != nil {
195				cmds = append(cmds, cmd)
196			}
197		}
198	case UpdateStatusBarMsg:
199		cmds = append(cmds, r.updateStatusBarCmd)
200	case tea.WindowSizeMsg:
201		cmds = append(cmds, r.updateModels(msg))
202	}
203	s, cmd := r.statusbar.Update(msg)
204	r.statusbar = s.(*statusbar.StatusBar)
205	if cmd != nil {
206		cmds = append(cmds, cmd)
207	}
208	m, cmd := r.boxes[r.activeTab].Update(msg)
209	r.boxes[r.activeTab] = m.(common.Component)
210	if cmd != nil {
211		cmds = append(cmds, cmd)
212	}
213	return r, tea.Batch(cmds...)
214}
215
216// View implements tea.Model.
217func (r *Repo) View() string {
218	s := r.common.Styles.Repo.Copy().
219		Width(r.common.Width).
220		Height(r.common.Height)
221	repoBodyStyle := r.common.Styles.RepoBody.Copy()
222	hm := repoBodyStyle.GetVerticalFrameSize() +
223		r.common.Styles.RepoHeader.GetHeight() +
224		r.common.Styles.RepoHeader.GetVerticalFrameSize() +
225		r.common.Styles.StatusBar.GetHeight() +
226		r.common.Styles.Tabs.GetHeight() +
227		r.common.Styles.Tabs.GetVerticalFrameSize()
228	mainStyle := repoBodyStyle.
229		Height(r.common.Height - hm)
230	main := r.boxes[r.activeTab].View()
231	view := lipgloss.JoinVertical(lipgloss.Top,
232		r.headerView(),
233		r.tabs.View(),
234		mainStyle.Render(main),
235		r.statusbar.View(),
236	)
237	return s.Render(view)
238}
239
240func (r *Repo) headerView() string {
241	if r.selectedRepo == nil {
242		return ""
243	}
244	cfg := r.cfg
245	name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name())
246	url := git.RepoURL(cfg.Host, cfg.Port, r.selectedRepo.Repo())
247	// TODO move this into a style.
248	url = lipgloss.NewStyle().
249		MarginLeft(1).
250		Foreground(lipgloss.Color("168")).
251		Width(r.common.Width - lipgloss.Width(name) - 1).
252		Align(lipgloss.Right).
253		Render(url)
254	desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedRepo.Description())
255	style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
256	return style.Render(
257		lipgloss.JoinVertical(lipgloss.Top,
258			lipgloss.JoinHorizontal(lipgloss.Left,
259				name,
260				url,
261			),
262			desc,
263		),
264	)
265}
266
267func (r *Repo) updateStatusBarCmd() tea.Msg {
268	value := r.boxes[r.activeTab].(statusbar.Model).StatusBarValue()
269	info := r.boxes[r.activeTab].(statusbar.Model).StatusBarInfo()
270	return statusbar.StatusBarMsg{
271		Key:    r.selectedRepo.Name(),
272		Value:  value,
273		Info:   info,
274		Branch: fmt.Sprintf(" %s", r.ref.Name().Short()),
275	}
276}
277
278func (r *Repo) updateRefCmd() tea.Msg {
279	head, err := r.selectedRepo.HEAD()
280	if err != nil {
281		return common.ErrorMsg(err)
282	}
283	return RefMsg(head)
284}
285
286func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
287	cmds := make([]tea.Cmd, 0)
288	for i, b := range r.boxes {
289		m, cmd := b.Update(msg)
290		r.boxes[i] = m.(common.Component)
291		if cmd != nil {
292			cmds = append(cmds, cmd)
293		}
294	}
295	return tea.Batch(cmds...)
296}
297
298func updateStatusBarCmd() tea.Msg {
299	return UpdateStatusBarMsg{}
300}