1package header
  2
  3import (
  4	"context"
  5	"fmt"
  6	"strings"
  7
  8	tea "github.com/charmbracelet/bubbletea/v2"
  9	"github.com/charmbracelet/crush/internal/client"
 10	"github.com/charmbracelet/crush/internal/config"
 11	"github.com/charmbracelet/crush/internal/fsext"
 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	"github.com/charmbracelet/x/ansi"
 18	"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
 19)
 20
 21type Header interface {
 22	util.Model
 23	SetSession(session session.Session) tea.Cmd
 24	SetWidth(width int) tea.Cmd
 25	SetDetailsOpen(open bool)
 26	ShowingDetails() bool
 27}
 28
 29type header struct {
 30	width       int
 31	session     session.Session
 32	client      *client.Client
 33	cfg         *config.Config
 34	detailsOpen bool
 35}
 36
 37func New(lspClients *client.Client, cfg *config.Config) Header {
 38	return &header{
 39		client: lspClients,
 40		cfg:    cfg,
 41		width:  0,
 42	}
 43}
 44
 45func (h *header) Init() tea.Cmd {
 46	return nil
 47}
 48
 49func (h *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 50	switch msg := msg.(type) {
 51	case pubsub.Event[session.Session]:
 52		if msg.Type == pubsub.UpdatedEvent {
 53			if h.session.ID == msg.Payload.ID {
 54				h.session = msg.Payload
 55			}
 56		}
 57	}
 58	return h, nil
 59}
 60
 61func (h *header) View() string {
 62	if h.session.ID == "" {
 63		return ""
 64	}
 65
 66	const (
 67		gap          = " "
 68		diag         = "β±"
 69		minDiags     = 3
 70		leftPadding  = 1
 71		rightPadding = 1
 72	)
 73
 74	t := styles.CurrentTheme()
 75
 76	var b strings.Builder
 77
 78	b.WriteString(t.S().Base.Foreground(t.Secondary).Render("Charmβ’"))
 79	b.WriteString(gap)
 80	b.WriteString(styles.ApplyBoldForegroundGrad("CRUSH", t.Secondary, t.Primary))
 81	b.WriteString(gap)
 82
 83	availDetailWidth := h.width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minDiags
 84	details := h.details(availDetailWidth)
 85
 86	remainingWidth := h.width -
 87		lipgloss.Width(b.String()) -
 88		lipgloss.Width(details) -
 89		leftPadding -
 90		rightPadding
 91
 92	if remainingWidth > 0 {
 93		b.WriteString(t.S().Base.Foreground(t.Primary).Render(
 94			strings.Repeat(diag, max(minDiags, remainingWidth)),
 95		))
 96		b.WriteString(gap)
 97	}
 98
 99	b.WriteString(details)
100
101	return t.S().Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
102}
103
104func (h *header) details(availWidth int) string {
105	s := styles.CurrentTheme().S()
106
107	var parts []string
108
109	errorCount := 0
110	// TODO: Move this to update?
111	lsps, err := h.client.GetLSPs(context.TODO())
112	if err != nil {
113		return ""
114	}
115
116	for l := range lsps {
117		// TODO: Same here, move to update?
118		diags, err := h.client.GetLSPDiagnostics(context.TODO(), l)
119		if err != nil {
120			return ""
121		}
122		for _, diagnostics := range diags {
123			for _, diagnostic := range diagnostics {
124				if diagnostic.Severity == protocol.SeverityError {
125					errorCount++
126				}
127			}
128		}
129	}
130
131	if errorCount > 0 {
132		parts = append(parts, s.Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
133	}
134
135	agentCfg := h.cfg.Agents["coder"]
136	model := h.cfg.GetModelByType(agentCfg.Model)
137	if model == nil {
138		return "No model"
139	}
140	percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
141	formattedPercentage := s.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
142	parts = append(parts, formattedPercentage)
143
144	const keystroke = "ctrl+d"
145	if h.detailsOpen {
146		parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" close"))
147	} else {
148		parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" open "))
149	}
150
151	dot := s.Subtle.Render(" β’ ")
152	metadata := strings.Join(parts, dot)
153	metadata = dot + metadata
154
155	// Truncate cwd if necessary, and insert it at the beginning.
156	const dirTrimLimit = 4
157	cwd := fsext.DirTrim(fsext.PrettyPath(h.cfg.WorkingDir()), dirTrimLimit)
158	cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "β¦")
159	cwd = s.Muted.Render(cwd)
160
161	return cwd + metadata
162}
163
164func (h *header) SetDetailsOpen(open bool) {
165	h.detailsOpen = open
166}
167
168// SetSession implements Header.
169func (h *header) SetSession(session session.Session) tea.Cmd {
170	h.session = session
171	return nil
172}
173
174// SetWidth implements Header.
175func (h *header) SetWidth(width int) tea.Cmd {
176	h.width = width
177	return nil
178}
179
180// ShowingDetails implements Header.
181func (h *header) ShowingDetails() bool {
182	return h.detailsOpen
183}