1package repo
2
3import (
4 "bytes"
5 "fmt"
6 "smoothie/git"
7 "smoothie/tui/style"
8 "strconv"
9 "text/template"
10
11 "github.com/charmbracelet/bubbles/viewport"
12 tea "github.com/charmbracelet/bubbletea"
13 "github.com/charmbracelet/glamour"
14 "github.com/charmbracelet/lipgloss"
15 "github.com/muesli/reflow/wordwrap"
16 "github.com/muesli/reflow/wrap"
17)
18
19const glamourMaxWidth = 120
20
21type ErrMsg struct {
22 Error error
23}
24
25type Bubble struct {
26 templateObject interface{}
27 repoSource *git.RepoSource
28 name string
29 repo *git.Repo
30 styles *style.Styles
31 readmeViewport *ViewportBubble
32 readme string
33 height int
34 heightMargin int
35 width int
36 widthMargin int
37 Active bool
38
39 // XXX: ideally, we get these from the parent as a pointer. Currently, we
40 // can't add a *tui.Config because it's an illegal import cycle. One
41 // solution would be to (rename and) move this Bubble into the parent
42 // package.
43 Host string
44 Port int64
45}
46
47func NewBubble(rs *git.RepoSource, name string, styles *style.Styles, width, wm, height, hm int, tmp interface{}) *Bubble {
48 b := &Bubble{
49 templateObject: tmp,
50 repoSource: rs,
51 name: name,
52 styles: styles,
53 heightMargin: hm,
54 widthMargin: wm,
55 readmeViewport: &ViewportBubble{
56 Viewport: &viewport.Model{},
57 },
58 }
59 b.SetSize(width, height)
60 return b
61}
62
63func (b *Bubble) Init() tea.Cmd {
64 return b.setupCmd
65}
66
67func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
68 var cmds []tea.Cmd
69 switch msg := msg.(type) {
70 case tea.WindowSizeMsg:
71 b.SetSize(msg.Width, msg.Height)
72 // XXX: if we find that longer readmes take more than a few
73 // milliseconds to render we may need to move Glamour rendering into a
74 // command.
75 md, err := b.glamourize(b.readme)
76 if err != nil {
77 return b, nil
78 }
79 b.readmeViewport.Viewport.SetContent(md)
80 }
81 rv, cmd := b.readmeViewport.Update(msg)
82 b.readmeViewport = rv.(*ViewportBubble)
83 cmds = append(cmds, cmd)
84 return b, tea.Batch(cmds...)
85}
86
87func (b *Bubble) SetSize(w, h int) {
88 b.width = w
89 b.height = h
90 b.readmeViewport.Viewport.Width = w - b.widthMargin
91 b.readmeViewport.Viewport.Height = h - b.heightMargin
92}
93
94func (b *Bubble) GotoTop() {
95 b.readmeViewport.Viewport.GotoTop()
96}
97
98func (b Bubble) headerView() string {
99 ts := b.styles.RepoTitle
100 ns := b.styles.RepoNote
101 if b.Active {
102 ts = ts.Copy().BorderForeground(b.styles.ActiveBorderColor)
103 ns = ns.Copy().BorderForeground(b.styles.ActiveBorderColor)
104 }
105 var gc string
106 n := b.name
107 if n == "config" {
108 n = "Home"
109 } else {
110 gc = fmt.Sprintf("git clone %s", b.sshAddress())
111 }
112 title := ts.Render(n)
113 note := ns.Width(b.width - b.widthMargin - lipgloss.Width(title)).Render(gc)
114 return lipgloss.JoinHorizontal(lipgloss.Top, title, note)
115}
116
117func (b *Bubble) View() string {
118 header := b.headerView()
119 bs := b.styles.RepoBody.Copy()
120 if b.Active {
121 bs = bs.BorderForeground(b.styles.ActiveBorderColor)
122 }
123 body := bs.
124 Width(b.width - b.widthMargin - b.styles.RepoBody.GetVerticalFrameSize()).
125 Height(b.height - b.heightMargin - lipgloss.Height(header)).
126 Render(b.readmeViewport.View())
127 return header + body
128}
129
130func (b Bubble) sshAddress() string {
131 p := ":" + strconv.Itoa(int(b.Port))
132 if p == ":22" {
133 p = ""
134 }
135 return fmt.Sprintf("ssh://%s%s/%s", b.Host, p, b.name)
136}
137
138func (b *Bubble) setupCmd() tea.Msg {
139 r, err := b.repoSource.GetRepo(b.name)
140 if err == git.ErrMissingRepo {
141 return nil
142 }
143 if err != nil {
144 return ErrMsg{err}
145 }
146 md := r.Readme
147 if b.templateObject != nil {
148 md, err = b.templatize(md)
149 if err != nil {
150 return ErrMsg{err}
151 }
152 }
153 b.readme = md
154 md, err = b.glamourize(md)
155 if err != nil {
156 return ErrMsg{err}
157 }
158 b.readmeViewport.Viewport.SetContent(md)
159 b.GotoTop()
160 return nil
161}
162
163func (b *Bubble) templatize(mdt string) (string, error) {
164 t, err := template.New("readme").Parse(mdt)
165 if err != nil {
166 return "", err
167 }
168 buf := &bytes.Buffer{}
169 err = t.Execute(buf, b.templateObject)
170 if err != nil {
171 return "", err
172 }
173 return buf.String(), nil
174}
175
176func (b *Bubble) glamourize(md string) (string, error) {
177 // TODO: read gaps in appropriate style to remove the magic number below.
178 w := b.width - b.widthMargin - 4
179 if w > glamourMaxWidth {
180 w = glamourMaxWidth
181 }
182 tr, err := glamour.NewTermRenderer(
183 glamour.WithStandardStyle("dark"),
184 glamour.WithWordWrap(w),
185 )
186
187 if err != nil {
188 return "", err
189 }
190 mdt, err := tr.Render(md)
191 if err != nil {
192 return "", err
193 }
194 // Enforce a maximum width for cases when glamour lines run long.
195 //
196 // TODO: This should utlimately be implemented as a Glamour option.
197 mdt = wrap.String(wordwrap.String((mdt), w), w)
198 return mdt, nil
199}