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