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