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