1package code
2
3import (
4 "strings"
5 "sync"
6
7 "github.com/alecthomas/chroma/lexers"
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 *vp.Viewport
21 common common.Common
22 content string
23 extension string
24 renderContext gansi.RenderContext
25 renderMutex sync.Mutex
26 styleConfig gansi.StyleConfig
27 NoContentStyle lipgloss.Style
28}
29
30// New returns a new Code.
31func New(c common.Common, content, extension string) *Code {
32 r := &Code{
33 common: c,
34 content: content,
35 extension: extension,
36 Viewport: vp.New(c),
37 NoContentStyle: c.Styles.CodeNoContent.Copy(),
38 }
39 st := common.StyleConfig()
40 r.styleConfig = st
41 r.renderContext = gansi.NewRenderContext(gansi.Options{
42 ColorProfile: termenv.TrueColor,
43 Styles: st,
44 })
45 r.SetSize(c.Width, c.Height)
46 return r
47}
48
49// SetSize implements common.Component.
50func (r *Code) SetSize(width, height int) {
51 r.common.SetSize(width, height)
52 r.Viewport.SetSize(width, height)
53}
54
55// SetContent sets the content of the Code.
56func (r *Code) SetContent(c, ext string) tea.Cmd {
57 r.content = c
58 r.extension = ext
59 return r.Init()
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 := r.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.Model.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.Viewport)
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
104// GotoTop moves the viewport to the top of the log.
105func (r *Code) GotoTop() {
106 r.Viewport.GotoTop()
107}
108
109// GotoBottom moves the viewport to the bottom of the log.
110func (r *Code) GotoBottom() {
111 r.Viewport.GotoBottom()
112}
113
114// HalfViewDown moves the viewport down by half the viewport height.
115func (r *Code) HalfViewDown() {
116 r.Viewport.HalfViewDown()
117}
118
119// HalfViewUp moves the viewport up by half the viewport height.
120func (r *Code) HalfViewUp() {
121 r.Viewport.HalfViewUp()
122}
123
124// ViewUp moves the viewport up by a page.
125func (r *Code) ViewUp() []string {
126 return r.Viewport.ViewUp()
127}
128
129// ViewDown moves the viewport down by a page.
130func (r *Code) ViewDown() []string {
131 return r.Viewport.ViewDown()
132}
133
134// LineUp moves the viewport up by the given number of lines.
135func (r *Code) LineUp(n int) []string {
136 return r.Viewport.LineUp(n)
137}
138
139// LineDown moves the viewport down by the given number of lines.
140func (r *Code) LineDown(n int) []string {
141 return r.Viewport.LineDown(n)
142}
143
144// ScrollPercent returns the viewport's scroll percentage.
145func (r *Code) ScrollPercent() float64 {
146 return r.Viewport.ScrollPercent()
147}
148
149func (r *Code) glamourize(w int, md string) (string, error) {
150 r.renderMutex.Lock()
151 defer r.renderMutex.Unlock()
152 if w > 120 {
153 w = 120
154 }
155 tr, err := glamour.NewTermRenderer(
156 glamour.WithStyles(r.styleConfig),
157 glamour.WithWordWrap(w),
158 )
159
160 if err != nil {
161 return "", err
162 }
163 mdt, err := tr.Render(md)
164 if err != nil {
165 return "", err
166 }
167 return mdt, nil
168}
169
170func (r *Code) renderFile(path, content string, width int) (string, error) {
171 lexer := lexers.Fallback
172 if path == "" {
173 lexer = lexers.Analyse(content)
174 } else {
175 lexer = lexers.Match(path)
176 }
177 lang := ""
178 if lexer != nil && lexer.Config() != nil {
179 lang = lexer.Config().Name
180 }
181 if lang == "markdown" {
182 md, err := r.glamourize(width, content)
183 if err != nil {
184 return "", err
185 }
186 return md, nil
187 }
188 formatter := &gansi.CodeBlockElement{
189 Code: content,
190 Language: lang,
191 }
192 s := strings.Builder{}
193 err := formatter.Render(&s, r.renderContext)
194 if err != nil {
195 return "", err
196 }
197 return s.String(), nil
198}