browse.go

  1package browse
  2
  3import (
  4	"fmt"
  5	"path/filepath"
  6	"time"
  7
  8	"github.com/charmbracelet/bubbles/key"
  9	tea "github.com/charmbracelet/bubbletea"
 10	"github.com/charmbracelet/lipgloss"
 11	"github.com/charmbracelet/soft-serve/git"
 12	"github.com/charmbracelet/soft-serve/pkg/proto"
 13	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 14	"github.com/charmbracelet/soft-serve/pkg/ui/components/footer"
 15	"github.com/charmbracelet/soft-serve/pkg/ui/pages/repo"
 16	"github.com/muesli/termenv"
 17	"github.com/spf13/cobra"
 18)
 19
 20// Command is the browse command.
 21var Command = &cobra.Command{
 22	Use:   "browse PATH",
 23	Short: "Browse a repository",
 24	Args:  cobra.MaximumNArgs(1),
 25	RunE: func(cmd *cobra.Command, args []string) error {
 26		rp := "."
 27		if len(args) > 0 {
 28			rp = args[0]
 29		}
 30
 31		abs, err := filepath.Abs(rp)
 32		if err != nil {
 33			return err
 34		}
 35
 36		r, err := git.Open(abs)
 37		if err != nil {
 38			return fmt.Errorf("failed to open repository: %w", err)
 39		}
 40
 41		// Bubble Tea uses Termenv default output so we have to use the same
 42		// thing here.
 43		output := termenv.DefaultOutput()
 44		ctx := cmd.Context()
 45		c := common.NewCommon(ctx, output, 0, 0)
 46		c.HideCloneCmd = true
 47		comps := []common.TabComponent{
 48			repo.NewReadme(c),
 49			repo.NewFiles(c),
 50			repo.NewLog(c),
 51		}
 52		if !r.IsBare {
 53			comps = append(comps, repo.NewStash(c))
 54		}
 55		comps = append(comps, repo.NewRefs(c, git.RefsHeads), repo.NewRefs(c, git.RefsTags))
 56		m := &model{
 57			model:  repo.New(c, comps...),
 58			repo:   repository{r},
 59			common: c,
 60		}
 61
 62		m.footer = footer.New(c, m)
 63		p := tea.NewProgram(m,
 64			tea.WithAltScreen(),
 65			tea.WithMouseCellMotion(),
 66		)
 67
 68		_, err = p.Run()
 69		return err
 70	},
 71}
 72
 73type state int
 74
 75const (
 76	startState state = iota
 77	errorState
 78)
 79
 80type model struct {
 81	model      *repo.Repo
 82	footer     *footer.Footer
 83	repo       proto.Repository
 84	common     common.Common
 85	state      state
 86	showFooter bool
 87	error      error
 88}
 89
 90var _ tea.Model = &model{}
 91
 92func (m *model) SetSize(w, h int) {
 93	m.common.SetSize(w, h)
 94	style := m.common.Styles.App.Copy()
 95	wm := style.GetHorizontalFrameSize()
 96	hm := style.GetVerticalFrameSize()
 97	if m.showFooter {
 98		hm += m.footer.Height()
 99	}
100
101	m.footer.SetSize(w-wm, h-hm)
102	m.model.SetSize(w-wm, h-hm)
103}
104
105// ShortHelp implements help.KeyMap.
106func (m model) ShortHelp() []key.Binding {
107	switch m.state {
108	case errorState:
109		return []key.Binding{
110			m.common.KeyMap.Back,
111			m.common.KeyMap.Quit,
112			m.common.KeyMap.Help,
113		}
114	default:
115		return m.model.ShortHelp()
116	}
117}
118
119// FullHelp implements help.KeyMap.
120func (m model) FullHelp() [][]key.Binding {
121	switch m.state {
122	case errorState:
123		return [][]key.Binding{
124			{
125				m.common.KeyMap.Back,
126			},
127			{
128				m.common.KeyMap.Quit,
129				m.common.KeyMap.Help,
130			},
131		}
132	default:
133		return m.model.FullHelp()
134	}
135}
136
137// Init implements tea.Model.
138func (m *model) Init() tea.Cmd {
139	return tea.Batch(
140		m.model.Init(),
141		m.footer.Init(),
142		func() tea.Msg {
143			return repo.RepoMsg(m.repo)
144		},
145		repo.UpdateRefCmd(m.repo),
146	)
147}
148
149// Update implements tea.Model.
150func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
151	m.common.Logger.Debugf("msg received: %T", msg)
152	cmds := make([]tea.Cmd, 0)
153	switch msg := msg.(type) {
154	case tea.WindowSizeMsg:
155		m.SetSize(msg.Width, msg.Height)
156	case tea.KeyMsg:
157		switch {
158		case key.Matches(msg, m.common.KeyMap.Back) && m.error != nil:
159			m.error = nil
160			m.state = startState
161			// Always show the footer on error.
162			m.showFooter = m.footer.ShowAll()
163		case key.Matches(msg, m.common.KeyMap.Help):
164			cmds = append(cmds, footer.ToggleFooterCmd)
165		case key.Matches(msg, m.common.KeyMap.Quit):
166			// Stop bubblezone background workers.
167			m.common.Zone.Close()
168			return m, tea.Quit
169		}
170	case tea.MouseMsg:
171		switch msg.Type {
172		case tea.MouseLeft:
173			switch {
174			case m.common.Zone.Get("footer").InBounds(msg):
175				cmds = append(cmds, footer.ToggleFooterCmd)
176			}
177		}
178	case footer.ToggleFooterMsg:
179		m.footer.SetShowAll(!m.footer.ShowAll())
180		m.showFooter = !m.showFooter
181	case common.ErrorMsg:
182		m.error = msg
183		m.state = errorState
184		m.showFooter = true
185	}
186
187	f, cmd := m.footer.Update(msg)
188	m.footer = f.(*footer.Footer)
189	if cmd != nil {
190		cmds = append(cmds, cmd)
191	}
192
193	r, cmd := m.model.Update(msg)
194	m.model = r.(*repo.Repo)
195	if cmd != nil {
196		cmds = append(cmds, cmd)
197	}
198
199	// This fixes determining the height margin of the footer.
200	m.SetSize(m.common.Width, m.common.Height)
201
202	return m, tea.Batch(cmds...)
203}
204
205// View implements tea.Model.
206func (m *model) View() string {
207	style := m.common.Styles.App.Copy()
208	wm, hm := style.GetHorizontalFrameSize(), style.GetVerticalFrameSize()
209	if m.showFooter {
210		hm += m.footer.Height()
211	}
212
213	var view string
214	switch m.state {
215	case startState:
216		view = m.model.View()
217	case errorState:
218		err := m.common.Styles.ErrorTitle.Render("Bummer")
219		err += m.common.Styles.ErrorBody.Render(m.error.Error())
220		view = m.common.Styles.Error.Copy().
221			Width(m.common.Width -
222				wm -
223				m.common.Styles.ErrorBody.GetHorizontalFrameSize()).
224			Height(m.common.Height -
225				hm -
226				m.common.Styles.Error.GetVerticalFrameSize()).
227			Render(err)
228	}
229
230	if m.showFooter {
231		view = lipgloss.JoinVertical(lipgloss.Top, view, m.footer.View())
232	}
233
234	return m.common.Zone.Scan(style.Render(view))
235}
236
237type repository struct {
238	r *git.Repository
239}
240
241var _ proto.Repository = repository{}
242
243// Description implements proto.Repository.
244func (r repository) Description() string {
245	return ""
246}
247
248// ID implements proto.Repository.
249func (r repository) ID() int64 {
250	return 0
251}
252
253// IsHidden implements proto.Repository.
254func (repository) IsHidden() bool {
255	return false
256}
257
258// IsMirror implements proto.Repository.
259func (repository) IsMirror() bool {
260	return false
261}
262
263// IsPrivate implements proto.Repository.
264func (repository) IsPrivate() bool {
265	return false
266}
267
268// Name implements proto.Repository.
269func (r repository) Name() string {
270	return filepath.Base(r.r.Path)
271}
272
273// Open implements proto.Repository.
274func (r repository) Open() (*git.Repository, error) {
275	return r.r, nil
276}
277
278// ProjectName implements proto.Repository.
279func (r repository) ProjectName() string {
280	return r.Name()
281}
282
283// UpdatedAt implements proto.Repository.
284func (r repository) UpdatedAt() time.Time {
285	t, err := r.r.LatestCommitTime()
286	if err != nil {
287		return time.Time{}
288	}
289
290	return t
291}
292
293// UserID implements proto.Repository.
294func (r repository) UserID() int64 {
295	return 0
296}
297
298// CreatedAt implements proto.Repository.
299func (r repository) CreatedAt() time.Time {
300	return time.Time{}
301}