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) // 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 logo := lipgloss.JoinHorizontal(lipgloss.Top, leftField.String(), hGap, crush, hGap, rightField.String())
102 if o.Width > 0 {
103 // Truncate the logo to the specified width.
104 lines := strings.Split(logo, "\n")
105 for i, line := range lines {
106 lines[i] = ansi.Truncate(line, o.Width, "")
107 }
108 logo = strings.Join(lines, "\n")
109 }
110 return logo
111}
112
113// renderWord renders letterforms to fork a word.
114func renderWord(spacing int, stretchRandomLetter bool, letterforms ...letterform) string {
115 if spacing < 0 {
116 spacing = 0
117 }
118
119 renderedLetterforms := make([]string, len(letterforms))
120
121 // pick one letter randomly to stretch
122 stretchIndex := -1
123 if stretchRandomLetter {
124 stretchIndex = rand.IntN(len(letterforms)) //nolint:gosec
125 }
126
127 for i, letter := range letterforms {
128 renderedLetterforms[i] = letter(i == stretchIndex)
129 }
130
131 if spacing > 0 {
132 // Add spaces between the letters and render.
133 renderedLetterforms = slice.Intersperse(renderedLetterforms, strings.Repeat(" ", spacing))
134 }
135 return strings.TrimSpace(
136 lipgloss.JoinHorizontal(lipgloss.Top, renderedLetterforms...),
137 )
138}
139
140// letterC renders the letter C in a stylized way. It takes an integer that
141// determines how many cells to stretch the letter. If the stretch is less than
142// 1, it defaults to no stretching.
143func letterC(stretch bool) string {
144 // Here's what we're making:
145 //
146 // βββββ
147 // β
148 // ββββ
149
150 left := heredoc.Doc(`
151 β
152 β
153 `)
154 right := heredoc.Doc(`
155 β
156
157 β
158 `)
159 return joinLetterform(
160 left,
161 stretchLetterformPart(right, letterformProps{
162 stretch: stretch,
163 width: 4,
164 minStretch: 7,
165 maxStretch: 12,
166 }),
167 )
168}
169
170// letterH renders the letter H in a stylized way. It takes an integer that
171// determines how many cells to stretch the letter. If the stretch is less than
172// 1, it defaults to no stretching.
173func letterH(stretch bool) string {
174 // Here's what we're making:
175 //
176 // β β
177 // βββββ
178 // β β
179
180 side := heredoc.Doc(`
181 β
182 β
183 β`)
184 middle := heredoc.Doc(`
185
186 β
187 `)
188 return joinLetterform(
189 side,
190 stretchLetterformPart(middle, letterformProps{
191 stretch: stretch,
192 width: 3,
193 minStretch: 8,
194 maxStretch: 12,
195 }),
196 side,
197 )
198}
199
200// letterR renders the letter R in a stylized way. It takes an integer that
201// determines how many cells to stretch the letter. If the stretch is less than
202// 1, it defaults to no stretching.
203func letterR(stretch bool) string {
204 // Here's what we're making:
205 //
206 // βββββ
207 // βββββ
208 // β β
209
210 left := heredoc.Doc(`
211 β
212 β
213 β
214 `)
215 center := heredoc.Doc(`
216 β
217 β
218 `)
219 right := heredoc.Doc(`
220 β
221 β
222 β
223 `)
224 return joinLetterform(
225 left,
226 stretchLetterformPart(center, letterformProps{
227 stretch: stretch,
228 width: 3,
229 minStretch: 7,
230 maxStretch: 12,
231 }),
232 right,
233 )
234}
235
236// letterS renders the letter S in a stylized way. It takes an integer that
237// determines how many cells to stretch the letter. If the stretch is less than
238// 1, it defaults to no stretching.
239func letterS(stretch bool) string {
240 // Here's what we're making:
241 //
242 // βββββ
243 // ββββ
244 // ββββ
245
246 left := heredoc.Doc(`
247 β
248
249 β
250 `)
251 center := heredoc.Doc(`
252 β
253 β
254 β
255 `)
256 right := heredoc.Doc(`
257 β
258 β
259 `)
260 return joinLetterform(
261 left,
262 stretchLetterformPart(center, letterformProps{
263 stretch: stretch,
264 width: 3,
265 minStretch: 7,
266 maxStretch: 12,
267 }),
268 right,
269 )
270}
271
272// letterSStylized renders the letter S in a stylized way, more so than
273// [letterS]. It takes an integer that determines how many cells to stretch the
274// letter. If the stretch is less than 1, it defaults to no stretching.
275func letterSStylized(stretch bool) string {
276 // Here's what we're making:
277 //
278 // ββββββ
279 // ββββββ
280 // βββββ
281
282 left := heredoc.Doc(`
283 β
284 β
285 β
286 `)
287 center := heredoc.Doc(`
288 β
289 β
290 β
291 `)
292 right := heredoc.Doc(`
293 β
294 β
295 `)
296 return joinLetterform(
297 left,
298 stretchLetterformPart(center, letterformProps{
299 stretch: stretch,
300 width: 3,
301 minStretch: 7,
302 maxStretch: 12,
303 }),
304 right,
305 )
306}
307
308// letterU renders the letter U in a stylized way. It takes an integer that
309// determines how many cells to stretch the letter. If the stretch is less than
310// 1, it defaults to no stretching.
311func letterU(stretch bool) string {
312 // Here's what we're making:
313 //
314 // β β
315 // β β
316 // βββ
317
318 side := heredoc.Doc(`
319 β
320 β
321 `)
322 middle := heredoc.Doc(`
323
324
325 β
326 `)
327 return joinLetterform(
328 side,
329 stretchLetterformPart(middle, letterformProps{
330 stretch: stretch,
331 width: 3,
332 minStretch: 7,
333 maxStretch: 12,
334 }),
335 side,
336 )
337}
338
339func joinLetterform(letters ...string) string {
340 return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
341}
342
343// letterformProps defines letterform stretching properties.
344// for readability.
345type letterformProps struct {
346 width int
347 minStretch int
348 maxStretch int
349 stretch bool
350}
351
352// stretchLetterformPart is a helper function for letter stretching. If randomize
353// is false the minimum number will be used.
354func stretchLetterformPart(s string, p letterformProps) string {
355 if p.maxStretch < p.minStretch {
356 p.minStretch, p.maxStretch = p.maxStretch, p.minStretch
357 }
358 n := p.width
359 if p.stretch {
360 n = rand.IntN(p.maxStretch-p.minStretch) + p.minStretch //nolint:gosec
361 }
362 parts := make([]string, n)
363 for i := range parts {
364 parts[i] = s
365 }
366 return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
367}