chore(ui): formal hypercrush type treatment

Christian Rocha created

Change summary

internal/ui/logo/example/main.go | 57 ++++++++++-----------------------
internal/ui/logo/logo.go         | 45 +++++++++++++++++++++++---
2 files changed, 57 insertions(+), 45 deletions(-)

Detailed changes

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

@@ -1,49 +1,17 @@
 package main
 
+// This is an example for testing logo treatments. Do not remove.
+
 import (
 	"fmt"
-	"math/rand/v2"
 	"os"
 
 	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/ui/logo"
 	"github.com/charmbracelet/crush/internal/ui/styles"
-	"github.com/charmbracelet/x/exp/slice"
 	"github.com/charmbracelet/x/term"
 )
 
-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() {
 	w, _, err := term.GetSize(os.Stdout.Fd())
 	if err != nil {
@@ -58,13 +26,24 @@ func main() {
 		CharmColor:   s.LogoCharmColor,
 		VersionColor: s.LogoVersionColor,
 		Width:        w,
+		Unstable:     true,
+	}
+
+	renderCompact := func(hyper bool) string {
+		opts.Hyper = hyper
+		return logo.Render(s.Base, "v1.0.0", true, opts)
+	}
+
+	renderWide := func(hyper bool) string {
+		opts.Hyper = hyper
+		return logo.Render(s.Base, "v1.0.0", false, opts)
 	}
 
-	lipgloss.Println(logo.Render(s.Base, "v1.0.0", false, opts))
-	lipgloss.Println(logo.Render(s.Base, "v1.0.0", true, opts))
+	lipgloss.Println(
+		lipgloss.JoinHorizontal(lipgloss.Top, renderCompact(false), "  ", renderCompact(true)),
+	)
 
-	fmt.Println(renderLetterforms(false))
-	for range 5 {
-		fmt.Println(renderLetterforms(true))
+	for i := range 6 {
+		lipgloss.Println(renderWide(i > 0))
 	}
 }

internal/ui/logo/logo.go 🔗

@@ -4,6 +4,7 @@ package logo
 import (
 	"fmt"
 	"image/color"
+	"math/rand/v2"
 	"strings"
 
 	"charm.land/lipgloss/v2"
@@ -26,6 +27,11 @@ type Opts struct {
 	VersionColor color.Color // version text color
 	Width        int         // width of the rendered logo, used for truncation
 	Hyper        bool        // whether it is Crush or Hypercrush
+
+	// When true, stretch a random letterform on each render. Has no effect in
+	// compact mode. Mainly for testing. In production you will want to cache
+	// the stretched letterform to keep the logo from jittering on resize.
+	Unstable bool
 }
 
 // Render renders the Crush logo. Set the argument to true to render the narrow
@@ -34,7 +40,10 @@ type Opts struct {
 // The compact argument determines whether it renders compact for the sidebar
 // or wider for the main pane.
 func Render(base lipgloss.Style, version string, compact bool, o Opts) string {
-	const charm = " Charm™"
+	charm := "Charm™"
+	if !o.Hyper {
+		charm = " " + charm
+	}
 
 	fg := func(c color.Color, s string) string {
 		return lipgloss.NewStyle().Foreground(c).Render(s)
@@ -42,18 +51,39 @@ func Render(base lipgloss.Style, version string, compact bool, o Opts) string {
 
 	// Title.
 	const spacing = 1
-	letterforms := []letterform{
+	var hyperLetterforms []letterform
+	if o.Hyper {
+		hyperLetterforms = []letterform{
+			LetterH,
+			LetterYAlt,
+			LetterP,
+			LetterE,
+			LetterR,
+		}
+	}
+	crushLetterforms := []letterform{
 		LetterC,
 		LetterR,
 		LetterU,
 		LetterSAlt,
 		LetterH,
 	}
+	if o.Hyper && !compact {
+		crushLetterforms = append(hyperLetterforms, crushLetterforms...)
+	}
+
 	stretchIndex := -1 // -1 means no stretching.
-	if !compact {
-		stretchIndex = cachedRandN(len(letterforms))
+	if !compact && !o.Unstable {
+		// Always stretch the same letterform, which is picked once at random.
+		stretchIndex = cachedRandN(len(crushLetterforms))
+	} else if !compact && o.Unstable {
+		// Stretch a random letterform on every render.
+		stretchIndex = rand.IntN(len(crushLetterforms))
+	}
+	crush := renderWord(spacing, stretchIndex, crushLetterforms...)
+	if o.Hyper && compact {
+		crush = renderWord(spacing, stretchIndex, hyperLetterforms...) + "\n" + crush
 	}
-	crush := renderWord(spacing, stretchIndex, letterforms...)
 	crushWidth := lipgloss.Width(crush)
 	b := new(strings.Builder)
 	for r := range strings.SplitSeq(crush, "\n") {
@@ -65,13 +95,16 @@ func Render(base lipgloss.Style, version string, compact bool, o Opts) string {
 	metaRowGap := 1
 	maxVersionWidth := crushWidth - lipgloss.Width(charm) - metaRowGap
 	version = ansi.Truncate(version, maxVersionWidth, "…") // truncate version if too long.
+	if o.Hyper && compact {
+		version += " "
+	}
 	gap := max(0, crushWidth-lipgloss.Width(charm)-lipgloss.Width(version))
 	metaRow := fg(o.CharmColor, charm) + strings.Repeat(" ", gap) + fg(o.VersionColor, version)
 
 	// Join the meta row and big Crush title.
 	crush = strings.TrimSpace(metaRow + "\n" + crush)
 
-	// Narrow version.
+	// Narrow version. If this is Hypercrush, this is also a stacked version.
 	if compact {
 		field := fg(o.FieldColor, strings.Repeat(diag, crushWidth))
 		return strings.Join([]string{field, field, crush, field, ""}, "\n")