logo.go

  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}