logo.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. // Package logo renders a Crush wordmark in a stylized way.
  2. package logo
  3. import (
  4. "fmt"
  5. "image/color"
  6. "strings"
  7. "charm.land/lipgloss/v2"
  8. "github.com/MakeNowJust/heredoc"
  9. "github.com/charmbracelet/crush/internal/ui/styles"
  10. "github.com/charmbracelet/x/ansi"
  11. "github.com/charmbracelet/x/exp/slice"
  12. )
  13. // letterform represents a letterform. It can be stretched horizontally by
  14. // a given amount via the boolean argument.
  15. type letterform func(bool) string
  16. const diag = `╱`
  17. // Opts are the options for rendering the Crush title art.
  18. type Opts struct {
  19. FieldColor color.Color // diagonal lines
  20. TitleColorA color.Color // left gradient ramp point
  21. TitleColorB color.Color // right gradient ramp point
  22. CharmColor color.Color // Charm™ text color
  23. VersionColor color.Color // Version text color
  24. Width int // width of the rendered logo, used for truncation
  25. }
  26. // Render renders the Crush logo. Set the argument to true to render the narrow
  27. // version, intended for use in a sidebar.
  28. //
  29. // The compact argument determines whether it renders compact for the sidebar
  30. // or wider for the main pane.
  31. func Render(s *styles.Styles, version string, compact bool, o Opts) string {
  32. const charm = " Charm™"
  33. fg := func(c color.Color, s string) string {
  34. return lipgloss.NewStyle().Foreground(c).Render(s)
  35. }
  36. // Title.
  37. const spacing = 1
  38. letterforms := []letterform{
  39. letterC,
  40. letterR,
  41. letterU,
  42. letterSStylized,
  43. letterH,
  44. }
  45. stretchIndex := -1 // -1 means no stretching.
  46. if !compact {
  47. stretchIndex = cachedRandN(len(letterforms))
  48. }
  49. crush := renderWord(spacing, stretchIndex, letterforms...)
  50. crushWidth := lipgloss.Width(crush)
  51. b := new(strings.Builder)
  52. for r := range strings.SplitSeq(crush, "\n") {
  53. fmt.Fprintln(b, styles.ApplyForegroundGrad(s, r, o.TitleColorA, o.TitleColorB))
  54. }
  55. crush = b.String()
  56. // Charm and version.
  57. metaRowGap := 1
  58. maxVersionWidth := crushWidth - lipgloss.Width(charm) - metaRowGap
  59. version = ansi.Truncate(version, maxVersionWidth, "…") // truncate version if too long.
  60. gap := max(0, crushWidth-lipgloss.Width(charm)-lipgloss.Width(version))
  61. metaRow := fg(o.CharmColor, charm) + strings.Repeat(" ", gap) + fg(o.VersionColor, version)
  62. // Join the meta row and big Crush title.
  63. crush = strings.TrimSpace(metaRow + "\n" + crush)
  64. // Narrow version.
  65. if compact {
  66. field := fg(o.FieldColor, strings.Repeat(diag, crushWidth))
  67. return strings.Join([]string{field, field, crush, field, ""}, "\n")
  68. }
  69. fieldHeight := lipgloss.Height(crush)
  70. // Left field.
  71. const leftWidth = 6
  72. leftFieldRow := fg(o.FieldColor, strings.Repeat(diag, leftWidth))
  73. leftField := new(strings.Builder)
  74. for range fieldHeight {
  75. fmt.Fprintln(leftField, leftFieldRow)
  76. }
  77. // Right field.
  78. rightWidth := max(15, o.Width-crushWidth-leftWidth-2) // 2 for the gap.
  79. const stepDownAt = 0
  80. rightField := new(strings.Builder)
  81. for i := range fieldHeight {
  82. width := rightWidth
  83. if i >= stepDownAt {
  84. width = rightWidth - (i - stepDownAt)
  85. }
  86. fmt.Fprint(rightField, fg(o.FieldColor, strings.Repeat(diag, width)), "\n")
  87. }
  88. // Return the wide version.
  89. const hGap = " "
  90. logo := lipgloss.JoinHorizontal(lipgloss.Top, leftField.String(), hGap, crush, hGap, rightField.String())
  91. if o.Width > 0 {
  92. // Truncate the logo to the specified width.
  93. lines := strings.Split(logo, "\n")
  94. for i, line := range lines {
  95. lines[i] = ansi.Truncate(line, o.Width, "")
  96. }
  97. logo = strings.Join(lines, "\n")
  98. }
  99. return logo
  100. }
  101. // SmallRender renders a smaller version of the Crush logo, suitable for
  102. // smaller windows or sidebar usage.
  103. func SmallRender(t *styles.Styles, width int) string {
  104. title := t.Base.Foreground(t.Secondary).Render("Charm™")
  105. title = fmt.Sprintf("%s %s", title, styles.ApplyBoldForegroundGrad(t, "Crush", t.Secondary, t.Primary))
  106. remainingWidth := width - lipgloss.Width(title) - 1 // 1 for the space after "Crush"
  107. if remainingWidth > 0 {
  108. lines := strings.Repeat("╱", remainingWidth)
  109. title = fmt.Sprintf("%s %s", title, t.Base.Foreground(t.Primary).Render(lines))
  110. }
  111. return title
  112. }
  113. // renderWord renders letterforms to fork a word. stretchIndex is the index of
  114. // the letter to stretch, or -1 if no letter should be stretched.
  115. func renderWord(spacing int, stretchIndex int, letterforms ...letterform) string {
  116. if spacing < 0 {
  117. spacing = 0
  118. }
  119. renderedLetterforms := make([]string, len(letterforms))
  120. // pick one letter randomly to stretch
  121. for i, letter := range letterforms {
  122. renderedLetterforms[i] = letter(i == stretchIndex)
  123. }
  124. if spacing > 0 {
  125. // Add spaces between the letters and render.
  126. renderedLetterforms = slice.Intersperse(renderedLetterforms, strings.Repeat(" ", spacing))
  127. }
  128. return strings.TrimSpace(
  129. lipgloss.JoinHorizontal(lipgloss.Top, renderedLetterforms...),
  130. )
  131. }
  132. // letterC renders the letter C in a stylized way. It takes an integer that
  133. // determines how many cells to stretch the letter. If the stretch is less than
  134. // 1, it defaults to no stretching.
  135. func letterC(stretch bool) string {
  136. // Here's what we're making:
  137. //
  138. // ▄▀▀▀▀
  139. // █
  140. // ▀▀▀▀
  141. left := heredoc.Doc(`
  142. `)
  143. right := heredoc.Doc(`
  144. `)
  145. return joinLetterform(
  146. left,
  147. stretchLetterformPart(right, letterformProps{
  148. stretch: stretch,
  149. width: 4,
  150. minStretch: 7,
  151. maxStretch: 12,
  152. }),
  153. )
  154. }
  155. // letterH renders the letter H in a stylized way. It takes an integer that
  156. // determines how many cells to stretch the letter. If the stretch is less than
  157. // 1, it defaults to no stretching.
  158. func letterH(stretch bool) string {
  159. // Here's what we're making:
  160. //
  161. // █ █
  162. // █▀▀▀█
  163. // ▀ ▀
  164. side := heredoc.Doc(`
  165. ▀`)
  166. middle := heredoc.Doc(`
  167. `)
  168. return joinLetterform(
  169. side,
  170. stretchLetterformPart(middle, letterformProps{
  171. stretch: stretch,
  172. width: 3,
  173. minStretch: 8,
  174. maxStretch: 12,
  175. }),
  176. side,
  177. )
  178. }
  179. // letterR renders the letter R in a stylized way. It takes an integer that
  180. // determines how many cells to stretch the letter. If the stretch is less than
  181. // 1, it defaults to no stretching.
  182. func letterR(stretch bool) string {
  183. // Here's what we're making:
  184. //
  185. // █▀▀▀▄
  186. // █▀▀▀▄
  187. // ▀ ▀
  188. left := heredoc.Doc(`
  189. `)
  190. center := heredoc.Doc(`
  191. `)
  192. right := heredoc.Doc(`
  193. `)
  194. return joinLetterform(
  195. left,
  196. stretchLetterformPart(center, letterformProps{
  197. stretch: stretch,
  198. width: 3,
  199. minStretch: 7,
  200. maxStretch: 12,
  201. }),
  202. right,
  203. )
  204. }
  205. // letterSStylized renders the letter S in a stylized way, more so than
  206. // [letterS]. It takes an integer that determines how many cells to stretch the
  207. // letter. If the stretch is less than 1, it defaults to no stretching.
  208. func letterSStylized(stretch bool) string {
  209. // Here's what we're making:
  210. //
  211. // ▄▀▀▀▀▀
  212. // ▀▀▀▀▀█
  213. // ▀▀▀▀▀
  214. left := heredoc.Doc(`
  215. `)
  216. center := heredoc.Doc(`
  217. `)
  218. right := heredoc.Doc(`
  219. `)
  220. return joinLetterform(
  221. left,
  222. stretchLetterformPart(center, letterformProps{
  223. stretch: stretch,
  224. width: 3,
  225. minStretch: 7,
  226. maxStretch: 12,
  227. }),
  228. right,
  229. )
  230. }
  231. // letterU renders the letter U in a stylized way. It takes an integer that
  232. // determines how many cells to stretch the letter. If the stretch is less than
  233. // 1, it defaults to no stretching.
  234. func letterU(stretch bool) string {
  235. // Here's what we're making:
  236. //
  237. // █ █
  238. // █ █
  239. // ▀▀▀
  240. side := heredoc.Doc(`
  241. `)
  242. middle := heredoc.Doc(`
  243. `)
  244. return joinLetterform(
  245. side,
  246. stretchLetterformPart(middle, letterformProps{
  247. stretch: stretch,
  248. width: 3,
  249. minStretch: 7,
  250. maxStretch: 12,
  251. }),
  252. side,
  253. )
  254. }
  255. func joinLetterform(letters ...string) string {
  256. return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
  257. }
  258. // letterformProps defines letterform stretching properties.
  259. // for readability.
  260. type letterformProps struct {
  261. width int
  262. minStretch int
  263. maxStretch int
  264. stretch bool
  265. }
  266. // stretchLetterformPart is a helper function for letter stretching. If randomize
  267. // is false the minimum number will be used.
  268. func stretchLetterformPart(s string, p letterformProps) string {
  269. if p.maxStretch < p.minStretch {
  270. p.minStretch, p.maxStretch = p.maxStretch, p.minStretch
  271. }
  272. n := p.width
  273. if p.stretch {
  274. n = cachedRandN(p.maxStretch-p.minStretch) + p.minStretch //nolint:gosec
  275. }
  276. parts := make([]string, n)
  277. for i := range parts {
  278. parts[i] = s
  279. }
  280. return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
  281. }