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 c = wrap.String(f, w)
74 r.viewport.Viewport.SetContent(c)
75 return nil
76}
77
78// Update implements tea.Model.
79func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
80 v, cmd := r.viewport.Update(msg)
81 r.viewport = v.(*vp.ViewportBubble)
82 return r, cmd
83}
84
85// View implements tea.View.
86func (r *Code) View() string {
87 return r.viewport.View()
88}
89
90func styleConfig() gansi.StyleConfig {
91 noColor := ""
92 s := glamour.DarkStyleConfig
93 s.Document.StylePrimitive.Color = &noColor
94 s.CodeBlock.Chroma.Text.Color = &noColor
95 s.CodeBlock.Chroma.Name.Color = &noColor
96 return s
97}
98
99func renderCtx() gansi.RenderContext {
100 return gansi.NewRenderContext(gansi.Options{
101 ColorProfile: termenv.TrueColor,
102 Styles: styleConfig(),
103 })
104}
105
106func glamourize(w int, md string) (string, error) {
107 if w > 120 {
108 w = 120
109 }
110 tr, err := glamour.NewTermRenderer(
111 glamour.WithStyles(styleConfig()),
112 glamour.WithWordWrap(w),
113 )
114
115 if err != nil {
116 return "", err
117 }
118 mdt, err := tr.Render(md)
119 if err != nil {
120 return "", err
121 }
122 return mdt, nil
123}
124
125func renderFile(path, content string, width int) (string, error) {
126 lexer := lexers.Fallback
127 if path == "" {
128 lexer = lexers.Analyse(content)
129 } else {
130 lexer = lexers.Match(path)
131 }
132 lang := ""
133 if lexer != nil && lexer.Config() != nil {
134 lang = lexer.Config().Name
135 }
136 if lang == "markdown" {
137 md, err := glamourize(width, content)
138 if err != nil {
139 return "", err
140 }
141 return md, nil
142 }
143 formatter := &gansi.CodeBlockElement{
144 Code: content,
145 Language: lang,
146 }
147 r := strings.Builder{}
148 err := formatter.Render(&r, renderCtx())
149 if err != nil {
150 return "", err
151 }
152 return r.String(), nil
153}