1package browse23import (4 "fmt"5 "path/filepath"6 "time"78 "charm.land/bubbles/v2/key"9 tea "charm.land/bubbletea/v2"10 "charm.land/lipgloss/v2"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/spf13/cobra"17)1819// Command is the browse command.20var Command = &cobra.Command{21 Use: "browse PATH",22 Short: "Browse a repository",23 Args: cobra.MaximumNArgs(1),24 RunE: func(cmd *cobra.Command, args []string) error {25 rp := "."26 if len(args) > 0 {27 rp = args[0]28 }2930 abs, err := filepath.Abs(rp)31 if err != nil {32 return err33 }3435 r, err := git.Open(abs)36 if err != nil {37 return fmt.Errorf("failed to open repository: %w", err)38 }3940 // Bubble Tea uses Termenv default output so we have to use the same41 // thing here.42 ctx := cmd.Context()43 c := common.NewCommon(ctx, 0, 0)44 c.HideCloneCmd = true45 comps := []common.TabComponent{46 repo.NewReadme(c),47 repo.NewFiles(c),48 repo.NewLog(c),49 }50 if !r.IsBare {51 comps = append(comps, repo.NewStash(c))52 }53 comps = append(comps, repo.NewRefs(c, git.RefsHeads), repo.NewRefs(c, git.RefsTags))54 m := &model{55 model: repo.New(c, comps...),56 repo: repository{r},57 common: c,58 }5960 m.footer = footer.New(c, m)61 p := tea.NewProgram(m)6263 _, err = p.Run()64 return err65 },66}6768type state int6970const (71 startState state = iota72 errorState73)7475type model struct {76 model *repo.Repo77 footer *footer.Footer78 repo proto.Repository79 common common.Common80 state state81 showFooter bool82 error error83}8485var _ tea.Model = &model{}8687func (m *model) SetSize(w, h int) {88 m.common.SetSize(w, h)89 style := m.common.Styles.App90 wm := style.GetHorizontalFrameSize()91 hm := style.GetVerticalFrameSize()92 if m.showFooter {93 hm += m.footer.Height()94 }9596 m.footer.SetSize(w-wm, h-hm)97 m.model.SetSize(w-wm, h-hm)98}99100// ShortHelp implements help.KeyMap.101func (m model) ShortHelp() []key.Binding {102 switch m.state {103 case errorState:104 return []key.Binding{105 m.common.KeyMap.Back,106 m.common.KeyMap.Quit,107 m.common.KeyMap.Help,108 }109 default:110 return m.model.ShortHelp()111 }112}113114// FullHelp implements help.KeyMap.115func (m model) FullHelp() [][]key.Binding {116 switch m.state {117 case errorState:118 return [][]key.Binding{119 {120 m.common.KeyMap.Back,121 },122 {123 m.common.KeyMap.Quit,124 m.common.KeyMap.Help,125 },126 }127 default:128 return m.model.FullHelp()129 }130}131132// Init implements tea.Model.133func (m *model) Init() tea.Cmd {134 return tea.Batch(135 m.model.Init(),136 m.footer.Init(),137 func() tea.Msg {138 return repo.RepoMsg(m.repo)139 },140 repo.UpdateRefCmd(m.repo),141 )142}143144// Update implements tea.Model.145func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {146 m.common.Logger.Debugf("msg received: %T", msg)147 cmds := make([]tea.Cmd, 0)148 switch msg := msg.(type) {149 case tea.WindowSizeMsg:150 m.SetSize(msg.Width, msg.Height)151 case tea.KeyPressMsg:152 switch {153 case key.Matches(msg, m.common.KeyMap.Back) && m.error != nil:154 m.error = nil155 m.state = startState156 // Always show the footer on error.157 m.showFooter = m.footer.ShowAll()158 case key.Matches(msg, m.common.KeyMap.Help):159 cmds = append(cmds, footer.ToggleFooterCmd)160 case key.Matches(msg, m.common.KeyMap.Quit):161 // Stop bubblezone background workers.162 m.common.Zone.Close()163 return m, tea.Quit164 }165 case tea.MouseClickMsg:166 mouse := msg.Mouse()167 switch mouse.Button {168 case tea.MouseLeft:169 switch {170 case m.common.Zone.Get("footer").InBounds(msg):171 cmds = append(cmds, footer.ToggleFooterCmd)172 }173 }174 case footer.ToggleFooterMsg:175 m.footer.SetShowAll(!m.footer.ShowAll())176 m.showFooter = !m.showFooter177 case common.ErrorMsg:178 m.error = msg179 m.state = errorState180 m.showFooter = true181 }182183 f, cmd := m.footer.Update(msg)184 m.footer = f.(*footer.Footer)185 if cmd != nil {186 cmds = append(cmds, cmd)187 }188189 r, cmd := m.model.Update(msg)190 m.model = r.(*repo.Repo)191 if cmd != nil {192 cmds = append(cmds, cmd)193 }194195 // This fixes determining the height margin of the footer.196 m.SetSize(m.common.Width, m.common.Height)197198 return m, tea.Batch(cmds...)199}200201// View implements tea.Model.202func (m *model) View() tea.View {203 var v tea.View204 v.AltScreen = true205 v.MouseMode = tea.MouseModeCellMotion206207 style := m.common.Styles.App208 wm, hm := style.GetHorizontalFrameSize(), style.GetVerticalFrameSize()209 if m.showFooter {210 hm += m.footer.Height()211 }212213 var view string214 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.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 }229230 if m.showFooter {231 view = lipgloss.JoinVertical(lipgloss.Left, view, m.footer.View())232 }233234 v.Content = m.common.Zone.Scan(style.Render(view))235 return v236}237238type repository struct {239 r *git.Repository240}241242var _ proto.Repository = repository{}243244// Description implements proto.Repository.245func (r repository) Description() string {246 return ""247}248249// ID implements proto.Repository.250func (r repository) ID() int64 {251 return 0252}253254// IsHidden implements proto.Repository.255func (repository) IsHidden() bool {256 return false257}258259// IsMirror implements proto.Repository.260func (repository) IsMirror() bool {261 return false262}263264// IsPrivate implements proto.Repository.265func (repository) IsPrivate() bool {266 return false267}268269// Name implements proto.Repository.270func (r repository) Name() string {271 return filepath.Base(r.r.Path)272}273274// Open implements proto.Repository.275func (r repository) Open() (*git.Repository, error) {276 return r.r, nil277}278279// ProjectName implements proto.Repository.280func (r repository) ProjectName() string {281 return r.Name()282}283284// UpdatedAt implements proto.Repository.285func (r repository) UpdatedAt() time.Time {286 t, err := r.r.LatestCommitTime()287 if err != nil {288 return time.Time{}289 }290291 return t292}293294// UserID implements proto.Repository.295func (r repository) UserID() int64 {296 return 0297}298299// CreatedAt implements proto.Repository.300func (r repository) CreatedAt() time.Time {301 return time.Time{}302}