@@ -0,0 +1,346 @@
1+// Package logo renders a Crush wordmark in a stylized way.
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"
16+// letterform represents a letterform. It can be stretched horizontally by
17+// a given amount via the boolean argument.
18+type letterform func(bool) string
22+// Opts are the options for rendering the Crush title art.
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
32+// Render renders the Crush logo. Set the argument to true to render the narrow
33+// version, intended for use in a sidebar.
35+// The compact argument determines whether it renders compact for the sidebar
36+// or wider for the main pane.
37+func Render(version string, compact bool, o Opts) string {
38+ const charm = " Charm™"
40+ fg := func(c color.Color, s string) string {
41+ return lipgloss.NewStyle().Foreground(c).Render(s)
46+ letterforms := []letterform{
53+ stretchIndex := -1 // -1 means no stretching.
55+ stretchIndex = cachedRandN(len(letterforms))
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))
66+ // Charm and version.
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)
73+ // Join the meta row and big Crush title.
74+ crush = strings.TrimSpace(metaRow + "\n" + crush)
78+ field := fg(o.FieldColor, strings.Repeat(diag, crushWidth))
79+ return strings.Join([]string{field, field, crush, field, ""}, "\n")
82+ fieldHeight := lipgloss.Height(crush)
86+ leftFieldRow := fg(o.FieldColor, strings.Repeat(diag, leftWidth))
87+ leftField := new(strings.Builder)
88+ for range fieldHeight {
89+ fmt.Fprintln(leftField, leftFieldRow)
93+ rightWidth := max(15, o.Width-crushWidth-leftWidth-2) // 2 for the gap.
95+ rightField := new(strings.Builder)
96+ for i := range fieldHeight {
99+ width = rightWidth - (i - stepDownAt)
101+ fmt.Fprint(rightField, fg(o.FieldColor, strings.Repeat(diag, width)), "\n")
104+ // Return the wide version.
106+ logo := lipgloss.JoinHorizontal(lipgloss.Top, leftField.String(), hGap, crush, hGap, rightField.String())
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, "")
113+ logo = strings.Join(lines, "\n")
118+// SmallRender renders a smaller version of the Crush logo, suitable for
119+// smaller windows or sidebar usage.
120+func 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))
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.
134+func renderWord(spacing int, stretchIndex int, letterforms ...letterform) string {
139+ renderedLetterforms := make([]string, len(letterforms))
141+ // pick one letter randomly to stretch
142+ for i, letter := range letterforms {
143+ renderedLetterforms[i] = letter(i == stretchIndex)
147+ // Add spaces between the letters and render.
148+ renderedLetterforms = slice.Intersperse(renderedLetterforms, strings.Repeat(" ", spacing))
150+ return strings.TrimSpace(
151+ lipgloss.JoinHorizontal(lipgloss.Top, renderedLetterforms...),
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.
158+func letterC(stretch bool) string {
159+ // Here's what we're making:
165+ left := heredoc.Doc(`
169+ right := heredoc.Doc(`
174+ return joinLetterform(
176+ stretchLetterformPart(right, letterformProps{
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.
188+func letterH(stretch bool) string {
189+ // Here's what we're making:
195+ side := heredoc.Doc(`
199+ middle := heredoc.Doc(`
203+ return joinLetterform(
205+ stretchLetterformPart(middle, letterformProps{
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.
218+func letterR(stretch bool) string {
219+ // Here's what we're making:
225+ left := heredoc.Doc(`
230+ center := heredoc.Doc(`
234+ right := heredoc.Doc(`
239+ return joinLetterform(
241+ stretchLetterformPart(center, letterformProps{
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.
254+func letterSStylized(stretch bool) string {
255+ // Here's what we're making:
261+ left := heredoc.Doc(`
266+ center := heredoc.Doc(`
271+ right := heredoc.Doc(`
275+ return joinLetterform(
277+ stretchLetterformPart(center, letterformProps{
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.
290+func letterU(stretch bool) string {
291+ // Here's what we're making:
297+ side := heredoc.Doc(`
301+ middle := heredoc.Doc(`
306+ return joinLetterform(
308+ stretchLetterformPart(middle, letterformProps{
318+func joinLetterform(letters ...string) string {
319+ return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
322+// letterformProps defines letterform stretching properties.
324+type letterformProps struct {
331+// stretchLetterformPart is a helper function for letter stretching. If randomize
332+// is false the minimum number will be used.
333+func stretchLetterformPart(s string, p letterformProps) string {
334+ if p.maxStretch < p.minStretch {
335+ p.minStretch, p.maxStretch = p.maxStretch, p.minStretch
339+ n = cachedRandN(p.maxStretch-p.minStretch) + p.minStretch //nolint:gosec
341+ parts := make([]string, n)
342+ for i := range parts {
345+ return lipgloss.JoinHorizontal(lipgloss.Top, parts...)