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