logo.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. // Package logo renders a Crush wordmark in a stylized way.
  2. package logo
  3. import (
  4. "fmt"
  5. "image/color"
  6. "strings"
  7. "github.com/MakeNowJust/heredoc"
  8. "github.com/charmbracelet/crush/internal/tui/styles"
  9. "github.com/charmbracelet/lipgloss/v2"
  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(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(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(width int) string {
  104. t := styles.CurrentTheme()
  105. title := t.S().Base.Foreground(t.Secondary).Render("Charm™")
  106. title = fmt.Sprintf("%s %s", title, styles.ApplyBoldForegroundGrad("Crush", t.Secondary, t.Primary))
  107. remainingWidth := width - lipgloss.Width(title) - 1 // 1 for the space after "Crush"
  108. if remainingWidth > 0 {
  109. lines := strings.Repeat("╱", remainingWidth)
  110. title = fmt.Sprintf("%s %s", title, t.S().Base.Foreground(t.Primary).Render(lines))
  111. }
  112. return title
  113. }
  114. // renderWord renders letterforms to fork a word. stretchIndex is the index of
  115. // the letter to stretch, or -1 if no letter should be stretched.
  116. func renderWord(spacing int, stretchIndex int, letterforms ...letterform) string {
  117. if spacing < 0 {
  118. spacing = 0
  119. }
  120. renderedLetterforms := make([]string, len(letterforms))
  121. // pick one letter randomly to stretch
  122. for i, letter := range letterforms {
  123. renderedLetterforms[i] = letter(i == stretchIndex)
  124. }
  125. if spacing > 0 {
  126. // Add spaces between the letters and render.
  127. renderedLetterforms = slice.Intersperse(renderedLetterforms, strings.Repeat(" ", spacing))
  128. }
  129. return strings.TrimSpace(
  130. lipgloss.JoinHorizontal(lipgloss.Top, renderedLetterforms...),
  131. )
  132. }
  133. // letterC renders the letter C in a stylized way. It takes an integer that
  134. // determines how many cells to stretch the letter. If the stretch is less than
  135. // 1, it defaults to no stretching.
  136. func letterC(stretch bool) string {
  137. // Here's what we're making:
  138. //
  139. // ▄▀▀▀▀
  140. // █
  141. // ▀▀▀▀
  142. left := heredoc.Doc(`
  143. `)
  144. right := heredoc.Doc(`
  145. `)
  146. return joinLetterform(
  147. left,
  148. stretchLetterformPart(right, letterformProps{
  149. stretch: stretch,
  150. width: 4,
  151. minStretch: 7,
  152. maxStretch: 12,
  153. }),
  154. )
  155. }
  156. // letterH renders the letter H in a stylized way. It takes an integer that
  157. // determines how many cells to stretch the letter. If the stretch is less than
  158. // 1, it defaults to no stretching.
  159. func letterH(stretch bool) string {
  160. // Here's what we're making:
  161. //
  162. // █ █
  163. // █▀▀▀█
  164. // ▀ ▀
  165. side := heredoc.Doc(`
  166. ▀`)
  167. middle := heredoc.Doc(`
  168. `)
  169. return joinLetterform(
  170. side,
  171. stretchLetterformPart(middle, letterformProps{
  172. stretch: stretch,
  173. width: 3,
  174. minStretch: 8,
  175. maxStretch: 12,
  176. }),
  177. side,
  178. )
  179. }
  180. // letterR renders the letter R in a stylized way. It takes an integer that
  181. // determines how many cells to stretch the letter. If the stretch is less than
  182. // 1, it defaults to no stretching.
  183. func letterR(stretch bool) string {
  184. // Here's what we're making:
  185. //
  186. // █▀▀▀▄
  187. // █▀▀▀▄
  188. // ▀ ▀
  189. left := heredoc.Doc(`
  190. `)
  191. center := heredoc.Doc(`
  192. `)
  193. right := heredoc.Doc(`
  194. `)
  195. return joinLetterform(
  196. left,
  197. stretchLetterformPart(center, letterformProps{
  198. stretch: stretch,
  199. width: 3,
  200. minStretch: 7,
  201. maxStretch: 12,
  202. }),
  203. right,
  204. )
  205. }
  206. // letterSStylized renders the letter S in a stylized way, more so than
  207. // [letterS]. It takes an integer that determines how many cells to stretch the
  208. // letter. If the stretch is less than 1, it defaults to no stretching.
  209. func letterSStylized(stretch bool) string {
  210. // Here's what we're making:
  211. //
  212. // ▄▀▀▀▀▀
  213. // ▀▀▀▀▀█
  214. // ▀▀▀▀▀
  215. left := heredoc.Doc(`
  216. `)
  217. center := heredoc.Doc(`
  218. `)
  219. right := heredoc.Doc(`
  220. `)
  221. return joinLetterform(
  222. left,
  223. stretchLetterformPart(center, letterformProps{
  224. stretch: stretch,
  225. width: 3,
  226. minStretch: 7,
  227. maxStretch: 12,
  228. }),
  229. right,
  230. )
  231. }
  232. // letterU renders the letter U in a stylized way. It takes an integer that
  233. // determines how many cells to stretch the letter. If the stretch is less than
  234. // 1, it defaults to no stretching.
  235. func letterU(stretch bool) string {
  236. // Here's what we're making:
  237. //
  238. // █ █
  239. // █ █
  240. // ▀▀▀
  241. side := heredoc.Doc(`
  242. `)
  243. middle := heredoc.Doc(`
  244. `)
  245. return joinLetterform(
  246. side,
  247. stretchLetterformPart(middle, letterformProps{
  248. stretch: stretch,
  249. width: 3,
  250. minStretch: 7,
  251. maxStretch: 12,
  252. }),
  253. side,
  254. )
  255. }
  256. func joinLetterform(letters ...string) string {
  257. return lipgloss.JoinHorizontal(lipgloss.Top, letters...)
  258. }
  259. // letterformProps defines letterform stretching properties.
  260. // for readability.
  261. type letterformProps struct {
  262. width int
  263. minStretch int
  264. maxStretch int
  265. stretch bool
  266. }
  267. // stretchLetterformPart is a helper function for letter stretching. If randomize
  268. // is false the minimum number will be used.
  269. func stretchLetterformPart(s string, p letterformProps) string {
  270. if p.maxStretch < p.minStretch {
  271. p.minStretch, p.maxStretch = p.maxStretch, p.minStretch
  272. }
  273. n := p.width
  274. if p.stretch {
  275. n = cachedRandN(p.maxStretch-p.minStretch) + p.minStretch //nolint:gosec
  276. }
  277. parts := make([]string, n)
  278. for i := range parts {
  279. parts[i] = s
  280. }
  281. return lipgloss.JoinHorizontal(lipgloss.Top, parts...)
  282. }