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