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/fsext"
11 "github.com/charmbracelet/crush/internal/proto"
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 ins *proto.Instance
34 detailsOpen bool
35}
36
37func New(lspClients *client.Client, ins *proto.Instance) Header {
38 return &header{
39 client: lspClients,
40 ins: ins,
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(), h.ins.ID)
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(), h.ins.ID, 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.ins.Config.Agents["coder"]
136 model := h.ins.Config.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.ins.Config.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}