1package repo
2
3import (
4 "bytes"
5 "smoothie/git"
6 "text/template"
7
8 "github.com/charmbracelet/bubbles/viewport"
9 tea "github.com/charmbracelet/bubbletea"
10 "github.com/charmbracelet/glamour"
11 "github.com/charmbracelet/lipgloss"
12)
13
14const glamourMaxWidth = 120
15
16type ErrMsg struct {
17 Error error
18}
19
20type Bubble struct {
21 templateObject interface{}
22 repoSource *git.RepoSource
23 name string
24 repo *git.Repo
25 readmeViewport *ViewportBubble
26 readme string
27 height int
28 heightMargin int
29 width int
30 widthMargin int
31}
32
33func NewBubble(rs *git.RepoSource, name string, width, wm, height, hm int, tmp interface{}) *Bubble {
34 b := &Bubble{
35 templateObject: tmp,
36 repoSource: rs,
37 name: name,
38 heightMargin: hm,
39 widthMargin: wm,
40 readmeViewport: &ViewportBubble{
41 Viewport: &viewport.Model{},
42 },
43 }
44 b.SetSize(width, height)
45 return b
46}
47
48func (b *Bubble) Init() tea.Cmd {
49 return b.setupCmd
50}
51
52func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
53 var cmds []tea.Cmd
54 switch msg := msg.(type) {
55 case tea.WindowSizeMsg:
56 b.SetSize(msg.Width, msg.Height)
57 // XXX: if we find that longer readmes take more than a few
58 // milliseconds to render we may need to move Glamour rendering into a
59 // command.
60 md, err := b.glamourize(b.readme)
61 if err != nil {
62 return b, nil
63 }
64 b.readmeViewport.Viewport.SetContent(md)
65 }
66 rv, cmd := b.readmeViewport.Update(msg)
67 b.readmeViewport = rv.(*ViewportBubble)
68 cmds = append(cmds, cmd)
69 return b, tea.Batch(cmds...)
70}
71
72func (b *Bubble) SetSize(w, h int) {
73 b.width = w
74 b.height = h
75 b.readmeViewport.Viewport.Width = w - b.widthMargin
76 b.readmeViewport.Viewport.Height = h - b.heightMargin
77}
78
79func (b *Bubble) GotoTop() {
80 b.readmeViewport.Viewport.GotoTop()
81}
82
83func (b *Bubble) View() string {
84 return b.readmeViewport.View()
85}
86
87func (b *Bubble) setupCmd() tea.Msg {
88 r, err := b.repoSource.GetRepo(b.name)
89 if err == git.ErrMissingRepo {
90 return nil
91 }
92 if err != nil {
93 return ErrMsg{err}
94 }
95 md := r.Readme
96 if b.templateObject != nil {
97 md, err = b.templatize(md)
98 if err != nil {
99 return ErrMsg{err}
100 }
101 }
102 b.readme = md
103 md, err = b.glamourize(md)
104 if err != nil {
105 return ErrMsg{err}
106 }
107 b.readmeViewport.Viewport.SetContent(md)
108 b.GotoTop()
109 return nil
110}
111
112func (b *Bubble) templatize(mdt string) (string, error) {
113 t, err := template.New("readme").Parse(mdt)
114 if err != nil {
115 return "", err
116 }
117 buf := &bytes.Buffer{}
118 err = t.Execute(buf, b.templateObject)
119 if err != nil {
120 return "", err
121 }
122 return buf.String(), nil
123}
124
125func (b *Bubble) glamourize(md string) (string, error) {
126 // TODO: read gaps in appropriate style to remove the magic number below.
127 w := b.width - b.widthMargin - 2
128 if w > glamourMaxWidth {
129 w = glamourMaxWidth
130 }
131 tr, err := glamour.NewTermRenderer(
132 glamour.WithStandardStyle("dark"),
133 glamour.WithWordWrap(w),
134 )
135
136 if err != nil {
137 return "", err
138 }
139 mdt, err := tr.Render(md)
140 if err != nil {
141 return "", err
142 }
143 // Enforce a maximum width for cases when glamour lines run long.
144 //
145 // TODO: use Reflow's unconditional wrapping to force-wrap long lines. This
146 // should utlimately happen as a Glamour option.
147 //
148 // See:
149 // https://github.com/muesli/reflow#unconditional-wrapping
150 mdt = lipgloss.NewStyle().MaxWidth(w).Render(mdt)
151 return mdt, nil
152}