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