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