header.go

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