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