1package header
2
3import (
4 "fmt"
5 "strings"
6
7 tea "charm.land/bubbletea/v2"
8 "charm.land/lipgloss/v2"
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/csync"
11 "github.com/charmbracelet/crush/internal/fsext"
12 "github.com/charmbracelet/crush/internal/lsp"
13 "github.com/charmbracelet/crush/internal/pubsub"
14 "github.com/charmbracelet/crush/internal/session"
15 "github.com/charmbracelet/crush/internal/tui/styles"
16 "github.com/charmbracelet/crush/internal/tui/util"
17 "github.com/charmbracelet/x/ansi"
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 *csync.Map[string, *lsp.Client]
32 detailsOpen bool
33}
34
35func New(lspClients *csync.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) (util.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.Seq() {
108 errorCount += l.GetDiagnosticCounts().Error
109 }
110
111 if errorCount > 0 {
112 parts = append(parts, s.Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
113 }
114
115 agentCfg := config.Get().Agents[config.AgentCoder]
116 model := config.Get().GetModelByType(agentCfg.Model)
117 percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
118 formattedPercentage := s.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
119 parts = append(parts, formattedPercentage)
120
121 const keystroke = "ctrl+d"
122 if h.detailsOpen {
123 parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" close"))
124 } else {
125 parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" open "))
126 }
127
128 dot := s.Subtle.Render(" β’ ")
129 metadata := strings.Join(parts, dot)
130 metadata = dot + metadata
131
132 // Truncate cwd if necessary, and insert it at the beginning.
133 const dirTrimLimit = 4
134 cwd := fsext.DirTrim(fsext.PrettyPath(config.Get().WorkingDir()), dirTrimLimit)
135 cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "β¦")
136 cwd = s.Muted.Render(cwd)
137
138 return cwd + metadata
139}
140
141func (h *header) SetDetailsOpen(open bool) {
142 h.detailsOpen = open
143}
144
145// SetSession implements Header.
146func (h *header) SetSession(session session.Session) tea.Cmd {
147 h.session = session
148 return nil
149}
150
151// SetWidth implements Header.
152func (h *header) SetWidth(width int) tea.Cmd {
153 h.width = width
154 return nil
155}
156
157// ShowingDetails implements Header.
158func (h *header) ShowingDetails() bool {
159 return h.detailsOpen
160}