1// Package logo renders a Crush wordmark in a stylized way.
2package logo
3
4import (
5 "fmt"
6 "image/color"
7 "strings"
8
9 "charm.land/lipgloss/v2"
10 "github.com/charmbracelet/crush/internal/ui/styles"
11 "github.com/charmbracelet/x/ansi"
12)
13
14// letterform represents a letterform. It can be stretched horizontally by
15// a given amount via the boolean argument.
16type letterform func(bool) string
17
18const diag = `╱`
19
20// Opts are the options for rendering the Crush title art.
21type Opts struct {
22 FieldColor color.Color // diagonal lines
23 TitleColorA color.Color // left gradient ramp point
24 TitleColorB color.Color // right gradient ramp point
25 CharmColor color.Color // Charm™ text color
26 VersionColor color.Color // version text color
27 Width int // width of the rendered logo, used for truncation
28 Hyper bool // whether it is Crush or Hypercrush
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(base lipgloss.Style, 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 const spacing = 1
45 letterforms := []letterform{
46 LetterC,
47 LetterR,
48 LetterU,
49 LetterSAlt,
50 LetterH,
51 }
52 stretchIndex := -1 // -1 means no stretching.
53 if !compact {
54 stretchIndex = cachedRandN(len(letterforms))
55 }
56 crush := renderWord(spacing, stretchIndex, letterforms...)
57 crushWidth := lipgloss.Width(crush)
58 b := new(strings.Builder)
59 for r := range strings.SplitSeq(crush, "\n") {
60 fmt.Fprintln(b, styles.ApplyForegroundGrad(base, r, o.TitleColorA, o.TitleColorB))
61 }
62 crush = b.String()
63
64 // Charm and version.
65 metaRowGap := 1
66 maxVersionWidth := crushWidth - lipgloss.Width(charm) - metaRowGap
67 version = ansi.Truncate(version, maxVersionWidth, "…") // truncate version if too long.
68 gap := max(0, crushWidth-lipgloss.Width(charm)-lipgloss.Width(version))
69 metaRow := fg(o.CharmColor, charm) + strings.Repeat(" ", gap) + fg(o.VersionColor, version)
70
71 // Join the meta row and big Crush title.
72 crush = strings.TrimSpace(metaRow + "\n" + crush)
73
74 // Narrow version.
75 if compact {
76 field := fg(o.FieldColor, strings.Repeat(diag, crushWidth))
77 return strings.Join([]string{field, field, crush, field, ""}, "\n")
78 }
79
80 fieldHeight := lipgloss.Height(crush)
81
82 // Left field.
83 const leftWidth = 6
84 leftFieldRow := fg(o.FieldColor, strings.Repeat(diag, leftWidth))
85 leftField := new(strings.Builder)
86 for range fieldHeight {
87 fmt.Fprintln(leftField, leftFieldRow)
88 }
89
90 // Right field.
91 rightWidth := max(15, o.Width-crushWidth-leftWidth-2) // 2 for the gap.
92 const stepDownAt = 0
93 rightField := new(strings.Builder)
94 for i := range fieldHeight {
95 width := rightWidth
96 if i >= stepDownAt {
97 width = rightWidth - (i - stepDownAt)
98 }
99 fmt.Fprint(rightField, fg(o.FieldColor, strings.Repeat(diag, width)), "\n")
100 }
101
102 // Return the wide version.
103 const hGap = " "
104 logo := lipgloss.JoinHorizontal(lipgloss.Top, leftField.String(), hGap, crush, hGap, rightField.String())
105 if o.Width > 0 {
106 // Truncate the logo to the specified width.
107 lines := strings.Split(logo, "\n")
108 for i, line := range lines {
109 lines[i] = ansi.Truncate(line, o.Width, "")
110 }
111 logo = strings.Join(lines, "\n")
112 }
113 return logo
114}
115
116// SmallRender renders a smaller version of the Crush logo, suitable for
117// smaller windows or sidebar usage.
118func SmallRender(t *styles.Styles, width int) string {
119 title := t.Base.Foreground(t.Secondary).Render("Charm™")
120 title = fmt.Sprintf("%s %s", title, styles.ApplyBoldForegroundGrad(t.Base, "Crush", t.Secondary, t.Primary))
121 remainingWidth := width - lipgloss.Width(title) - 1 // 1 for the space after "Crush"
122 if remainingWidth > 0 {
123 lines := strings.Repeat("╱", remainingWidth)
124 title = fmt.Sprintf("%s %s", title, t.Base.Foreground(t.Primary).Render(lines))
125 }
126 return title
127}