bubble.go

  1package about
  2
  3import (
  4	"github.com/charmbracelet/bubbles/viewport"
  5	tea "github.com/charmbracelet/bubbletea"
  6	"github.com/charmbracelet/glamour"
  7	"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
  8	vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
  9	"github.com/charmbracelet/soft-serve/internal/tui/style"
 10	"github.com/muesli/reflow/wrap"
 11)
 12
 13type Bubble struct {
 14	readmeViewport *vp.ViewportBubble
 15	repo           types.Repo
 16	styles         *style.Styles
 17	height         int
 18	heightMargin   int
 19	width          int
 20	widthMargin    int
 21}
 22
 23func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
 24	b := &Bubble{
 25		readmeViewport: &vp.ViewportBubble{
 26			Viewport: &viewport.Model{},
 27		},
 28		repo:         repo,
 29		styles:       styles,
 30		widthMargin:  wm,
 31		heightMargin: hm,
 32	}
 33	b.SetSize(width, height)
 34	return b
 35}
 36func (b *Bubble) Init() tea.Cmd {
 37	return b.setupCmd
 38}
 39
 40func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 41	var cmds []tea.Cmd
 42	switch msg := msg.(type) {
 43	case tea.WindowSizeMsg:
 44		b.SetSize(msg.Width, msg.Height)
 45		// XXX: if we find that longer readmes take more than a few
 46		// milliseconds to render we may need to move Glamour rendering into a
 47		// command.
 48		md, err := b.glamourize(b.repo.GetReadme())
 49		if err != nil {
 50			return b, nil
 51		}
 52		b.readmeViewport.Viewport.SetContent(md)
 53	case tea.KeyMsg:
 54		switch msg.String() {
 55		case "A":
 56			b.GotoTop()
 57		}
 58	}
 59	rv, cmd := b.readmeViewport.Update(msg)
 60	b.readmeViewport = rv.(*vp.ViewportBubble)
 61	cmds = append(cmds, cmd)
 62	return b, tea.Batch(cmds...)
 63}
 64
 65func (b *Bubble) SetSize(w, h int) {
 66	b.width = w
 67	b.height = h
 68	b.readmeViewport.Viewport.Width = w - b.widthMargin
 69	b.readmeViewport.Viewport.Height = h - b.heightMargin
 70}
 71
 72func (b *Bubble) GotoTop() {
 73	b.readmeViewport.Viewport.GotoTop()
 74}
 75
 76func (b *Bubble) View() string {
 77	return b.readmeViewport.View()
 78}
 79
 80func (b *Bubble) Help() []types.HelpEntry {
 81	return []types.HelpEntry{
 82		{"f/b", "pgup/pgdown"},
 83	}
 84}
 85
 86func (b *Bubble) setupCmd() tea.Msg {
 87	md, err := b.glamourize(b.repo.GetReadme())
 88	if err != nil {
 89		return types.ErrMsg{err}
 90	}
 91	b.readmeViewport.Viewport.SetContent(md)
 92	b.GotoTop()
 93	return nil
 94}
 95
 96func (b *Bubble) glamourize(md string) (string, error) {
 97	w := b.width - b.widthMargin - b.styles.RepoBody.GetHorizontalFrameSize()
 98	if w > types.GlamourMaxWidth {
 99		w = types.GlamourMaxWidth
100	}
101	tr, err := glamour.NewTermRenderer(
102		glamour.WithStyles(types.DefaultStyles()),
103		glamour.WithWordWrap(w),
104	)
105
106	if err != nil {
107		return "", err
108	}
109	mdt, err := tr.Render(md)
110	if err != nil {
111		return "", err
112	}
113	// For now, hard-wrap long lines in Glamour that would otherwise break the
114	// layout when wrapping. This may be due to #43 in Reflow, which has to do
115	// with a bug in the way lines longer than the given width are wrapped.
116	//
117	//     https://github.com/muesli/reflow/issues/43
118	//
119	// TODO: solve this upstream in Glamour/Reflow.
120	return wrap.String(mdt, w), nil
121}