Detailed changes
@@ -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
@@ -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=
@@ -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))
+ }
+}
@@ -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...)
+}
@@ -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...)
-}
@@ -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)