@@ -12,6 +12,7 @@ import (
"github.com/charmbracelet/crush/internal/session"
"github.com/charmbracelet/crush/internal/ui/common"
"github.com/charmbracelet/crush/internal/ui/styles"
+ uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/x/ansi"
)
@@ -22,29 +23,58 @@ const (
rightPadding = 1
)
-// renderCompactHeader renders the compact header for the given session.
-func renderCompactHeader(
- com *common.Common,
+type header struct {
+ // cached logo and compact logo
+ logo string
+ compactLogo string
+
+ com *common.Common
+ width int
+ compact bool
+}
+
+// newHeader creates a new header model.
+func newHeader(com *common.Common) *header {
+ h := &header{
+ com: com,
+ }
+ t := com.Styles
+ h.compactLogo = t.Header.Charm.Render("Charm™") + " " +
+ styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary) + " "
+ return h
+}
+
+// drawHeader draws the header for the given session.
+func (h *header) drawHeader(
+ scr uv.Screen,
+ area uv.Rectangle,
session *session.Session,
- lspClients *csync.Map[string, *lsp.Client],
+ compact bool,
detailsOpen bool,
width int,
-) string {
- if session == nil || session.ID == "" {
- return ""
+) {
+ t := h.com.Styles
+ if width != h.width || compact != h.compact {
+ h.logo = renderLogo(h.com.Styles, compact, width)
}
- t := com.Styles
+ h.width = width
+ h.compact = compact
- var b strings.Builder
+ if !compact || session == nil || h.com.App == nil {
+ uv.NewStyledString(h.logo).Draw(scr, area)
+ return
+ }
+
+ if session.ID == "" {
+ return
+ }
- b.WriteString(t.Header.Charm.Render("Charm™"))
- b.WriteString(" ")
- b.WriteString(styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary))
- b.WriteString(" ")
+ var b strings.Builder
+ b.WriteString(h.compactLogo)
availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags
- details := renderHeaderDetails(com, session, lspClients, detailsOpen, availDetailWidth)
+ details := renderHeaderDetails(h.com, session, h.com.App.LSPClients, detailsOpen, availDetailWidth)
remainingWidth := width -
lipgloss.Width(b.String()) -
@@ -61,7 +91,9 @@ func renderCompactHeader(
b.WriteString(details)
- return t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
+ view := uv.NewStyledString(
+ t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
+ view.Draw(scr, area)
}
// renderHeaderDetails renders the details section of the header.
@@ -141,8 +141,7 @@ type UI struct {
// isCanceling tracks whether the user has pressed escape once to cancel.
isCanceling bool
- // header is the last cached header logo
- header string
+ header *header
// sendProgressBar instructs the TUI to send progress bar updates to the
// terminal.
@@ -261,12 +260,15 @@ func New(com *common.Common) *UI {
},
)
+ header := newHeader(com)
+
ui := &UI{
com: com,
dialog: dialog.NewOverlay(),
keyMap: keyMap,
textarea: ta,
chat: ch,
+ header: header,
completions: comp,
attachments: attachments,
todoSpinner: todoSpinner,
@@ -325,6 +327,10 @@ func (m *UI) Init() tea.Cmd {
// setState changes the UI state and focus.
func (m *UI) setState(state uiState, focus uiFocusState) {
+ if state == uiLanding {
+ // Always turn off compact mode when going to landing
+ m.isCompact = false
+ }
m.state = state
m.focus = focus
// Changing the state may change layout, so update it.
@@ -1761,6 +1767,18 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
return tea.Batch(cmds...)
}
+// drawHeader draws the header section of the UI.
+func (m *UI) drawHeader(scr uv.Screen, area uv.Rectangle) {
+ m.header.drawHeader(
+ scr,
+ area,
+ m.session,
+ m.isCompact,
+ m.detailsOpen,
+ m.width,
+ )
+}
+
// Draw implements [uv.Drawable] and draws the UI model.
func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
layout := m.generateLayout(area.Dx(), area.Dy())
@@ -1775,22 +1793,19 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
switch m.state {
case uiOnboarding:
- header := uv.NewStyledString(m.header)
- header.Draw(scr, layout.header)
+ m.drawHeader(scr, layout.header)
// NOTE: Onboarding flow will be rendered as dialogs below, but
// positioned at the bottom left of the screen.
case uiInitialize:
- header := uv.NewStyledString(m.header)
- header.Draw(scr, layout.header)
+ m.drawHeader(scr, layout.header)
main := uv.NewStyledString(m.initializeView())
main.Draw(scr, layout.main)
case uiLanding:
- header := uv.NewStyledString(m.header)
- header.Draw(scr, layout.header)
+ m.drawHeader(scr, layout.header)
main := uv.NewStyledString(m.landingView())
main.Draw(scr, layout.main)
@@ -1799,8 +1814,7 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
case uiChat:
if m.isCompact {
- header := uv.NewStyledString(m.header)
- header.Draw(scr, layout.header)
+ m.drawHeader(scr, layout.header)
} else {
m.drawSidebar(scr, layout.sidebar)
}
@@ -2177,14 +2191,9 @@ func (m *UI) updateSize() {
// Handle different app states
switch m.state {
- case uiOnboarding, uiInitialize, uiLanding:
- m.renderHeader(false, m.layout.header.Dx())
-
case uiChat:
- if m.isCompact {
- m.renderHeader(true, m.layout.header.Dx())
- } else {
- m.renderSidebarLogo(m.layout.sidebar.Dx())
+ if !m.isCompact {
+ m.cacheSidebarLogo(m.layout.sidebar.Dx())
}
}
}
@@ -2590,18 +2599,8 @@ func (m *UI) renderEditorView(width int) string {
)
}
-// renderHeader renders and caches the header logo at the specified width.
-func (m *UI) renderHeader(compact bool, width int) {
- if compact && m.session != nil && m.com.App != nil {
- m.header = renderCompactHeader(m.com, m.session, m.com.App.LSPClients, m.detailsOpen, width)
- } else {
- m.header = renderLogo(m.com.Styles, compact, width)
- }
-}
-
-// renderSidebarLogo renders and caches the sidebar logo at the specified
-// width.
-func (m *UI) renderSidebarLogo(width int) {
+// cacheSidebarLogo renders and caches the sidebar logo at the specified width.
+func (m *UI) cacheSidebarLogo(width int) {
m.sidebarLogo = renderLogo(m.com.Styles, true, width)
}