1// Package logo renders a Crush wordmark in a stylized way.
9 "charm.land/lipgloss/v2"
10 "github.com/MakeNowJust/heredoc"
11 "github.com/charmbracelet/crush/internal/tui/styles"
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.
18type 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.
37func 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))
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.
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))
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 {
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.
158func 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.
188func 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.
218func 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.
254func 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.
290func 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{
318func joinLetterform(letters ...string) string {
319 return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
322// letterformProps defines letterform stretching properties.
324type letterformProps struct {
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
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...)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list