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