1package logo
2
3import (
4 "fmt"
5 "image/color"
6 "math/rand/v2"
7 "strings"
8
9 "github.com/MakeNowJust/heredoc"
10 "github.com/charmbracelet/crush/internal/tui/styles"
11 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/charmbracelet/x/ansi"
13 "github.com/charmbracelet/x/exp/slice"
14)
15
16// letterform represents a letterform. It can be stretched horizontally by
17// a given amount via the boolean argument.
18type letterform func(bool) string
19
20const diag = `β±`
21
22// Opts are the options for rendering the Crush title art.
23type Opts struct {
24 FieldColor color.Color // diagonal lines
25 TitleColorA color.Color // left gradient ramp point
26 TitleColorB color.Color // right gradient ramp point
27 CharmColor color.Color // Charmβ’ text color
28 VersionColor color.Color // Version text color
29}
30
31// Render renders the Crush logo. Set the argument to true to render the narrow
32// version, intended for use in a sidebar.
33//
34// The compact argument determines whether it renders compact for the sidebar
35// or wider for the main pane.
36func Render(version string, compact bool, o Opts) string {
37 const charm = " Charmβ’"
38
39 fg := func(c color.Color, s string) string {
40 return lipgloss.NewStyle().Foreground(c).Render(s)
41 }
42
43 // Title.
44 crush := renderWord(1, !compact, letterC, letterR, letterU, letterS, letterH)
45 crushWidth := lipgloss.Width(crush)
46 b := new(strings.Builder)
47 for r := range strings.SplitSeq(crush, "\n") {
48 fmt.Fprintln(b, styles.ApplyForegroundGrad(r, o.TitleColorA, o.TitleColorB))
49 }
50 crush = b.String()
51
52 // Charm and version.
53 metaRowGap := 1
54 maxVersionWidth := crushWidth - lipgloss.Width(charm) - metaRowGap
55 version = ansi.Truncate(version, maxVersionWidth, "β¦") // truncate version if too long.
56 gap := max(0, crushWidth-lipgloss.Width(charm)-lipgloss.Width(version))
57 metaRow := fg(o.CharmColor, charm) + strings.Repeat(" ", gap) + fg(o.VersionColor, version)
58
59 // Join the meta row and big Crush title.
60 crush = strings.TrimSpace(metaRow + "\n" + crush)
61
62 // Narrow version.
63 if compact {
64 field := fg(o.FieldColor, strings.Repeat(diag, crushWidth))
65 return strings.Join([]string{field, field, crush, field, ""}, "\n")
66 }
67
68 fieldHeight := lipgloss.Height(crush)
69
70 // Left field.
71 const leftWidth = 6
72 leftFieldRow := fg(o.FieldColor, strings.Repeat(diag, leftWidth))
73 leftField := new(strings.Builder)
74 for range fieldHeight {
75 fmt.Fprintln(leftField, leftFieldRow)
76 }
77
78 // Right field.
79 const rightWidth = 15
80 const stepDownAt = 0
81 rightField := new(strings.Builder)
82 for i := range fieldHeight {
83 width := rightWidth
84 if i >= stepDownAt {
85 width = rightWidth - (i - stepDownAt)
86 }
87 fmt.Fprint(rightField, fg(o.FieldColor, strings.Repeat(diag, width)), "\n")
88 }
89
90 // Return the wide version.
91 const hGap = " "
92 return lipgloss.JoinHorizontal(lipgloss.Top, leftField.String(), hGap, crush, hGap, rightField.String())
93}
94
95// renderWord renders letterforms to fork a word.
96func renderWord(spacing int, stretchRandomLetter bool, letterforms ...letterform) string {
97 if spacing < 0 {
98 spacing = 0
99 }
100
101 renderedLetterforms := make([]string, len(letterforms))
102
103 // pick one letter randomly to stretch
104 stretchIndex := -1
105 if stretchRandomLetter {
106 stretchIndex = rand.IntN(len(letterforms)) //nolint:gosec
107 }
108
109 for i, letter := range letterforms {
110 renderedLetterforms[i] = letter(i == stretchIndex)
111 }
112
113 if spacing > 0 {
114 // Add spaces between the letters and render.
115 renderedLetterforms = slice.Intersperse(renderedLetterforms, strings.Repeat(" ", spacing))
116 }
117 return strings.TrimSpace(
118 lipgloss.JoinHorizontal(lipgloss.Top, renderedLetterforms...),
119 )
120}
121
122// letterC renders the letter C in a stylized way. It takes an integer that
123// determines how many cells to stretch the letter. If the stretch is less than
124// 1, it defaults to no stretching.
125func letterC(stretch bool) string {
126 // Here's what we're making:
127 //
128 // βββββ
129 // β
130 // ββββ
131
132 left := heredoc.Doc(`
133 β
134 β
135 `)
136 right := heredoc.Doc(`
137 β
138
139 β
140 `)
141 return joinLetterform(
142 left,
143 stretchLetterformPart(right, letterformProps{
144 stretch: stretch,
145 width: 4,
146 minStretch: 7,
147 maxStretch: 12,
148 }),
149 )
150}
151
152// letterH renders the letter H in a stylized way. It takes an integer that
153// determines how many cells to stretch the letter. If the stretch is less than
154// 1, it defaults to no stretching.
155func letterH(stretch bool) string {
156 // Here's what we're making:
157 //
158 // β β
159 // βββββ
160 // β β
161
162 side := heredoc.Doc(`
163 β
164 β
165 β`)
166 middle := heredoc.Doc(`
167
168 β
169 `)
170 return joinLetterform(
171 side,
172 stretchLetterformPart(middle, letterformProps{
173 stretch: stretch,
174 width: 3,
175 minStretch: 8,
176 maxStretch: 12,
177 }),
178 side,
179 )
180}
181
182// letterR renders the letter R in a stylized way. It takes an integer that
183// determines how many cells to stretch the letter. If the stretch is less than
184// 1, it defaults to no stretching.
185func letterR(stretch bool) string {
186 // Here's what we're making:
187 //
188 // βββββ
189 // βββββ
190 // β β
191
192 left := heredoc.Doc(`
193 β
194 β
195 β
196 `)
197 center := heredoc.Doc(`
198 β
199 β
200 `)
201 right := heredoc.Doc(`
202 β
203 β
204 β
205 `)
206 return joinLetterform(
207 left,
208 stretchLetterformPart(center, letterformProps{
209 stretch: stretch,
210 width: 3,
211 minStretch: 7,
212 maxStretch: 12,
213 }),
214 right,
215 )
216}
217
218// letterS renders the letter S in a stylized way. It takes an integer that
219// determines how many cells to stretch the letter. If the stretch is less than
220// 1, it defaults to no stretching.
221func letterS(stretch bool) string {
222 // Here's what we're making:
223 //
224 // βββββ
225 // ββββ
226 // ββββ
227
228 left := heredoc.Doc(`
229 β
230
231 β
232 `)
233 center := heredoc.Doc(`
234 β
235 β
236 β
237 `)
238 right := heredoc.Doc(`
239 β
240 β
241 `)
242 return joinLetterform(
243 left,
244 stretchLetterformPart(center, letterformProps{
245 stretch: stretch,
246 width: 3,
247 minStretch: 7,
248 maxStretch: 12,
249 }),
250 right,
251 )
252}
253
254// letterU renders the letter U in a stylized way. It takes an integer that
255// determines how many cells to stretch the letter. If the stretch is less than
256// 1, it defaults to no stretching.
257func letterU(stretch bool) string {
258 // Here's what we're making:
259 //
260 // β β
261 // β β
262 // βββ
263
264 side := heredoc.Doc(`
265 β
266 β
267 `)
268 middle := heredoc.Doc(`
269
270
271 β
272 `)
273 return joinLetterform(
274 side,
275 stretchLetterformPart(middle, letterformProps{
276 stretch: stretch,
277 width: 3,
278 minStretch: 7,
279 maxStretch: 12,
280 }),
281 side,
282 )
283}
284
285func joinLetterform(letters ...string) string {
286 return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
287}
288
289// letterformProps defines letterform stretching properties.
290// for readability.
291type letterformProps struct {
292 width int
293 minStretch int
294 maxStretch int
295 stretch bool
296}
297
298// stretchLetterformPart is a helper function for letter stretching. If randomize
299// is false the minimum number will be used.
300func stretchLetterformPart(s string, p letterformProps) string {
301 if p.maxStretch < p.minStretch {
302 p.minStretch, p.maxStretch = p.maxStretch, p.minStretch
303 }
304 n := p.width
305 if p.stretch {
306 n = rand.IntN(p.maxStretch-p.minStretch) + p.minStretch //nolint:gosec
307 }
308 parts := make([]string, n)
309 for i := range parts {
310 parts[i] = s
311 }
312 return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
313}