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