1package code
2
3import (
4 "strings"
5
6 "github.com/alecthomas/chroma/lexers"
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/glamour"
9 gansi "github.com/charmbracelet/glamour/ansi"
10 "github.com/charmbracelet/lipgloss"
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
17// Code is a code snippet.
18type Code struct {
19 *vp.Viewport
20 common common.Common
21 content string
22 extension string
23 NoContentStyle lipgloss.Style
24}
25
26// New returns a new Code.
27func New(c common.Common, content, extension string) *Code {
28 r := &Code{
29 common: c,
30 content: content,
31 extension: extension,
32 Viewport: vp.New(c),
33 NoContentStyle: c.Styles.CodeNoContent.Copy(),
34 }
35 r.SetSize(c.Width, c.Height)
36 return r
37}
38
39// SetSize implements common.Component.
40func (r *Code) SetSize(width, height int) {
41 r.common.SetSize(width, height)
42 r.Viewport.SetSize(width, height)
43}
44
45// SetContent sets the content of the Code.
46func (r *Code) SetContent(c, ext string) tea.Cmd {
47 r.content = c
48 r.extension = ext
49 return r.Init()
50}
51
52// Init implements tea.Model.
53func (r *Code) Init() tea.Cmd {
54 w := r.common.Width
55 c := r.content
56 if c == "" {
57 c = r.NoContentStyle.String()
58 }
59 f, err := renderFile(r.extension, c, w)
60 if err != nil {
61 return common.ErrorCmd(err)
62 }
63 // FIXME: this is a hack to reset formatting at the end of every line.
64 c = wrap.String(f, w)
65 s := strings.Split(c, "\n")
66 for i, l := range s {
67 s[i] = l + "\x1b[0m"
68 }
69 r.Viewport.Model.SetContent(strings.Join(s, "\n"))
70 return nil
71}
72
73// Update implements tea.Model.
74func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
75 cmds := make([]tea.Cmd, 0)
76 switch msg.(type) {
77 case tea.WindowSizeMsg:
78 // Recalculate content width and line wrap.
79 cmds = append(cmds, r.Init())
80 }
81 v, cmd := r.Viewport.Update(msg)
82 r.Viewport = v.(*vp.Viewport)
83 if cmd != nil {
84 cmds = append(cmds, cmd)
85 }
86 return r, tea.Batch(cmds...)
87}
88
89// View implements tea.View.
90func (r *Code) View() string {
91 return r.Viewport.View()
92}
93
94// GotoTop moves the viewport to the top of the log.
95func (r *Code) GotoTop() {
96 r.Viewport.GotoTop()
97}
98
99// GotoBottom moves the viewport to the bottom of the log.
100func (r *Code) GotoBottom() {
101 r.Viewport.GotoBottom()
102}
103
104// HalfViewDown moves the viewport down by half the viewport height.
105func (r *Code) HalfViewDown() {
106 r.Viewport.HalfViewDown()
107}
108
109// HalfViewUp moves the viewport up by half the viewport height.
110func (r *Code) HalfViewUp() {
111 r.Viewport.HalfViewUp()
112}
113
114// ViewUp moves the viewport up by a page.
115func (r *Code) ViewUp() []string {
116 return r.Viewport.ViewUp()
117}
118
119// ViewDown moves the viewport down by a page.
120func (r *Code) ViewDown() []string {
121 return r.Viewport.ViewDown()
122}
123
124// LineUp moves the viewport up by the given number of lines.
125func (r *Code) LineUp(n int) []string {
126 return r.Viewport.LineUp(n)
127}
128
129// LineDown moves the viewport down by the given number of lines.
130func (r *Code) LineDown(n int) []string {
131 return r.Viewport.LineDown(n)
132}
133
134// ScrollPercent returns the viewport's scroll percentage.
135func (r *Code) ScrollPercent() float64 {
136 return r.Viewport.ScrollPercent()
137}
138
139func styleConfig() gansi.StyleConfig {
140 noColor := ""
141 s := glamour.DarkStyleConfig
142 s.Document.StylePrimitive.Color = &noColor
143 s.CodeBlock.Chroma.Text.Color = &noColor
144 s.CodeBlock.Chroma.Name.Color = &noColor
145 return s
146}
147
148func renderCtx() gansi.RenderContext {
149 return gansi.NewRenderContext(gansi.Options{
150 ColorProfile: termenv.TrueColor,
151 Styles: styleConfig(),
152 })
153}
154
155func glamourize(w int, md string) (string, error) {
156 if w > 120 {
157 w = 120
158 }
159 tr, err := glamour.NewTermRenderer(
160 glamour.WithStyles(styleConfig()),
161 glamour.WithWordWrap(w),
162 )
163
164 if err != nil {
165 return "", err
166 }
167 mdt, err := tr.Render(md)
168 if err != nil {
169 return "", err
170 }
171 return mdt, nil
172}
173
174func renderFile(path, content string, width int) (string, error) {
175 lexer := lexers.Fallback
176 if path == "" {
177 lexer = lexers.Analyse(content)
178 } else {
179 lexer = lexers.Match(path)
180 }
181 lang := ""
182 if lexer != nil && lexer.Config() != nil {
183 lang = lexer.Config().Name
184 }
185 if lang == "markdown" {
186 md, err := glamourize(width, content)
187 if err != nil {
188 return "", err
189 }
190 return md, nil
191 }
192 formatter := &gansi.CodeBlockElement{
193 Code: content,
194 Language: lang,
195 }
196 r := strings.Builder{}
197 err := formatter.Render(&r, renderCtx())
198 if err != nil {
199 return "", err
200 }
201 return r.String(), nil
202}