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}