header.go

  1package header
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	tea "charm.land/bubbletea/v2"
  8	"charm.land/lipgloss/v2"
  9	"github.com/charmbracelet/crush/internal/config"
 10	"github.com/charmbracelet/crush/internal/csync"
 11	"github.com/charmbracelet/crush/internal/fsext"
 12	"github.com/charmbracelet/crush/internal/lsp"
 13	"github.com/charmbracelet/crush/internal/pubsub"
 14	"github.com/charmbracelet/crush/internal/session"
 15	"github.com/charmbracelet/crush/internal/tui/styles"
 16	"github.com/charmbracelet/crush/internal/tui/util"
 17	"github.com/charmbracelet/x/ansi"
 18)
 19
 20type Header interface {
 21	util.Model
 22	SetSession(session session.Session) tea.Cmd
 23	SetWidth(width int) tea.Cmd
 24	SetDetailsOpen(open bool)
 25	ShowingDetails() bool
 26}
 27
 28type header struct {
 29	width       int
 30	session     session.Session
 31	lspClients  *csync.Map[string, *lsp.Client]
 32	detailsOpen bool
 33}
 34
 35func New(lspClients *csync.Map[string, *lsp.Client]) Header {
 36	return &header{
 37		lspClients: lspClients,
 38		width:      0,
 39	}
 40}
 41
 42func (h *header) Init() tea.Cmd {
 43	return nil
 44}
 45
 46func (h *header) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 47	switch msg := msg.(type) {
 48	case pubsub.Event[session.Session]:
 49		if msg.Type == pubsub.UpdatedEvent {
 50			if h.session.ID == msg.Payload.ID {
 51				h.session = msg.Payload
 52			}
 53		}
 54	}
 55	return h, nil
 56}
 57
 58func (h *header) View() string {
 59	if h.session.ID == "" {
 60		return ""
 61	}
 62
 63	const (
 64		gap          = " "
 65		diag         = "β•±"
 66		minDiags     = 3
 67		leftPadding  = 1
 68		rightPadding = 1
 69	)
 70
 71	t := styles.CurrentTheme()
 72
 73	var b strings.Builder
 74
 75	b.WriteString(t.S().Base.Foreground(t.Secondary).Render("Charmβ„’"))
 76	b.WriteString(gap)
 77	b.WriteString(styles.ApplyBoldForegroundGrad("CRUSH", t.Secondary, t.Primary))
 78	b.WriteString(gap)
 79
 80	availDetailWidth := h.width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minDiags
 81	details := h.details(availDetailWidth)
 82
 83	remainingWidth := h.width -
 84		lipgloss.Width(b.String()) -
 85		lipgloss.Width(details) -
 86		leftPadding -
 87		rightPadding
 88
 89	if remainingWidth > 0 {
 90		b.WriteString(t.S().Base.Foreground(t.Primary).Render(
 91			strings.Repeat(diag, max(minDiags, remainingWidth)),
 92		))
 93		b.WriteString(gap)
 94	}
 95
 96	b.WriteString(details)
 97
 98	return t.S().Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
 99}
100
101func (h *header) details(availWidth int) string {
102	s := styles.CurrentTheme().S()
103
104	var parts []string
105
106	errorCount := 0
107	for l := range h.lspClients.Seq() {
108		errorCount += l.GetDiagnosticCounts().Error
109	}
110
111	if errorCount > 0 {
112		parts = append(parts, s.Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
113	}
114
115	agentCfg := config.Get().Agents[config.AgentCoder]
116	model := config.Get().GetModelByType(agentCfg.Model)
117	percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
118	formattedPercentage := s.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
119	parts = append(parts, formattedPercentage)
120
121	const keystroke = "ctrl+d"
122	if h.detailsOpen {
123		parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" close"))
124	} else {
125		parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" open "))
126	}
127
128	dot := s.Subtle.Render(" β€’ ")
129	metadata := strings.Join(parts, dot)
130	metadata = dot + metadata
131
132	// Truncate cwd if necessary, and insert it at the beginning.
133	const dirTrimLimit = 4
134	cwd := fsext.DirTrim(fsext.PrettyPath(config.Get().WorkingDir()), dirTrimLimit)
135	cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "…")
136	cwd = s.Muted.Render(cwd)
137
138	return cwd + metadata
139}
140
141func (h *header) SetDetailsOpen(open bool) {
142	h.detailsOpen = open
143}
144
145// SetSession implements Header.
146func (h *header) SetSession(session session.Session) tea.Cmd {
147	h.session = session
148	return nil
149}
150
151// SetWidth implements Header.
152func (h *header) SetWidth(width int) tea.Cmd {
153	h.width = width
154	return nil
155}
156
157// ShowingDetails implements Header.
158func (h *header) ShowingDetails() bool {
159	return h.detailsOpen
160}