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