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