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	"github.com/charmbracelet/x/ansi"
 16)
 17
 18const (
 19	headerDiag     = "β•±"
 20	minHeaderDiags = 3
 21	leftPadding    = 1
 22	rightPadding   = 1
 23)
 24
 25// renderCompactHeader renders the compact header for the given session.
 26func renderCompactHeader(
 27	com *common.Common,
 28	session *session.Session,
 29	lspClients *csync.Map[string, *lsp.Client],
 30	detailsOpen bool,
 31	width int,
 32) string {
 33	if session == nil || session.ID == "" {
 34		return ""
 35	}
 36
 37	t := com.Styles
 38
 39	var b strings.Builder
 40
 41	b.WriteString(t.Header.Charm.Render("Charmβ„’"))
 42	b.WriteString(" ")
 43	b.WriteString(styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary))
 44	b.WriteString(" ")
 45
 46	availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags
 47	details := renderHeaderDetails(com, session, lspClients, detailsOpen, availDetailWidth)
 48
 49	remainingWidth := width -
 50		lipgloss.Width(b.String()) -
 51		lipgloss.Width(details) -
 52		leftPadding -
 53		rightPadding
 54
 55	if remainingWidth > 0 {
 56		b.WriteString(t.Header.Diagonals.Render(
 57			strings.Repeat(headerDiag, max(minHeaderDiags, remainingWidth)),
 58		))
 59		b.WriteString(" ")
 60	}
 61
 62	b.WriteString(details)
 63
 64	return t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
 65}
 66
 67// renderHeaderDetails renders the details section of the header.
 68func renderHeaderDetails(
 69	com *common.Common,
 70	session *session.Session,
 71	lspClients *csync.Map[string, *lsp.Client],
 72	detailsOpen bool,
 73	availWidth int,
 74) string {
 75	t := com.Styles
 76
 77	var parts []string
 78
 79	errorCount := 0
 80	for l := range lspClients.Seq() {
 81		errorCount += l.GetDiagnosticCounts().Error
 82	}
 83
 84	if errorCount > 0 {
 85		parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
 86	}
 87
 88	agentCfg := config.Get().Agents[config.AgentCoder]
 89	model := config.Get().GetModelByType(agentCfg.Model)
 90	percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
 91	formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
 92	parts = append(parts, formattedPercentage)
 93
 94	const keystroke = "ctrl+d"
 95	if detailsOpen {
 96		parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" close"))
 97	} else {
 98		parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" open "))
 99	}
100
101	dot := t.Header.Separator.Render(" β€’ ")
102	metadata := strings.Join(parts, dot)
103	metadata = dot + metadata
104
105	const dirTrimLimit = 4
106	cfg := com.Config()
107	cwd := fsext.DirTrim(fsext.PrettyPath(cfg.WorkingDir()), dirTrimLimit)
108	cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "…")
109	cwd = t.Header.WorkingDir.Render(cwd)
110
111	return cwd + metadata
112}