logo.go

  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}