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