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