From 15f20e6a7acdff8f671ec805671c205addd3dacd Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Tue, 14 Apr 2026 17:42:30 -0400 Subject: [PATCH] chore(ui): add new letterforms: h, y, p, e, with alts --- 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(-) create mode 100644 internal/ui/logo/example/main.go create mode 100644 internal/ui/logo/letterforms.go diff --git a/go.mod b/go.mod index e60c4149ef307827d8a706359eac94270a2b41a4..8129723b134ce6c3ad5c67c787f5dbd9cb575a71 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0b3a11910c051767727c6901cadc54c64abf11f4..82ddfa65f4e64eb3650fb909c7b6ca5d85e931f5 100644 --- a/go.sum +++ b/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= diff --git a/internal/ui/logo/example/main.go b/internal/ui/logo/example/main.go new file mode 100644 index 0000000000000000000000000000000000000000..32a67d0d68ca885ac6426e6282ff916694d4d488 --- /dev/null +++ b/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)) + } +} diff --git a/internal/ui/logo/letterforms.go b/internal/ui/logo/letterforms.go new file mode 100644 index 0000000000000000000000000000000000000000..2b656739382d12e068ce4afb2c6eb49a22776419 --- /dev/null +++ b/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...) +} diff --git a/internal/ui/logo/logo.go b/internal/ui/logo/logo.go index 68387d4c0ba2c8914929d041e149f4b23ff3694b..ac4ada2be171ece4150d95c0dccdec0c22d57247 100644 --- a/internal/ui/logo/logo.go +++ b/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...) -} diff --git a/internal/ui/logo/rand.go b/internal/ui/logo/rand.go index cf79487e23825b468c98a0f27bbc8dbfbb1a7081..f17afda544b4e96b2c0552db94e77a9f481734c5 100644 --- a/internal/ui/logo/rand.go +++ b/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)