code.go

  1package code
  2
  3import (
  4	"strings"
  5
  6	"github.com/alecthomas/chroma/lexers"
  7	"github.com/charmbracelet/bubbles/viewport"
  8	tea "github.com/charmbracelet/bubbletea"
  9	"github.com/charmbracelet/glamour"
 10	gansi "github.com/charmbracelet/glamour/ansi"
 11	"github.com/charmbracelet/soft-serve/ui/common"
 12	vp "github.com/charmbracelet/soft-serve/ui/components/viewport"
 13	"github.com/muesli/reflow/wrap"
 14	"github.com/muesli/termenv"
 15)
 16
 17type Code struct {
 18	common    common.Common
 19	content   string
 20	extension string
 21	viewport  *vp.ViewportBubble
 22}
 23
 24func New(c common.Common, content, extension string) *Code {
 25	r := &Code{
 26		common:    c,
 27		content:   content,
 28		extension: extension,
 29		viewport: &vp.ViewportBubble{
 30			Viewport: &viewport.Model{
 31				MouseWheelEnabled: true,
 32			},
 33		},
 34	}
 35	return r
 36}
 37
 38func (r *Code) SetSize(width, height int) {
 39	r.common.Width = width
 40	r.common.Height = height
 41	r.viewport.SetSize(width, height)
 42}
 43
 44func (r *Code) SetContent(c, ext string) tea.Cmd {
 45	r.content = c
 46	r.extension = ext
 47	return r.Init()
 48}
 49
 50func (r *Code) Init() tea.Cmd {
 51	w := r.common.Width
 52	s := r.common.Styles
 53	c := r.content
 54	if c == "" {
 55		c = s.AboutNoReadme.Render("File is empty.")
 56	}
 57	f, err := renderFile(r.extension, c, w)
 58	if err != nil {
 59		return common.ErrorCmd(err)
 60	}
 61	c = wrap.String(f, w)
 62	r.viewport.Viewport.SetContent(c)
 63	return nil
 64}
 65
 66func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 67	v, cmd := r.viewport.Update(msg)
 68	r.viewport = v.(*vp.ViewportBubble)
 69	return r, cmd
 70}
 71
 72func (r *Code) View() string {
 73	return r.viewport.View()
 74}
 75
 76func styleConfig() gansi.StyleConfig {
 77	noColor := ""
 78	s := glamour.DarkStyleConfig
 79	s.Document.StylePrimitive.Color = &noColor
 80	s.CodeBlock.Chroma.Text.Color = &noColor
 81	s.CodeBlock.Chroma.Name.Color = &noColor
 82	return s
 83}
 84
 85func renderCtx() gansi.RenderContext {
 86	return gansi.NewRenderContext(gansi.Options{
 87		ColorProfile: termenv.TrueColor,
 88		Styles:       styleConfig(),
 89	})
 90}
 91
 92func glamourize(w int, md string) (string, error) {
 93	if w > 120 {
 94		w = 120
 95	}
 96	tr, err := glamour.NewTermRenderer(
 97		glamour.WithStyles(styleConfig()),
 98		glamour.WithWordWrap(w),
 99	)
100
101	if err != nil {
102		return "", err
103	}
104	mdt, err := tr.Render(md)
105	if err != nil {
106		return "", err
107	}
108	return mdt, nil
109}
110
111func renderFile(path, content string, width int) (string, error) {
112	lexer := lexers.Fallback
113	if path == "" {
114		lexer = lexers.Analyse(content)
115	} else {
116		lexer = lexers.Match(path)
117	}
118	lang := ""
119	if lexer != nil && lexer.Config() != nil {
120		lang = lexer.Config().Name
121	}
122	if lang == "markdown" {
123		md, err := glamourize(width, content)
124		if err != nil {
125			return "", err
126		}
127		return md, nil
128	}
129	formatter := &gansi.CodeBlockElement{
130		Code:     content,
131		Language: lang,
132	}
133	r := strings.Builder{}
134	err := formatter.Render(&r, renderCtx())
135	if err != nil {
136		return "", err
137	}
138	return r.String(), nil
139}