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/lipgloss"
 12	"github.com/charmbracelet/soft-serve/ui/common"
 13	vp "github.com/charmbracelet/soft-serve/ui/components/viewport"
 14	"github.com/muesli/reflow/wrap"
 15	"github.com/muesli/termenv"
 16)
 17
 18// Code is a code snippet.
 19type Code struct {
 20	common         common.Common
 21	content        string
 22	extension      string
 23	viewport       *vp.ViewportBubble
 24	NoContentStyle lipgloss.Style
 25}
 26
 27// New returns a new Code.
 28func New(c common.Common, content, extension string) *Code {
 29	r := &Code{
 30		common:    c,
 31		content:   content,
 32		extension: extension,
 33		viewport: &vp.ViewportBubble{
 34			Viewport: &viewport.Model{
 35				MouseWheelEnabled: true,
 36			},
 37		},
 38		NoContentStyle: c.Styles.CodeNoContent.Copy(),
 39	}
 40	r.SetSize(c.Width, c.Height)
 41	return r
 42}
 43
 44// SetSize implements common.Component.
 45func (r *Code) SetSize(width, height int) {
 46	r.common.SetSize(width, height)
 47	r.viewport.SetSize(width, height)
 48}
 49
 50// SetContent sets the content of the Code.
 51func (r *Code) SetContent(c, ext string) tea.Cmd {
 52	r.content = c
 53	r.extension = ext
 54	return r.Init()
 55}
 56
 57// GotoTop reset the viewport to the top.
 58func (r *Code) GotoTop() {
 59	r.viewport.Viewport.GotoTop()
 60}
 61
 62// Init implements tea.Model.
 63func (r *Code) Init() tea.Cmd {
 64	w := r.common.Width
 65	c := r.content
 66	if c == "" {
 67		c = r.NoContentStyle.String()
 68	}
 69	f, err := renderFile(r.extension, c, w)
 70	if err != nil {
 71		return common.ErrorCmd(err)
 72	}
 73	// FIXME: this is a hack to reset formatting at the end of every line.
 74	c = wrap.String(f, w)
 75	s := strings.Split(c, "\n")
 76	for i, l := range s {
 77		s[i] = l + "\x1b[0m"
 78	}
 79	r.viewport.Viewport.SetContent(strings.Join(s, "\n"))
 80	return nil
 81}
 82
 83// Update implements tea.Model.
 84func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 85	cmds := make([]tea.Cmd, 0)
 86	switch msg.(type) {
 87	case tea.WindowSizeMsg:
 88		// Recalculate content width and line wrap.
 89		cmds = append(cmds, r.Init())
 90	}
 91	v, cmd := r.viewport.Update(msg)
 92	r.viewport = v.(*vp.ViewportBubble)
 93	if cmd != nil {
 94		cmds = append(cmds, cmd)
 95	}
 96	return r, tea.Batch(cmds...)
 97}
 98
 99// View implements tea.View.
100func (r *Code) View() string {
101	return r.viewport.View()
102}
103
104func styleConfig() gansi.StyleConfig {
105	noColor := ""
106	s := glamour.DarkStyleConfig
107	s.Document.StylePrimitive.Color = &noColor
108	s.CodeBlock.Chroma.Text.Color = &noColor
109	s.CodeBlock.Chroma.Name.Color = &noColor
110	return s
111}
112
113func renderCtx() gansi.RenderContext {
114	return gansi.NewRenderContext(gansi.Options{
115		ColorProfile: termenv.TrueColor,
116		Styles:       styleConfig(),
117	})
118}
119
120func glamourize(w int, md string) (string, error) {
121	if w > 120 {
122		w = 120
123	}
124	tr, err := glamour.NewTermRenderer(
125		glamour.WithStyles(styleConfig()),
126		glamour.WithWordWrap(w),
127	)
128
129	if err != nil {
130		return "", err
131	}
132	mdt, err := tr.Render(md)
133	if err != nil {
134		return "", err
135	}
136	return mdt, nil
137}
138
139func renderFile(path, content string, width int) (string, error) {
140	lexer := lexers.Fallback
141	if path == "" {
142		lexer = lexers.Analyse(content)
143	} else {
144		lexer = lexers.Match(path)
145	}
146	lang := ""
147	if lexer != nil && lexer.Config() != nil {
148		lang = lexer.Config().Name
149	}
150	if lang == "markdown" {
151		md, err := glamourize(width, content)
152		if err != nil {
153			return "", err
154		}
155		return md, nil
156	}
157	formatter := &gansi.CodeBlockElement{
158		Code:     content,
159		Language: lang,
160	}
161	r := strings.Builder{}
162	err := formatter.Render(&r, renderCtx())
163	if err != nil {
164		return "", err
165	}
166	return r.String(), nil
167}