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 h.compactLogo = t.Header.Charm.Render("Charmβ’") + " " +
49 styles.ApplyBoldForegroundGrad(t.Header.LogoGradCanvas, "CRUSH", t.Header.LogoGradFromColor, t.Header.LogoGradToColor) + " "
50 // Force drawHeader to re-render the wide logo on the next frame.
51 h.width = 0
52 h.logo = ""
53}
54
55// drawHeader draws the header for the given session.
56func (h *header) drawHeader(
57 scr uv.Screen,
58 area uv.Rectangle,
59 session *session.Session,
60 compact bool,
61 detailsOpen bool,
62 width int,
63) {
64 t := h.com.Styles
65 if width != h.width || compact != h.compact {
66 h.logo = renderLogo(h.com.Styles, compact, h.com.IsHyper(), width)
67 }
68
69 h.width = width
70 h.compact = compact
71
72 if !compact || session == nil {
73 uv.NewStyledString(h.logo).Draw(scr, area)
74 return
75 }
76
77 if session.ID == "" {
78 return
79 }
80
81 var b strings.Builder
82 b.WriteString(h.compactLogo)
83
84 availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags - diagToDetailsSpacing
85 lspErrorCount := 0
86 for _, info := range h.com.Workspace.LSPGetStates() {
87 lspErrorCount += info.DiagnosticCount
88 }
89 details := renderHeaderDetails(
90 h.com,
91 session,
92 lspErrorCount,
93 detailsOpen,
94 availDetailWidth,
95 )
96
97 remainingWidth := width -
98 lipgloss.Width(b.String()) -
99 lipgloss.Width(details) -
100 leftPadding -
101 rightPadding -
102 diagToDetailsSpacing
103
104 if remainingWidth > 0 {
105 b.WriteString(t.Header.Diagonals.Render(
106 strings.Repeat(headerDiag, max(minHeaderDiags, remainingWidth)),
107 ))
108 b.WriteString(" ")
109 }
110
111 b.WriteString(details)
112
113 view := uv.NewStyledString(
114 t.Header.Wrapper.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
115 view.Draw(scr, area)
116}
117
118// renderHeaderDetails renders the details section of the header.
119func renderHeaderDetails(
120 com *common.Common,
121 session *session.Session,
122 lspErrorCount int,
123 detailsOpen bool,
124 availWidth int,
125) string {
126 t := com.Styles
127
128 var parts []string
129
130 if lspErrorCount > 0 {
131 parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPErrorIcon, lspErrorCount)))
132 }
133
134 agentCfg := com.Config().Agents[config.AgentCoder]
135 model := com.Config().GetModelByType(agentCfg.Model)
136 if model != nil && model.ContextWindow > 0 {
137 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
138 formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
139 parts = append(parts, formattedPercentage)
140 }
141
142 const keystroke = "ctrl+d"
143 if detailsOpen {
144 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" close"))
145 } else {
146 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" open "))
147 }
148
149 dot := t.Header.Separator.Render(" β’ ")
150 metadata := strings.Join(parts, dot)
151 metadata = dot + metadata
152
153 const dirTrimLimit = 4
154 cwd := fsext.DirTrim(fsext.PrettyPath(com.Workspace.WorkingDir()), dirTrimLimit)
155 cwd = t.Header.WorkingDir.Render(cwd)
156
157 result := cwd + metadata
158 return ansi.Truncate(result, max(0, availWidth), "β¦")
159}