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