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}