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/csync"
10 "github.com/charmbracelet/crush/internal/fsext"
11 "github.com/charmbracelet/crush/internal/lsp"
12 "github.com/charmbracelet/crush/internal/session"
13 "github.com/charmbracelet/crush/internal/ui/common"
14 "github.com/charmbracelet/crush/internal/ui/styles"
15 uv "github.com/charmbracelet/ultraviolet"
16 "github.com/charmbracelet/x/ansi"
17)
18
19const (
20 headerDiag = "β±"
21 minHeaderDiags = 3
22 leftPadding = 1
23 rightPadding = 1
24 diagToDetailsSpacing = 1 // space between diagonal pattern and details section
25)
26
27type header struct {
28 // cached logo and compact logo
29 logo string
30 compactLogo string
31
32 com *common.Common
33 width int
34 compact bool
35}
36
37// newHeader creates a new header model.
38func newHeader(com *common.Common) *header {
39 h := &header{
40 com: com,
41 }
42 t := com.Styles
43 h.compactLogo = t.Header.Charm.Render("Charmβ’") + " " +
44 styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary) + " "
45 return h
46}
47
48// drawHeader draws the header for the given session.
49func (h *header) drawHeader(
50 scr uv.Screen,
51 area uv.Rectangle,
52 session *session.Session,
53 compact bool,
54 detailsOpen bool,
55 width int,
56) {
57 t := h.com.Styles
58 if width != h.width || compact != h.compact {
59 h.logo = renderLogo(h.com.Styles, compact, width)
60 }
61
62 h.width = width
63 h.compact = compact
64
65 if !compact || session == nil || h.com.App == nil {
66 uv.NewStyledString(h.logo).Draw(scr, area)
67 return
68 }
69
70 if session.ID == "" {
71 return
72 }
73
74 var b strings.Builder
75 b.WriteString(h.compactLogo)
76
77 availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags - diagToDetailsSpacing
78 details := renderHeaderDetails(
79 h.com,
80 session,
81 h.com.App.LSPManager.Clients(),
82 detailsOpen,
83 availDetailWidth,
84 )
85
86 remainingWidth := width -
87 lipgloss.Width(b.String()) -
88 lipgloss.Width(details) -
89 leftPadding -
90 rightPadding -
91 diagToDetailsSpacing
92
93 if remainingWidth > 0 {
94 b.WriteString(t.Header.Diagonals.Render(
95 strings.Repeat(headerDiag, max(minHeaderDiags, remainingWidth)),
96 ))
97 b.WriteString(" ")
98 }
99
100 b.WriteString(details)
101
102 view := uv.NewStyledString(
103 t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
104 view.Draw(scr, area)
105}
106
107// renderHeaderDetails renders the details section of the header.
108func renderHeaderDetails(
109 com *common.Common,
110 session *session.Session,
111 lspClients *csync.Map[string, *lsp.Client],
112 detailsOpen bool,
113 availWidth int,
114) string {
115 t := com.Styles
116
117 var parts []string
118
119 errorCount := 0
120 for l := range lspClients.Seq() {
121 errorCount += l.GetDiagnosticCounts().Error
122 }
123
124 if errorCount > 0 {
125 parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPErrorIcon, errorCount)))
126 }
127
128 agentCfg := com.Config().Agents[config.AgentCoder]
129 model := com.Config().GetModelByType(agentCfg.Model)
130 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
131 formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
132 parts = append(parts, formattedPercentage)
133
134 const keystroke = "ctrl+d"
135 if detailsOpen {
136 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" close"))
137 } else {
138 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" open "))
139 }
140
141 dot := t.Header.Separator.Render(" β’ ")
142 metadata := strings.Join(parts, dot)
143 metadata = dot + metadata
144
145 const dirTrimLimit = 4
146 cwd := fsext.DirTrim(fsext.PrettyPath(com.Store().WorkingDir()), dirTrimLimit)
147 cwd = t.Header.WorkingDir.Render(cwd)
148
149 result := cwd + metadata
150 return ansi.Truncate(result, max(0, availWidth), "β¦")
151}