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/fsext"
 10	"github.com/charmbracelet/crush/internal/lsp"
 11	"github.com/charmbracelet/crush/internal/pubsub"
 12	"github.com/charmbracelet/crush/internal/session"
 13	"github.com/charmbracelet/crush/internal/tui/styles"
 14	"github.com/charmbracelet/crush/internal/tui/util"
 15	"github.com/charmbracelet/lipgloss/v2"
 16	"github.com/charmbracelet/x/ansi"
 17	"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
 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  map[string]*lsp.Client
 32	detailsOpen bool
 33}
 34
 35func New(lspClients 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) (tea.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 {
108		for _, diagnostics := range l.GetDiagnostics() {
109			for _, diagnostic := range diagnostics {
110				if diagnostic.Severity == protocol.SeverityError {
111					errorCount++
112				}
113			}
114		}
115	}
116
117	if errorCount > 0 {
118		parts = append(parts, s.Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
119	}
120
121	agentCfg := config.Get().Agents["coder"]
122	model := config.Get().GetModelByType(agentCfg.Model)
123	percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
124	formattedPercentage := s.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
125	parts = append(parts, formattedPercentage)
126
127	const keystroke = "ctrl+d"
128	if h.detailsOpen {
129		parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" close"))
130	} else {
131		parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" open "))
132	}
133
134	dot := s.Subtle.Render(" β€’ ")
135	metadata := strings.Join(parts, dot)
136	metadata = dot + metadata
137
138	// Truncate cwd if necessary, and insert it at the beginning.
139	const dirTrimLimit = 4
140	cwd := fsext.DirTrim(fsext.PrettyPath(config.Get().WorkingDir()), dirTrimLimit)
141	cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "…")
142	cwd = s.Muted.Render(cwd)
143
144	return cwd + metadata
145}
146
147func (h *header) SetDetailsOpen(open bool) {
148	h.detailsOpen = open
149}
150
151// SetSession implements Header.
152func (h *header) SetSession(session session.Session) tea.Cmd {
153	h.session = session
154	return nil
155}
156
157// SetWidth implements Header.
158func (h *header) SetWidth(width int) tea.Cmd {
159	h.width = width
160	return nil
161}
162
163// ShowingDetails implements Header.
164func (h *header) ShowingDetails() bool {
165	return h.detailsOpen
166}