on main help sign in
Merge pull request #235 from charmbracelet/smaller-logo

fix(tui): render a smaller logo on splash screen

commit3247a8c80bc0adf0c41f76a3b1d58a504ce76350 authorKujtim Hoxha <kujtimii.h@gmail.com> dateTue 22 Jul 2025, 12:56 (1 year ago) parent913b91ea5b76d3
4 files changed+66−37
internal/tui/components/chat/chat.go+2−2 internal/tui/components/chat/sidebar/sidebar.go+11−21 internal/tui/components/chat/splash/splash.go+28−5 internal/tui/components/logo/logo.go+25−9
internal/tui/components/chat/chat.go+2−2
@@ -107,7 +107,7 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107107func (m *messageListCmp) View() string {
108108 t := styles.CurrentTheme()
109109 return t.S().Base.
110− Padding(1).
110+ Padding(1, 1, 0, 1).
111111 Width(m.width).
112112 Height(m.height).
113113 Render(
@@ -508,7 +508,7 @@ func (m *messageListCmp) GetSize() (int, int) {
508508func (m *messageListCmp) SetSize(width int, height int) tea.Cmd {
509509 m.width = width
510510 m.height = height
511− return m.listCmp.SetSize(width-2, height-2) // for padding
511+ return m.listCmp.SetSize(width-2, height-1) // for padding
512512}
513513
514514// Blur implements MessageListCmp.
internal/tui/components/chat/sidebar/sidebar.go+11−21
@@ -114,12 +114,22 @@ func (m *sidebarCmp) View() string {
114114 t := styles.CurrentTheme()
115115 parts := []string{}
116116
117+ style := t.S().Base.
118+ Width(m.width).
119+ Height(m.height).
120+ Padding(1)
121+ if m.compactMode {
122+ style = style.PaddingTop(0)
123+ }
124+
117125 if !m.compactMode {
118126 if m.height > LogoHeightBreakpoint {
119127 parts = append(parts, m.logo)
120128 } else {
121129 // Use a smaller logo for smaller screens
122− parts = append(parts, m.smallerScreenLogo(), "")
130+ parts = append(parts,
131+ logo.SmallRender(m.width-style.GetHorizontalFrameSize()),
132+ "")
123133 }
124134 }
125135
@@ -159,13 +169,6 @@ func (m *sidebarCmp) View() string {
159169 )
160170 }
161171
162− style := t.S().Base.
163− Width(m.width).
164− Height(m.height).
165− Padding(1)
166− if m.compactMode {
167− style = style.PaddingTop(0)
168− }
169172 return style.Render(
170173 lipgloss.JoinVertical(lipgloss.Left, parts...),
171174 )
@@ -934,19 +937,6 @@ func (s *sidebarCmp) currentModelBlock() string {
934937 )
935938}
936939
937−func (m *sidebarCmp) smallerScreenLogo() string {
938− t := styles.CurrentTheme()
939− title := t.S().Base.Foreground(t.Secondary).Render("Charm™")
940− title += " " + styles.ApplyBoldForegroundGrad("CRUSH", t.Secondary, t.Primary)
941− remainingWidth := m.width - lipgloss.Width(title) - 3
942− if remainingWidth > 0 {
943− char := "╱"
944− lines := strings.Repeat(char, remainingWidth)
945− title += " " + t.S().Base.Foreground(t.Primary).Render(lines)
946− }
947− return title
948−}
949−
950940// SetSession implements Sidebar.
951941func (m *sidebarCmp) SetSession(session session.Session) tea.Cmd {
952942 m.session = session
internal/tui/components/chat/splash/splash.go+28−5
@@ -143,9 +143,11 @@ func (s *splashCmp) Init() tea.Cmd {
143143
144144// SetSize implements SplashPage.
145145func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
146+ wasSmallScreen := s.isSmallScreen()
147+ rerenderLogo := width != s.width
146148 s.height = height
147− if width != s.width {
148− s.width = width
149+ s.width = width
150+ if rerenderLogo || wasSmallScreen != s.isSmallScreen() {
149151 s.logoRendered = s.logoBlock()
150152 }
151153 // remove padding, logo height, gap, title space
@@ -541,9 +543,19 @@ func (s *splashCmp) Cursor() *tea.Cursor {
541543 return nil
542544}
543545
546+func (s *splashCmp) isSmallScreen() bool {
547+ // Consider a screen small if either the width is less than 40 or if the
548+ // height is less than 20
549+ return s.width < 55 || s.height < 20
550+}
551+
544552func (s *splashCmp) infoSection() string {
545553 t := styles.CurrentTheme()
546− return t.S().Base.PaddingLeft(2).Render(
554+ infoStyle := t.S().Base.PaddingLeft(2)
555+ if s.isSmallScreen() {
556+ infoStyle = infoStyle.MarginTop(1)
557+ }
558+ return infoStyle.Render(
547559 lipgloss.JoinVertical(
548560 lipgloss.Left,
549561 s.cwd(),
@@ -556,14 +568,25 @@ func (s *splashCmp) infoSection() string {
556568
557569func (s *splashCmp) logoBlock() string {
558570 t := styles.CurrentTheme()
559− return t.S().Base.Padding(0, 2).Width(s.width).Render(
571+ logoStyle := t.S().Base.Padding(0, 2).Width(s.width)
572+ if s.isSmallScreen() {
573+ // If the width is too small, render a smaller version of the logo
574+ // NOTE: 20 is not correct because [splashCmp.height] is not the
575+ // *actual* window height, instead, it is the height of the splash
576+ // component and that depends on other variables like compact mode and
577+ // the height of the editor.
578+ return logoStyle.Render(
579+ logo.SmallRender(s.width - logoStyle.GetHorizontalFrameSize()),
580+ )
581+ }
582+ return logoStyle.Render(
560583 logo.Render(version.Version, false, logo.Opts{
561584 FieldColor: t.Primary,
562585 TitleColorA: t.Secondary,
563586 TitleColorB: t.Primary,
564587 CharmColor: t.Secondary,
565588 VersionColor: t.Primary,
566− Width: s.width - 4,
589+ Width: s.width - logoStyle.GetHorizontalFrameSize(),
567590 }),
568591 )
569592}
internal/tui/components/logo/logo.go+25−9
@@ -44,13 +44,19 @@ func Render(version string, compact bool, o Opts) string {
4444
4545 // Title.
4646 const spacing = 1
47− crush := renderWord(spacing, !compact,
47+ letterforms := []letterform{
4848 letterC,
4949 letterR,
5050 letterU,
5151 letterSStylized,
5252 letterH,
53− )
53+ }
54+ stretchIndex := -1 // -1 means no stretching.
55+ if !compact {
56+ stretchIndex = rand.IntN(len(letterforms))
57+ }
58+
59+ crush := renderWord(spacing, stretchIndex, letterforms...)
5460 crushWidth := lipgloss.Width(crush)
5561 b := new(strings.Builder)
5662 for r := range strings.SplitSeq(crush, "\n") {
@@ -110,8 +116,23 @@ func Render(version string, compact bool, o Opts) string {
110116 return logo
111117}
112118
113−// renderWord renders letterforms to fork a word.
114−func renderWord(spacing int, stretchRandomLetter bool, letterforms ...letterform) string {
119+// SmallRender renders a smaller version of the Crush logo, suitable for
120+// smaller windows or sidebar usage.
121+func SmallRender(width int) string {
122+ t := styles.CurrentTheme()
123+ title := t.S().Base.Foreground(t.Secondary).Render("Charm™")
124+ title = fmt.Sprintf("%s %s", title, styles.ApplyBoldForegroundGrad("Crush", t.Secondary, t.Primary))
125+ remainingWidth := width - lipgloss.Width(title) - 1 // 1 for the space after "Crush"
126+ if remainingWidth > 0 {
127+ lines := strings.Repeat("╱", remainingWidth)
128+ title = fmt.Sprintf("%s %s", title, t.S().Base.Foreground(t.Primary).Render(lines))
129+ }
130+ return title
131+}
132+
133+// renderWord renders letterforms to fork a word. stretchIndex is the index of
134+// the letter to stretch, or -1 if no letter should be stretched.
135+func renderWord(spacing int, stretchIndex int, letterforms ...letterform) string {
115136 if spacing < 0 {
116137 spacing = 0
117138 }
@@ -119,11 +140,6 @@ func renderWord(spacing int, stretchRandomLetter bool, letterforms ...letterform
119140 renderedLetterforms := make([]string, len(letterforms))
120141
121142 // pick one letter randomly to stretch
122− stretchIndex := -1
123− if stretchRandomLetter {
124− stretchIndex = rand.IntN(len(letterforms)) //nolint:gosec
125− }
126−
127143 for i, letter := range letterforms {
128144 renderedLetterforms[i] = letter(i == stretchIndex)
129145 }