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