on main help sign in
chore(ui): formal hypercrush type treatment
commit4f3a308f708ea53e3b27cb92a02e6515107092ff authorChristian Rocha <christian@rocha.is> dateThu 23 Apr 2026, 16:50 (6 months ago) parent511916a
2 files changed+57−45
internal/ui/logo/example/main.go+18−39 internal/ui/logo/logo.go+39−6
internal/ui/logo/example/main.go+18−39
@@ -1,49 +1,17 @@
11package main
22
3+// This is an example for testing logo treatments. Do not remove.
4+
35import (
46 "fmt"
5− "math/rand/v2"
67 "os"
78
89 "charm.land/lipgloss/v2"
910 "github.com/charmbracelet/crush/internal/ui/logo"
1011 "github.com/charmbracelet/crush/internal/ui/styles"
11− "github.com/charmbracelet/x/exp/slice"
1212 "github.com/charmbracelet/x/term"
1313)
1414
15−func renderLetterforms(stretch bool) string {
16− letterFuncs := []func(bool) string{
17− logo.LetterH,
18− logo.LetterY,
19− logo.LetterYAlt,
20− logo.LetterP,
21− logo.LetterE,
22− logo.LetterEAlt,
23− logo.LetterR,
24− logo.LetterC,
25− logo.LetterR,
26− logo.LetterU,
27− logo.LetterSAlt,
28− logo.LetterH,
29− }
30−
31− // Which letter to stretch, if we're stretching.
32− stretchIndex := -1
33− if stretch {
34− stretchIndex = rand.IntN(len(letterFuncs))
35− }
36−
37− // Build letterforms.
38− letterforms := make([]string, len(letterFuncs))
39− for i, f := range letterFuncs {
40− letterforms[i] = f(stretch && i == stretchIndex)
41− }
42− letterforms = slice.Intersperse(letterforms, " ")
43−
44− return lipgloss.JoinHorizontal(lipgloss.Top, letterforms...)
45−}
46−
4715func main() {
4816 w, _, err := term.GetSize(os.Stdout.Fd())
4917 if err != nil {
@@ -58,13 +26,24 @@ func main() {
5826 CharmColor: s.LogoCharmColor,
5927 VersionColor: s.LogoVersionColor,
6028 Width: w,
29+ Unstable: true,
30+ }
31+
32+ renderCompact := func(hyper bool) string {
33+ opts.Hyper = hyper
34+ return logo.Render(s.Base, "v1.0.0", true, opts)
35+ }
36+
37+ renderWide := func(hyper bool) string {
38+ opts.Hyper = hyper
39+ return logo.Render(s.Base, "v1.0.0", false, opts)
6140 }
6241
63− lipgloss.Println(logo.Render(s.Base, "v1.0.0", false, opts))
64− lipgloss.Println(logo.Render(s.Base, "v1.0.0", true, opts))
42+ lipgloss.Println(
43+ lipgloss.JoinHorizontal(lipgloss.Top, renderCompact(false), " ", renderCompact(true)),
44+ )
6545
66− fmt.Println(renderLetterforms(false))
67− for range 5 {
68− fmt.Println(renderLetterforms(true))
46+ for i := range 6 {
47+ lipgloss.Println(renderWide(i > 0))
6948 }
7049}
internal/ui/logo/logo.go+39−6
@@ -4,6 +4,7 @@ package logo
44import (
55 "fmt"
66 "image/color"
7+ "math/rand/v2"
78 "strings"
89
910 "charm.land/lipgloss/v2"
@@ -26,6 +27,11 @@ type Opts struct {
2627 VersionColor color.Color // version text color
2728 Width int // width of the rendered logo, used for truncation
2829 Hyper bool // whether it is Crush or Hypercrush
30+
31+ // When true, stretch a random letterform on each render. Has no effect in
32+ // compact mode. Mainly for testing. In production you will want to cache
33+ // the stretched letterform to keep the logo from jittering on resize.
34+ Unstable bool
2935}
3036
3137// Render renders the Crush logo. Set the argument to true to render the narrow
@@ -34,7 +40,10 @@ type Opts struct {
3440// The compact argument determines whether it renders compact for the sidebar
3541// or wider for the main pane.
3642func Render(base lipgloss.Style, version string, compact bool, o Opts) string {
37− const charm = " Charm™"
43+ charm := "Charm™"
44+ if !o.Hyper {
45+ charm = " " + charm
46+ }
3847
3948 fg := func(c color.Color, s string) string {
4049 return lipgloss.NewStyle().Foreground(c).Render(s)
@@ -42,18 +51,39 @@ func Render(base lipgloss.Style, version string, compact bool, o Opts) string {
4251
4352 // Title.
4453 const spacing = 1
45− letterforms := []letterform{
54+ var hyperLetterforms []letterform
55+ if o.Hyper {
56+ hyperLetterforms = []letterform{
57+ LetterH,
58+ LetterYAlt,
59+ LetterP,
60+ LetterE,
61+ LetterR,
62+ }
63+ }
64+ crushLetterforms := []letterform{
4665 LetterC,
4766 LetterR,
4867 LetterU,
4968 LetterSAlt,
5069 LetterH,
5170 }
71+ if o.Hyper && !compact {
72+ crushLetterforms = append(hyperLetterforms, crushLetterforms...)
73+ }
74+
5275 stretchIndex := -1 // -1 means no stretching.
53− if !compact {
54− stretchIndex = cachedRandN(len(letterforms))
76+ if !compact && !o.Unstable {
77+ // Always stretch the same letterform, which is picked once at random.
78+ stretchIndex = cachedRandN(len(crushLetterforms))
79+ } else if !compact && o.Unstable {
80+ // Stretch a random letterform on every render.
81+ stretchIndex = rand.IntN(len(crushLetterforms))
82+ }
83+ crush := renderWord(spacing, stretchIndex, crushLetterforms...)
84+ if o.Hyper && compact {
85+ crush = renderWord(spacing, stretchIndex, hyperLetterforms...) + "\n" + crush
5586 }
56− crush := renderWord(spacing, stretchIndex, letterforms...)
5787 crushWidth := lipgloss.Width(crush)
5888 b := new(strings.Builder)
5989 for r := range strings.SplitSeq(crush, "\n") {
@@ -65,13 +95,16 @@ func Render(base lipgloss.Style, version string, compact bool, o Opts) string {
6595 metaRowGap := 1
6696 maxVersionWidth := crushWidth - lipgloss.Width(charm) - metaRowGap
6797 version = ansi.Truncate(version, maxVersionWidth, "…") // truncate version if too long.
98+ if o.Hyper && compact {
99+ version += " "
100+ }
68101 gap := max(0, crushWidth-lipgloss.Width(charm)-lipgloss.Width(version))
69102 metaRow := fg(o.CharmColor, charm) + strings.Repeat(" ", gap) + fg(o.VersionColor, version)
70103
71104 // Join the meta row and big Crush title.
72105 crush = strings.TrimSpace(metaRow + "\n" + crush)
73106
74− // Narrow version.
107+ // Narrow version. If this is Hypercrush, this is also a stacked version.
75108 if compact {
76109 field := fg(o.FieldColor, strings.Repeat(diag, crushWidth))
77110 return strings.Join([]string{field, field, crush, field, ""}, "\n")