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