chore(ui): add new letterforms: h, y, p, e, with alts

Christian Rocha created

Change summary

go.mod                           |   2 
go.sum                           |   4 
internal/ui/logo/example/main.go |  49 +++
internal/ui/logo/letterforms.go  | 419 ++++++++++++++++++++++++++++++++++
internal/ui/logo/logo.go         | 228 ------------------
internal/ui/logo/rand.go         |   4 
6 files changed, 478 insertions(+), 228 deletions(-)

Detailed changes

go.mod 🔗

@@ -31,7 +31,7 @@ require (
 	github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
 	github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
 	github.com/charmbracelet/x/exp/ordered v0.1.0
-	github.com/charmbracelet/x/exp/slice v0.0.0-20260209194814-eeb2896ac759
+	github.com/charmbracelet/x/exp/slice v0.0.0-20260422141420-a6cbdff8a7e2
 	github.com/charmbracelet/x/exp/strings v0.1.0
 	github.com/charmbracelet/x/powernap v0.1.4
 	github.com/charmbracelet/x/term v0.2.2

go.sum 🔗

@@ -122,8 +122,8 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6g
 github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
 github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
 github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
-github.com/charmbracelet/x/exp/slice v0.0.0-20260209194814-eeb2896ac759 h1:96wFGlst+IDv3dIf5q29nw470wJYB3YAgemiciLZcG0=
-github.com/charmbracelet/x/exp/slice v0.0.0-20260209194814-eeb2896ac759/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
+github.com/charmbracelet/x/exp/slice v0.0.0-20260422141420-a6cbdff8a7e2 h1:EXQ7j9kQUGILoSIbxrr1osK9Ca3xqc6EJ7710FIiI3U=
+github.com/charmbracelet/x/exp/slice v0.0.0-20260422141420-a6cbdff8a7e2/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
 github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA=
 github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=
 github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ=

internal/ui/logo/example/main.go 🔗

@@ -0,0 +1,49 @@
+package main
+
+import (
+	"fmt"
+	"math/rand/v2"
+
+	"charm.land/lipgloss/v2"
+	"github.com/charmbracelet/crush/internal/ui/logo"
+	"github.com/charmbracelet/x/exp/slice"
+)
+
+func renderLetterforms(stretch bool) string {
+	letterFuncs := []func(bool) string{
+		logo.LetterH,
+		logo.LetterY,
+		logo.LetterYAlt,
+		logo.LetterP,
+		logo.LetterE,
+		logo.LetterEAlt,
+		logo.LetterR,
+		logo.LetterC,
+		logo.LetterR,
+		logo.LetterU,
+		logo.LetterSAlt,
+		logo.LetterH,
+	}
+
+	// Which letter to stretch, if we're stretching.
+	stretchIndex := -1
+	if stretch {
+		stretchIndex = rand.IntN(len(letterFuncs))
+	}
+
+	// Build letterforms.
+	letterforms := make([]string, len(letterFuncs))
+	for i, f := range letterFuncs {
+		letterforms[i] = f(stretch && i == stretchIndex)
+	}
+	letterforms = slice.Intersperse(letterforms, " ")
+
+	return lipgloss.JoinHorizontal(lipgloss.Top, letterforms...)
+}
+
+func main() {
+	fmt.Println(renderLetterforms(false))
+	for range 10 {
+		fmt.Println(renderLetterforms(true))
+	}
+}

internal/ui/logo/letterforms.go 🔗

@@ -0,0 +1,419 @@
+package logo
+
+import (
+	"strings"
+
+	"charm.land/lipgloss/v2"
+	"github.com/MakeNowJust/heredoc"
+	"github.com/charmbracelet/x/exp/slice"
+)
+
+// renderWord renders letterforms to fork a word. stretchIndex is the index of
+// the letter to stretch, or -1 if no letter should be stretched.
+func renderWord(spacing int, stretchIndex int, letterforms ...letterform) string {
+	if spacing < 0 {
+		spacing = 0
+	}
+
+	renderedLetterforms := make([]string, len(letterforms))
+
+	// pick one letter randomly to stretch
+	for i, letter := range letterforms {
+		renderedLetterforms[i] = letter(i == stretchIndex)
+	}
+
+	if spacing > 0 {
+		// Add spaces between the letters and render.
+		renderedLetterforms = slice.Intersperse(renderedLetterforms, strings.Repeat(" ", spacing))
+	}
+	return strings.TrimSpace(
+		lipgloss.JoinHorizontal(lipgloss.Top, renderedLetterforms...),
+	)
+}
+
+// LetterC renders the letter C in a stylized way. It takes an integer that
+// determines how many cells to stretch the letter. If the stretch is less than
+// 1, it defaults to no stretching.
+func LetterC(stretch bool) string {
+	// Here's what we're making:
+	//
+	// ▄▀▀▀▀
+	// █
+	//	▀▀▀▀
+
+	left := heredoc.Doc(`
+		▄
+		█
+	`)
+	right := heredoc.Doc(`
+		▀
+
+		▀
+	`)
+	return joinLetterform(
+		left,
+		stretchLetterformPart(right, letterformProps{
+			stretch:    stretch,
+			width:      4,
+			minStretch: 7,
+			maxStretch: 12,
+		}),
+	)
+}
+
+// LetterE renders the letter E in a stylized way. It takes an integer that
+// determines how many cells to stretch the letter. If the stretch is less than
+// 1, it defaults to no stretching.
+//
+// This is an alternate letterform. DO NOT REMOVE.
+func LetterE(stretch bool) string {
+	// Here's what we're making:
+	//
+	// █▀▀▀▀
+	// █▀▀▀▀
+	// ▀▀▀▀▀
+
+	left := heredoc.Doc(`
+       █
+       █
+       ▀
+	`)
+	middle := heredoc.Doc(`
+       ▀
+       ▀
+       ▀
+	`)
+	return joinLetterform(
+		left,
+		stretchLetterformPart(middle, letterformProps{
+			stretch:    stretch,
+			width:      4,
+			minStretch: 7,
+			maxStretch: 12,
+		}),
+	)
+}
+
+// LetterEAlt renders the letter E in a stylized way. It takes an integer that
+// determines how many cells to stretch the letter. If the stretch is less than
+// 1, it defaults to no stretching.
+//
+// This is an alternate letterform. DO NOT REMOVE.
+func LetterEAlt(stretch bool) string {
+	// Here's what we're making:
+	//
+	// █▀▀▀▀
+	// █ ▀▀▀
+	// ▀▀▀▀▀
+
+	left := heredoc.Doc(`
+       █▀
+       █
+       ▀▀
+	`)
+	middle := heredoc.Doc(`
+       ▀
+       ▀
+       ▀
+	`)
+	return joinLetterform(
+		left,
+		stretchLetterformPart(middle, letterformProps{
+			stretch:    stretch,
+			width:      3,
+			minStretch: 6,
+			maxStretch: 11,
+		}),
+	)
+}
+
+// LetterH renders the letter H in a stylized way. It takes an integer that
+// determines how many cells to stretch the letter. If the stretch is less than
+// 1, it defaults to no stretching.
+func LetterH(stretch bool) string {
+	// Here's what we're making:
+	//
+	// █   █
+	// █▀▀▀█
+	// ▀   ▀
+
+	side := heredoc.Doc(`
+		█
+		█
+		▀`)
+	middle := heredoc.Doc(`
+
+		▀
+	`)
+	return joinLetterform(
+		side,
+		stretchLetterformPart(middle, letterformProps{
+			stretch:    stretch,
+			width:      3,
+			minStretch: 8,
+			maxStretch: 12,
+		}),
+		side,
+	)
+}
+
+// LetterP renders the letter P in a stylized way. It takes an integer that
+// determines how many cells to stretch the letter. If the stretch is less than
+// 1, it defaults to no stretching.
+func LetterP(stretch bool) string {
+	// Here's what we're making:
+	//
+	// █▀▀▀▄
+	// █▀▀▀
+	// ▀
+
+	left := heredoc.Doc(`
+		█
+		█
+		▀
+	`)
+	center := heredoc.Doc(`
+		▀
+		▀
+	`)
+	right := heredoc.Doc(`
+		▄
+
+
+	`)
+	return joinLetterform(
+		left,
+		stretchLetterformPart(center, letterformProps{
+			stretch:    stretch,
+			width:      3,
+			minStretch: 7,
+			maxStretch: 12,
+		}),
+		right,
+	)
+}
+
+// LetterR renders the letter R in a stylized way. It takes an integer that
+// determines how many cells to stretch the letter. If the stretch is less than
+// 1, it defaults to no stretching.
+func LetterR(stretch bool) string {
+	// Here's what we're making:
+	//
+	// █▀▀▀▄
+	// █▀▀▀▄
+	// ▀   ▀
+
+	left := heredoc.Doc(`
+		█
+		█
+		▀
+	`)
+	center := heredoc.Doc(`
+		▀
+		▀
+	`)
+	right := heredoc.Doc(`
+		▄
+		▄
+		▀
+	`)
+	return joinLetterform(
+		left,
+		stretchLetterformPart(center, letterformProps{
+			stretch:    stretch,
+			width:      3,
+			minStretch: 7,
+			maxStretch: 12,
+		}),
+		right,
+	)
+}
+
+// LetterSAlt renders the letter S in a stylized way, more so than
+// [letterS]. It takes an integer that determines how many cells to stretch the
+// letter. If the stretch is less than 1, it defaults to no stretching.
+//
+// This is an alternate letterform. DO NOT REMOVE.
+func LetterSAlt(stretch bool) string {
+	// Here's what we're making:
+	//
+	// ▄▀▀▀▀▀
+	// ▀▀▀▀▀█
+	// ▀▀▀▀▀
+
+	left := heredoc.Doc(`
+		▄
+		▀
+		▀
+	`)
+	center := heredoc.Doc(`
+		▀
+		▀
+		▀
+	`)
+	right := heredoc.Doc(`
+		▀
+		█
+	`)
+	return joinLetterform(
+		left,
+		stretchLetterformPart(center, letterformProps{
+			stretch:    stretch,
+			width:      3,
+			minStretch: 7,
+			maxStretch: 12,
+		}),
+		right,
+	)
+}
+
+// LetterU renders the letter U in a stylized way. It takes an integer that
+// determines how many cells to stretch the letter. If the stretch is less than
+// 1, it defaults to no stretching.
+func LetterU(stretch bool) string {
+	// Here's what we're making:
+	//
+	// █   █
+	// █   █
+	//	▀▀▀
+
+	side := heredoc.Doc(`
+		█
+		█
+	`)
+	middle := heredoc.Doc(`
+
+
+		▀
+	`)
+	return joinLetterform(
+		side,
+		stretchLetterformPart(middle, letterformProps{
+			stretch:    stretch,
+			width:      3,
+			minStretch: 7,
+			maxStretch: 12,
+		}),
+		side,
+	)
+}
+
+// LetterY renders the letter Y in a stylized way. It takes an integer that
+// determines how many cells to stretch the letter. If the stretch is less than
+// 1, it defaults to no stretching.
+//
+// This is an alternate letterform. DO NOT REMOVE.
+func LetterY(stretch bool) string {
+	// Here's what we're making:
+	//
+	// █   █
+	//	▀▄▀
+	//	 ▀
+
+	side := heredoc.Doc(`
+		█
+
+	`)
+	inside := heredoc.Doc(`
+
+		▀
+
+	`)
+	middle := heredoc.Doc(`
+
+		▄
+		▀
+	`)
+	if stretch {
+		middle = heredoc.Doc(`
+
+			█
+			▀
+		`)
+	}
+
+	stretchedInside := stretchLetterformPart(inside, letterformProps{
+		stretch:    stretch,
+		width:      1,
+		minStretch: 4,
+		maxStretch: 10,
+	})
+
+	return joinLetterform(
+		side,
+		stretchedInside,
+		middle,
+		stretchedInside,
+		side,
+	)
+}
+
+// LetterYAlt renders the letter Y in a stylized way. It takes an integer that
+// determines how many cells to stretch the letter. If the stretch is less than
+// 1, it defaults to no stretching.
+//
+// This is an alternate letterform. DO NOT REMOVE.
+func LetterYAlt(stretch bool) string {
+	// Here's what we're making:
+	//
+	// █   █
+	// ▀▀▀▀█
+	// ▀▀▀▀
+
+	left := heredoc.Doc(`
+		█
+		▀
+		▀
+	`)
+	middle := heredoc.Doc(`
+
+		▀
+		▀
+	`)
+	right := heredoc.Doc(`
+		█
+		█
+
+	`)
+
+	return joinLetterform(
+		left,
+		stretchLetterformPart(middle, letterformProps{
+			stretch:    stretch,
+			width:      3,
+			minStretch: 6,
+			maxStretch: 10,
+		}),
+		right,
+	)
+}
+
+func joinLetterform(letters ...string) string {
+	return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
+}
+
+// letterformProps defines letterform stretching properties.
+// for readability.
+type letterformProps struct {
+	width      int
+	minStretch int
+	maxStretch int
+	stretch    bool
+}
+
+// stretchLetterformPart is a helper function for letter stretching. If randomize
+// is false the minimum number will be used.
+func stretchLetterformPart(s string, p letterformProps) string {
+	if p.maxStretch < p.minStretch {
+		p.minStretch, p.maxStretch = p.maxStretch, p.minStretch
+	}
+	n := p.width
+	if p.stretch {
+		n = cachedRandN(p.maxStretch-p.minStretch) + p.minStretch //nolint:gosec
+	}
+	parts := make([]string, n)
+	for i := range parts {
+		parts[i] = s
+	}
+	return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
+}

internal/ui/logo/logo.go 🔗

@@ -7,10 +7,8 @@ import (
 	"strings"
 
 	"charm.land/lipgloss/v2"
-	"github.com/MakeNowJust/heredoc"
 	"github.com/charmbracelet/crush/internal/ui/styles"
 	"github.com/charmbracelet/x/ansi"
-	"github.com/charmbracelet/x/exp/slice"
 )
 
 // letterform represents a letterform. It can be stretched horizontally by
@@ -44,11 +42,11 @@ func Render(s *styles.Styles, version string, compact bool, o Opts) string {
 	// Title.
 	const spacing = 1
 	letterforms := []letterform{
-		letterC,
-		letterR,
-		letterU,
-		letterSStylized,
-		letterH,
+		LetterC,
+		LetterR,
+		LetterU,
+		LetterSAlt,
+		LetterH,
 	}
 	stretchIndex := -1 // -1 means no stretching.
 	if !compact {
@@ -127,219 +125,3 @@ func SmallRender(t *styles.Styles, width int) string {
 	}
 	return title
 }
-
-// renderWord renders letterforms to fork a word. stretchIndex is the index of
-// the letter to stretch, or -1 if no letter should be stretched.
-func renderWord(spacing int, stretchIndex int, letterforms ...letterform) string {
-	if spacing < 0 {
-		spacing = 0
-	}
-
-	renderedLetterforms := make([]string, len(letterforms))
-
-	// pick one letter randomly to stretch
-	for i, letter := range letterforms {
-		renderedLetterforms[i] = letter(i == stretchIndex)
-	}
-
-	if spacing > 0 {
-		// Add spaces between the letters and render.
-		renderedLetterforms = slice.Intersperse(renderedLetterforms, strings.Repeat(" ", spacing))
-	}
-	return strings.TrimSpace(
-		lipgloss.JoinHorizontal(lipgloss.Top, renderedLetterforms...),
-	)
-}
-
-// letterC renders the letter C in a stylized way. It takes an integer that
-// determines how many cells to stretch the letter. If the stretch is less than
-// 1, it defaults to no stretching.
-func letterC(stretch bool) string {
-	// Here's what we're making:
-	//
-	// ▄▀▀▀▀
-	// █
-	//	▀▀▀▀
-
-	left := heredoc.Doc(`
-		▄
-		█
-	`)
-	right := heredoc.Doc(`
-		▀
-
-		▀
-	`)
-	return joinLetterform(
-		left,
-		stretchLetterformPart(right, letterformProps{
-			stretch:    stretch,
-			width:      4,
-			minStretch: 7,
-			maxStretch: 12,
-		}),
-	)
-}
-
-// letterH renders the letter H in a stylized way. It takes an integer that
-// determines how many cells to stretch the letter. If the stretch is less than
-// 1, it defaults to no stretching.
-func letterH(stretch bool) string {
-	// Here's what we're making:
-	//
-	// █   █
-	// █▀▀▀█
-	// ▀   ▀
-
-	side := heredoc.Doc(`
-		█
-		█
-		▀`)
-	middle := heredoc.Doc(`
-
-		▀
-	`)
-	return joinLetterform(
-		side,
-		stretchLetterformPart(middle, letterformProps{
-			stretch:    stretch,
-			width:      3,
-			minStretch: 8,
-			maxStretch: 12,
-		}),
-		side,
-	)
-}
-
-// letterR renders the letter R in a stylized way. It takes an integer that
-// determines how many cells to stretch the letter. If the stretch is less than
-// 1, it defaults to no stretching.
-func letterR(stretch bool) string {
-	// Here's what we're making:
-	//
-	// █▀▀▀▄
-	// █▀▀▀▄
-	// ▀   ▀
-
-	left := heredoc.Doc(`
-		█
-		█
-		▀
-	`)
-	center := heredoc.Doc(`
-		▀
-		▀
-	`)
-	right := heredoc.Doc(`
-		▄
-		▄
-		▀
-	`)
-	return joinLetterform(
-		left,
-		stretchLetterformPart(center, letterformProps{
-			stretch:    stretch,
-			width:      3,
-			minStretch: 7,
-			maxStretch: 12,
-		}),
-		right,
-	)
-}
-
-// letterSStylized renders the letter S in a stylized way, more so than
-// [letterS]. It takes an integer that determines how many cells to stretch the
-// letter. If the stretch is less than 1, it defaults to no stretching.
-func letterSStylized(stretch bool) string {
-	// Here's what we're making:
-	//
-	// ▄▀▀▀▀▀
-	// ▀▀▀▀▀█
-	// ▀▀▀▀▀
-
-	left := heredoc.Doc(`
-		▄
-		▀
-		▀
-	`)
-	center := heredoc.Doc(`
-		▀
-		▀
-		▀
-	`)
-	right := heredoc.Doc(`
-		▀
-		█
-	`)
-	return joinLetterform(
-		left,
-		stretchLetterformPart(center, letterformProps{
-			stretch:    stretch,
-			width:      3,
-			minStretch: 7,
-			maxStretch: 12,
-		}),
-		right,
-	)
-}
-
-// letterU renders the letter U in a stylized way. It takes an integer that
-// determines how many cells to stretch the letter. If the stretch is less than
-// 1, it defaults to no stretching.
-func letterU(stretch bool) string {
-	// Here's what we're making:
-	//
-	// █   █
-	// █   █
-	//	▀▀▀
-
-	side := heredoc.Doc(`
-		█
-		█
-	`)
-	middle := heredoc.Doc(`
-
-
-		▀
-	`)
-	return joinLetterform(
-		side,
-		stretchLetterformPart(middle, letterformProps{
-			stretch:    stretch,
-			width:      3,
-			minStretch: 7,
-			maxStretch: 12,
-		}),
-		side,
-	)
-}
-
-func joinLetterform(letters ...string) string {
-	return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
-}
-
-// letterformProps defines letterform stretching properties.
-// for readability.
-type letterformProps struct {
-	width      int
-	minStretch int
-	maxStretch int
-	stretch    bool
-}
-
-// stretchLetterformPart is a helper function for letter stretching. If randomize
-// is false the minimum number will be used.
-func stretchLetterformPart(s string, p letterformProps) string {
-	if p.maxStretch < p.minStretch {
-		p.minStretch, p.maxStretch = p.maxStretch, p.minStretch
-	}
-	n := p.width
-	if p.stretch {
-		n = cachedRandN(p.maxStretch-p.minStretch) + p.minStretch //nolint:gosec
-	}
-	parts := make([]string, n)
-	for i := range parts {
-		parts[i] = s
-	}
-	return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
-}

internal/ui/logo/rand.go 🔗

@@ -14,8 +14,8 @@ func cachedRandN(n int) int {
 	randCachesMu.Lock()
 	defer randCachesMu.Unlock()
 
-	if n, ok := randCaches[n]; ok {
-		return n
+	if nPrime, ok := randCaches[n]; ok {
+		return nPrime
 	}
 
 	r := rand.IntN(n)