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