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