header.go

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