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