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/lsp/protocol"
 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)
 18
 19type Header interface {
 20	util.Model
 21	SetSession(session session.Session) tea.Cmd
 22	SetWidth(width int) tea.Cmd
 23	SetDetailsOpen(open bool)
 24	ShowingDetails() bool
 25}
 26
 27type header struct {
 28	width       int
 29	session     session.Session
 30	lspClients  map[string]*lsp.Client
 31	detailsOpen bool
 32}
 33
 34func New(lspClients map[string]*lsp.Client) Header {
 35	return &header{
 36		lspClients: lspClients,
 37		width:      0,
 38	}
 39}
 40
 41func (h *header) Init() tea.Cmd {
 42	return nil
 43}
 44
 45func (p *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 46	switch msg := msg.(type) {
 47	case pubsub.Event[session.Session]:
 48		if msg.Type == pubsub.UpdatedEvent {
 49			if p.session.ID == msg.Payload.ID {
 50				p.session = msg.Payload
 51			}
 52		}
 53	}
 54	return p, nil
 55}
 56
 57func (p *header) View() string {
 58	if p.session.ID == "" {
 59		return ""
 60	}
 61
 62	t := styles.CurrentTheme()
 63	details := p.details()
 64	parts := []string{
 65		t.S().Base.Foreground(t.Secondary).Render("Charmβ„’"),
 66		" ",
 67		styles.ApplyBoldForegroundGrad("CRUSH", t.Secondary, t.Primary),
 68		" ",
 69	}
 70
 71	remainingWidth := p.width - lipgloss.Width(strings.Join(parts, "")) - lipgloss.Width(details) - 2
 72	if remainingWidth > 0 {
 73		char := "β•±"
 74		lines := strings.Repeat(char, remainingWidth)
 75		parts = append(parts, t.S().Base.Foreground(t.Primary).Render(lines), " ")
 76	}
 77
 78	parts = append(parts, details)
 79
 80	content := t.S().Base.Padding(0, 1).Render(
 81		lipgloss.JoinHorizontal(
 82			lipgloss.Left,
 83			parts...,
 84		),
 85	)
 86	return content
 87}
 88
 89func (h *header) details() string {
 90	t := styles.CurrentTheme()
 91	cwd := fsext.DirTrim(fsext.PrettyPath(config.Get().WorkingDir()), 4)
 92	parts := []string{
 93		t.S().Muted.Render(cwd),
 94	}
 95
 96	errorCount := 0
 97	for _, l := range h.lspClients {
 98		for _, diagnostics := range l.GetDiagnostics() {
 99			for _, diagnostic := range diagnostics {
100				if diagnostic.Severity == protocol.SeverityError {
101					errorCount++
102				}
103			}
104		}
105	}
106
107	if errorCount > 0 {
108		parts = append(parts, t.S().Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
109	}
110
111	agentCfg := config.Get().Agents["coder"]
112	model := config.Get().GetModelByType(agentCfg.Model)
113	percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
114	formattedPercentage := t.S().Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
115	parts = append(parts, formattedPercentage)
116
117	if h.detailsOpen {
118		parts = append(parts, t.S().Muted.Render("ctrl+d")+t.S().Subtle.Render(" close"))
119	} else {
120		parts = append(parts, t.S().Muted.Render("ctrl+d")+t.S().Subtle.Render(" open "))
121	}
122	dot := t.S().Subtle.Render(" β€’ ")
123	return strings.Join(parts, dot)
124}
125
126func (h *header) SetDetailsOpen(open bool) {
127	h.detailsOpen = open
128}
129
130// SetSession implements Header.
131func (h *header) SetSession(session session.Session) tea.Cmd {
132	h.session = session
133	return nil
134}
135
136// SetWidth implements Header.
137func (h *header) SetWidth(width int) tea.Cmd {
138	h.width = width
139	return nil
140}
141
142// ShowingDetails implements Header.
143func (h *header) ShowingDetails() bool {
144	return h.detailsOpen
145}