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	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}