feat: render markdown files using glamour

Ayman Bagabas created

Fixes: https://github.com/charmbracelet/soft-serve/issues/87

Change summary

internal/tui/bubbles/git/about/bubble.go    | 38 ++++------------------
internal/tui/bubbles/git/log/bubble.go      |  6 +-
internal/tui/bubbles/git/refs/bubble.go     |  6 +-
internal/tui/bubbles/git/tree/bubble.go     | 26 ++++++++++----
internal/tui/bubbles/git/types/formatter.go | 27 ++++++++++++++++
5 files changed, 58 insertions(+), 45 deletions(-)

Detailed changes

internal/tui/bubbles/git/about/bubble.go 🔗

@@ -3,11 +3,9 @@ package about
 import (
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
-	"github.com/charmbracelet/glamour"
 	"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
 	vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
 	"github.com/charmbracelet/soft-serve/internal/tui/style"
-	"github.com/muesli/reflow/wrap"
 )
 
 type Bubble struct {
@@ -45,7 +43,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		// XXX: if we find that longer readmes take more than a few
 		// milliseconds to render we may need to move Glamour rendering into a
 		// command.
-		md, err := b.glamourize(b.repo.GetReadme())
+		md, err := b.glamourize()
 		if err != nil {
 			return b, nil
 		}
@@ -81,8 +79,13 @@ func (b *Bubble) Help() []types.HelpEntry {
 	return nil
 }
 
+func (b *Bubble) glamourize() (string, error) {
+	w := b.width - b.widthMargin - b.styles.RepoBody.GetHorizontalFrameSize()
+	return types.Glamourize(w, b.repo.GetReadme())
+}
+
 func (b *Bubble) setupCmd() tea.Msg {
-	md, err := b.glamourize(b.repo.GetReadme())
+	md, err := b.glamourize()
 	if err != nil {
 		return types.ErrMsg{err}
 	}
@@ -90,30 +93,3 @@ func (b *Bubble) setupCmd() tea.Msg {
 	b.GotoTop()
 	return nil
 }
-
-func (b *Bubble) glamourize(md string) (string, error) {
-	w := b.width - b.widthMargin - b.styles.RepoBody.GetHorizontalFrameSize()
-	if w > types.GlamourMaxWidth {
-		w = types.GlamourMaxWidth
-	}
-	tr, err := glamour.NewTermRenderer(
-		glamour.WithStyles(types.DefaultStyles()),
-		glamour.WithWordWrap(w),
-	)
-
-	if err != nil {
-		return "", err
-	}
-	mdt, err := tr.Render(md)
-	if err != nil {
-		return "", err
-	}
-	// For now, hard-wrap long lines in Glamour that would otherwise break the
-	// layout when wrapping. This may be due to #43 in Reflow, which has to do
-	// with a bug in the way lines longer than the given width are wrapped.
-	//
-	//     https://github.com/muesli/reflow/issues/43
-	//
-	// TODO: solve this upstream in Glamour/Reflow.
-	return wrap.String(mdt, w), nil
-}

internal/tui/bubbles/git/log/bubble.go 🔗

@@ -99,8 +99,8 @@ type Bubble struct {
 	error          types.ErrMsg
 }
 
-func NewBubble(repo types.Repo, style *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
-	l := list.New([]list.Item{}, itemDelegate{style}, width-widthMargin, height-heightMargin)
+func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
+	l := list.New([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
 	l.SetShowFilter(false)
 	l.SetShowHelp(false)
 	l.SetShowPagination(false)
@@ -115,7 +115,7 @@ func NewBubble(repo types.Repo, style *style.Styles, width, widthMargin, height,
 			Viewport: &viewport.Model{},
 		},
 		repo:         repo,
-		style:        style,
+		style:        styles,
 		state:        logState,
 		width:        width,
 		widthMargin:  widthMargin,

internal/tui/bubbles/git/refs/bubble.go 🔗

@@ -75,8 +75,8 @@ type Bubble struct {
 	heightMargin int
 }
 
-func NewBubble(repo types.Repo, style *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
-	l := list.NewModel([]list.Item{}, itemDelegate{style}, width-widthMargin, height-heightMargin)
+func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
+	l := list.NewModel([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
 	l.SetShowFilter(false)
 	l.SetShowHelp(false)
 	l.SetShowPagination(false)
@@ -86,7 +86,7 @@ func NewBubble(repo types.Repo, style *style.Styles, width, widthMargin, height,
 	l.DisableQuitKeybindings()
 	b := &Bubble{
 		repo:         repo,
-		style:        style,
+		style:        styles,
 		width:        width,
 		height:       height,
 		widthMargin:  widthMargin,

internal/tui/bubbles/git/tree/bubble.go 🔗

@@ -130,8 +130,8 @@ type Bubble struct {
 	lastSelected []int
 }
 
-func NewBubble(repo types.Repo, style *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
-	l := list.New([]list.Item{}, itemDelegate{style}, width-widthMargin, height-heightMargin)
+func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
+	l := list.New([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
 	l.SetShowFilter(false)
 	l.SetShowHelp(false)
 	l.SetShowPagination(false)
@@ -146,7 +146,7 @@ func NewBubble(repo types.Repo, style *style.Styles, width, widthMargin, height,
 			Viewport: &viewport.Model{},
 		},
 		repo:         repo,
-		style:        style,
+		style:        styles,
 		width:        width,
 		height:       height,
 		widthMargin:  widthMargin,
@@ -330,12 +330,22 @@ func (b *Bubble) renderFile(m fileMsg) string {
 			Code:     c,
 			Language: lang,
 		}
-		r := strings.Builder{}
-		err := formatter.Render(&r, types.RenderCtx)
-		if err != nil {
-			s.WriteString(err.Error())
+		if lang == "markdown" {
+			w := b.width - b.widthMargin - b.style.RepoBody.GetHorizontalFrameSize()
+			md, err := types.Glamourize(w, c)
+			if err != nil {
+				s.WriteString(err.Error())
+			} else {
+				s.WriteString(md)
+			}
 		} else {
-			s.WriteString(r.String())
+			r := strings.Builder{}
+			err := formatter.Render(&r, types.RenderCtx)
+			if err != nil {
+				s.WriteString(err.Error())
+			} else {
+				s.WriteString(r.String())
+			}
 		}
 	}
 	return b.style.TreeFileContent.Copy().Width(b.width - b.widthMargin).Render(s.String())

internal/tui/bubbles/git/types/formatter.go 🔗

@@ -3,6 +3,7 @@ package types
 import (
 	"github.com/charmbracelet/glamour"
 	gansi "github.com/charmbracelet/glamour/ansi"
+	"github.com/muesli/reflow/wrap"
 	"github.com/muesli/termenv"
 )
 
@@ -34,3 +35,29 @@ func NewRenderCtx(worldwrap int) gansi.RenderContext {
 		WordWrap:     worldwrap,
 	})
 }
+
+func Glamourize(w int, md string) (string, error) {
+	if w > GlamourMaxWidth {
+		w = GlamourMaxWidth
+	}
+	tr, err := glamour.NewTermRenderer(
+		glamour.WithStyles(DefaultStyles()),
+		glamour.WithWordWrap(w),
+	)
+
+	if err != nil {
+		return "", err
+	}
+	mdt, err := tr.Render(md)
+	if err != nil {
+		return "", err
+	}
+	// For now, hard-wrap long lines in Glamour that would otherwise break the
+	// layout when wrapping. This may be due to #43 in Reflow, which has to do
+	// with a bug in the way lines longer than the given width are wrapped.
+	//
+	//     https://github.com/muesli/reflow/issues/43
+	//
+	// TODO: solve this upstream in Glamour/Reflow.
+	return wrap.String(mdt, w), nil
+}