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