bubble.go

  1package repo
  2
  3import (
  4	"bytes"
  5	"smoothie/git"
  6	"text/template"
  7
  8	"github.com/charmbracelet/bubbles/viewport"
  9	tea "github.com/charmbracelet/bubbletea"
 10	"github.com/charmbracelet/glamour"
 11	"github.com/charmbracelet/lipgloss"
 12)
 13
 14const glamourMaxWidth = 120
 15
 16type ErrMsg struct {
 17	Error error
 18}
 19
 20type Bubble struct {
 21	templateObject interface{}
 22	repoSource     *git.RepoSource
 23	name           string
 24	repo           *git.Repo
 25	readmeViewport *ViewportBubble
 26	readme         string
 27	height         int
 28	heightMargin   int
 29	width          int
 30	widthMargin    int
 31}
 32
 33func NewBubble(rs *git.RepoSource, name string, width, wm, height, hm int, tmp interface{}) *Bubble {
 34	b := &Bubble{
 35		templateObject: tmp,
 36		repoSource:     rs,
 37		name:           name,
 38		heightMargin:   hm,
 39		widthMargin:    wm,
 40		readmeViewport: &ViewportBubble{
 41			Viewport: &viewport.Model{},
 42		},
 43	}
 44	b.SetSize(width, height)
 45	return b
 46}
 47
 48func (b *Bubble) Init() tea.Cmd {
 49	return b.setupCmd
 50}
 51
 52func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 53	var cmds []tea.Cmd
 54	switch msg := msg.(type) {
 55	case tea.WindowSizeMsg:
 56		b.SetSize(msg.Width, msg.Height)
 57		// XXX: if we find that longer readmes take more than a few
 58		// milliseconds to render we may need to move Glamour rendering into a
 59		// command.
 60		md, err := b.glamourize(b.readme)
 61		if err != nil {
 62			return b, nil
 63		}
 64		b.readmeViewport.Viewport.SetContent(md)
 65	}
 66	rv, cmd := b.readmeViewport.Update(msg)
 67	b.readmeViewport = rv.(*ViewportBubble)
 68	cmds = append(cmds, cmd)
 69	return b, tea.Batch(cmds...)
 70}
 71
 72func (b *Bubble) SetSize(w, h int) {
 73	b.width = w
 74	b.height = h
 75	b.readmeViewport.Viewport.Width = w - b.widthMargin
 76	b.readmeViewport.Viewport.Height = h - b.heightMargin
 77}
 78
 79func (b *Bubble) GotoTop() {
 80	b.readmeViewport.Viewport.GotoTop()
 81}
 82
 83func (b *Bubble) View() string {
 84	return b.readmeViewport.View()
 85}
 86
 87func (b *Bubble) setupCmd() tea.Msg {
 88	r, err := b.repoSource.GetRepo(b.name)
 89	if err == git.ErrMissingRepo {
 90		return nil
 91	}
 92	if err != nil {
 93		return ErrMsg{err}
 94	}
 95	md := r.Readme
 96	if b.templateObject != nil {
 97		md, err = b.templatize(md)
 98		if err != nil {
 99			return ErrMsg{err}
100		}
101	}
102	b.readme = md
103	md, err = b.glamourize(md)
104	if err != nil {
105		return ErrMsg{err}
106	}
107	b.readmeViewport.Viewport.SetContent(md)
108	b.GotoTop()
109	return nil
110}
111
112func (b *Bubble) templatize(mdt string) (string, error) {
113	t, err := template.New("readme").Parse(mdt)
114	if err != nil {
115		return "", err
116	}
117	buf := &bytes.Buffer{}
118	err = t.Execute(buf, b.templateObject)
119	if err != nil {
120		return "", err
121	}
122	return buf.String(), nil
123}
124
125func (b *Bubble) glamourize(md string) (string, error) {
126	// TODO: read gaps in appropriate style to remove the magic number below.
127	w := b.width - b.widthMargin - 2
128	if w > glamourMaxWidth {
129		w = glamourMaxWidth
130	}
131	tr, err := glamour.NewTermRenderer(
132		glamour.WithStandardStyle("dark"),
133		glamour.WithWordWrap(w),
134	)
135
136	if err != nil {
137		return "", err
138	}
139	mdt, err := tr.Render(md)
140	if err != nil {
141		return "", err
142	}
143	// Enforce a maximum width for cases when glamour lines run long.
144	//
145	// TODO: use Reflow's unconditional wrapping to force-wrap long lines. This
146	// should utlimately happen as a Glamour option.
147	//
148	// See:
149	// https://github.com/muesli/reflow#unconditional-wrapping
150	mdt = lipgloss.NewStyle().MaxWidth(w).Render(mdt)
151	return mdt, nil
152}