1package model
2
3import (
4 "fmt"
5 "strings"
6
7 "charm.land/lipgloss/v2"
8 "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/fsext"
10 "github.com/charmbracelet/crush/internal/session"
11 "github.com/charmbracelet/crush/internal/ui/common"
12 "github.com/charmbracelet/crush/internal/ui/styles"
13 uv "github.com/charmbracelet/ultraviolet"
14 "github.com/charmbracelet/x/ansi"
15)
16
17const (
18 headerDiag = "β±"
19 minHeaderDiags = 3
20 leftPadding = 1
21 rightPadding = 1
22 diagToDetailsSpacing = 1 // space between diagonal pattern and details section
23)
24
25type header struct {
26 // cached logo and compact logo
27 logo string
28 compactLogo string
29
30 com *common.Common
31 width int
32 compact bool
33}
34
35// newHeader creates a new header model.
36func newHeader(com *common.Common) *header {
37 h := &header{
38 com: com,
39 }
40 h.refresh()
41 return h
42}
43
44// refresh rebuilds cached logo strings using the current styles. Call
45// after the theme changes.
46func (h *header) refresh() {
47 t := h.com.Styles
48 isHyper := h.com.IsHyper()
49 charm := "Charmβ’"
50 if !isHyper {
51 charm = " " + charm
52 }
53 name := "CRUSH"
54 if isHyper {
55 name = "HYPERCRUSH"
56 }
57 h.compactLogo = t.Header.Charm.Render(charm) + " " +
58 styles.ApplyBoldForegroundGrad(t.Header.LogoGradCanvas, name, t.Header.LogoGradFromColor, t.Header.LogoGradToColor) + " "
59 // Force drawHeader to re-render the wide logo on the next frame.
60 h.width = 0
61 h.logo = ""
62}
63
64// drawHeader draws the header for the given session.
65func (h *header) drawHeader(
66 scr uv.Screen,
67 area uv.Rectangle,
68 session *session.Session,
69 compact bool,
70 detailsOpen bool,
71 width int,
72 hyperCredits *int,
73) {
74 t := h.com.Styles
75 if width != h.width || compact != h.compact {
76 h.logo = renderLogo(h.com.Styles, compact, h.com.IsHyper(), width)
77 }
78
79 h.width = width
80 h.compact = compact
81
82 if !compact || session == nil {
83 uv.NewStyledString(h.logo).Draw(scr, area)
84 return
85 }
86
87 if session.ID == "" {
88 return
89 }
90
91 var b strings.Builder
92 b.WriteString(h.compactLogo)
93
94 availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags - diagToDetailsSpacing
95 lspErrorCount := 0
96 for _, info := range h.com.Workspace.LSPGetStates() {
97 lspErrorCount += info.DiagnosticCount
98 }
99 details := renderHeaderDetails(
100 h.com,
101 session,
102 lspErrorCount,
103 detailsOpen,
104 availDetailWidth,
105 hyperCredits,
106 )
107
108 remainingWidth := width -
109 lipgloss.Width(b.String()) -
110 lipgloss.Width(details) -
111 leftPadding -
112 rightPadding -
113 diagToDetailsSpacing
114
115 if remainingWidth > 0 {
116 b.WriteString(t.Header.Diagonals.Render(
117 strings.Repeat(headerDiag, max(minHeaderDiags, remainingWidth)),
118 ))
119 b.WriteString(" ")
120 }
121
122 b.WriteString(details)
123
124 view := uv.NewStyledString(
125 t.Header.Wrapper.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
126 view.Draw(scr, area)
127}
128
129// renderHeaderDetails renders the details section of the header.
130func renderHeaderDetails(
131 com *common.Common,
132 session *session.Session,
133 lspErrorCount int,
134 detailsOpen bool,
135 availWidth int,
136 hyperCredits *int,
137) string {
138 t := com.Styles
139
140 var parts []string
141
142 if lspErrorCount > 0 {
143 parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPErrorIcon, lspErrorCount)))
144 }
145
146 agentCfg := com.Config().Agents[config.AgentCoder]
147 model := com.Config().GetModelByType(agentCfg.Model)
148 if model != nil && model.ContextWindow > 0 {
149 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
150 formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
151 parts = append(parts, formattedPercentage)
152 }
153
154 if com.IsHyper() && hyperCredits != nil {
155 hc := t.Header.Hypercredit.Render(styles.HypercreditIcon) + " " + t.Header.Percentage.Render(common.FormatCredits(*hyperCredits))
156 parts = append(parts, hc)
157 }
158
159 const keystroke = "ctrl+d"
160 if detailsOpen {
161 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" close"))
162 } else {
163 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" open "))
164 }
165
166 dot := t.Header.Separator.Render(" β’ ")
167 metadata := strings.Join(parts, dot)
168 metadata = dot + metadata
169
170 const dirTrimLimit = 4
171 cwd := fsext.DirTrim(fsext.PrettyPath(com.Workspace.WorkingDir()), dirTrimLimit)
172 cwd = t.Header.WorkingDir.Render(cwd)
173
174 result := cwd + metadata
175 return ansi.Truncate(result, max(0, availWidth), "β¦")
176}