header.go

  1package model
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"charm.land/lipgloss/v2"
  8	"github.com/charmbracelet/crush/internal/config"
  9	"github.com/charmbracelet/crush/internal/fsext"
 10	"github.com/charmbracelet/crush/internal/session"
 11	"github.com/charmbracelet/crush/internal/ui/common"
 12	"github.com/charmbracelet/crush/internal/ui/styles"
 13	uv "github.com/charmbracelet/ultraviolet"
 14	"github.com/charmbracelet/x/ansi"
 15)
 16
 17const (
 18	headerDiag           = "β•±"
 19	minHeaderDiags       = 3
 20	leftPadding          = 1
 21	rightPadding         = 1
 22	diagToDetailsSpacing = 1 // space between diagonal pattern and details section
 23)
 24
 25type header struct {
 26	// cached logo and compact logo
 27	logo        string
 28	compactLogo string
 29
 30	com     *common.Common
 31	width   int
 32	compact bool
 33}
 34
 35// newHeader creates a new header model.
 36func newHeader(com *common.Common) *header {
 37	h := &header{
 38		com: com,
 39	}
 40	t := com.Styles
 41	h.compactLogo = t.Header.Charm.Render("Charmβ„’") + " " +
 42		styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary) + " "
 43	return h
 44}
 45
 46// drawHeader draws the header for the given session.
 47func (h *header) drawHeader(
 48	scr uv.Screen,
 49	area uv.Rectangle,
 50	session *session.Session,
 51	compact bool,
 52	detailsOpen bool,
 53	width int,
 54) {
 55	t := h.com.Styles
 56	if width != h.width || compact != h.compact {
 57		h.logo = renderLogo(h.com.Styles, compact, width)
 58	}
 59
 60	h.width = width
 61	h.compact = compact
 62
 63	if !compact || session == nil {
 64		uv.NewStyledString(h.logo).Draw(scr, area)
 65		return
 66	}
 67
 68	if session.ID == "" {
 69		return
 70	}
 71
 72	var b strings.Builder
 73	b.WriteString(h.compactLogo)
 74
 75	availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags - diagToDetailsSpacing
 76	lspErrorCount := 0
 77	for _, info := range h.com.Workspace.LSPGetStates() {
 78		lspErrorCount += info.DiagnosticCount
 79	}
 80	details := renderHeaderDetails(
 81		h.com,
 82		session,
 83		lspErrorCount,
 84		detailsOpen,
 85		availDetailWidth,
 86	)
 87
 88	remainingWidth := width -
 89		lipgloss.Width(b.String()) -
 90		lipgloss.Width(details) -
 91		leftPadding -
 92		rightPadding -
 93		diagToDetailsSpacing
 94
 95	if remainingWidth > 0 {
 96		b.WriteString(t.Header.Diagonals.Render(
 97			strings.Repeat(headerDiag, max(minHeaderDiags, remainingWidth)),
 98		))
 99		b.WriteString(" ")
100	}
101
102	b.WriteString(details)
103
104	view := uv.NewStyledString(
105		t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
106	view.Draw(scr, area)
107}
108
109// renderHeaderDetails renders the details section of the header.
110func renderHeaderDetails(
111	com *common.Common,
112	session *session.Session,
113	lspErrorCount int,
114	detailsOpen bool,
115	availWidth int,
116) string {
117	t := com.Styles
118
119	var parts []string
120
121	if lspErrorCount > 0 {
122		parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPErrorIcon, lspErrorCount)))
123	}
124
125	agentCfg := com.Config().Agents[config.AgentCoder]
126	model := com.Config().GetModelByType(agentCfg.Model)
127	if model != nil && model.ContextWindow > 0 {
128		percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
129		formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
130		parts = append(parts, formattedPercentage)
131	}
132
133	const keystroke = "ctrl+d"
134	if detailsOpen {
135		parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" close"))
136	} else {
137		parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" open "))
138	}
139
140	dot := t.Header.Separator.Render(" β€’ ")
141	metadata := strings.Join(parts, dot)
142	metadata = dot + metadata
143
144	const dirTrimLimit = 4
145	cwd := fsext.DirTrim(fsext.PrettyPath(com.Workspace.WorkingDir()), dirTrimLimit)
146	cwd = t.Header.WorkingDir.Render(cwd)
147
148	result := cwd + metadata
149	return ansi.Truncate(result, max(0, availWidth), "…")
150}