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/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
17type Code struct {
18 common common.Common
19 content string
20 extension string
21 viewport *vp.ViewportBubble
22}
23
24func New(c common.Common, content, extension string) *Code {
25 r := &Code{
26 common: c,
27 content: content,
28 extension: extension,
29 viewport: &vp.ViewportBubble{
30 Viewport: &viewport.Model{
31 MouseWheelEnabled: true,
32 },
33 },
34 }
35 return r
36}
37
38func (r *Code) SetSize(width, height int) {
39 r.common.Width = width
40 r.common.Height = height
41 r.viewport.SetSize(width, height)
42}
43
44func (r *Code) SetContent(c, ext string) tea.Cmd {
45 r.content = c
46 r.extension = ext
47 return r.Init()
48}
49
50func (r *Code) Init() tea.Cmd {
51 w := r.common.Width
52 s := r.common.Styles
53 c := r.content
54 if c == "" {
55 c = s.AboutNoReadme.Render("File is empty.")
56 }
57 f, err := renderFile(r.extension, c, w)
58 if err != nil {
59 return common.ErrorCmd(err)
60 }
61 c = wrap.String(f, w)
62 r.viewport.Viewport.SetContent(c)
63 return nil
64}
65
66func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
67 v, cmd := r.viewport.Update(msg)
68 r.viewport = v.(*vp.ViewportBubble)
69 return r, cmd
70}
71
72func (r *Code) View() string {
73 return r.viewport.View()
74}
75
76func styleConfig() gansi.StyleConfig {
77 noColor := ""
78 s := glamour.DarkStyleConfig
79 s.Document.StylePrimitive.Color = &noColor
80 s.CodeBlock.Chroma.Text.Color = &noColor
81 s.CodeBlock.Chroma.Name.Color = &noColor
82 return s
83}
84
85func renderCtx() gansi.RenderContext {
86 return gansi.NewRenderContext(gansi.Options{
87 ColorProfile: termenv.TrueColor,
88 Styles: styleConfig(),
89 })
90}
91
92func glamourize(w int, md string) (string, error) {
93 if w > 120 {
94 w = 120
95 }
96 tr, err := glamour.NewTermRenderer(
97 glamour.WithStyles(styleConfig()),
98 glamour.WithWordWrap(w),
99 )
100
101 if err != nil {
102 return "", err
103 }
104 mdt, err := tr.Render(md)
105 if err != nil {
106 return "", err
107 }
108 return mdt, nil
109}
110
111func renderFile(path, content string, width int) (string, error) {
112 lexer := lexers.Fallback
113 if path == "" {
114 lexer = lexers.Analyse(content)
115 } else {
116 lexer = lexers.Match(path)
117 }
118 lang := ""
119 if lexer != nil && lexer.Config() != nil {
120 lang = lexer.Config().Name
121 }
122 if lang == "markdown" {
123 md, err := glamourize(width, content)
124 if err != nil {
125 return "", err
126 }
127 return md, nil
128 }
129 formatter := &gansi.CodeBlockElement{
130 Code: content,
131 Language: lang,
132 }
133 r := strings.Builder{}
134 err := formatter.Render(&r, renderCtx())
135 if err != nil {
136 return "", err
137 }
138 return r.String(), nil
139}