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(h.com, session, h.com.App.LSPClients, detailsOpen, availDetailWidth)
78
79 remainingWidth := width -
80 lipgloss.Width(b.String()) -
81 lipgloss.Width(details) -
82 leftPadding -
83 rightPadding
84
85 if remainingWidth > 0 {
86 b.WriteString(t.Header.Diagonals.Render(
87 strings.Repeat(headerDiag, max(minHeaderDiags, remainingWidth)),
88 ))
89 b.WriteString(" ")
90 }
91
92 b.WriteString(details)
93
94 view := uv.NewStyledString(
95 t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String()))
96 view.Draw(scr, area)
97}
98
99// renderHeaderDetails renders the details section of the header.
100func renderHeaderDetails(
101 com *common.Common,
102 session *session.Session,
103 lspClients *csync.Map[string, *lsp.Client],
104 detailsOpen bool,
105 availWidth int,
106) string {
107 t := com.Styles
108
109 var parts []string
110
111 errorCount := 0
112 for l := range lspClients.Seq() {
113 errorCount += l.GetDiagnosticCounts().Error
114 }
115
116 if errorCount > 0 {
117 parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.LSPErrorIcon, errorCount)))
118 }
119
120 agentCfg := com.Config().Agents()[config.AgentCoder]
121 model := com.Config().GetModelByType(agentCfg.Model)
122 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
123 formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
124 parts = append(parts, formattedPercentage)
125
126 const keystroke = "ctrl+d"
127 if detailsOpen {
128 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" close"))
129 } else {
130 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" open "))
131 }
132
133 dot := t.Header.Separator.Render(" β’ ")
134 metadata := strings.Join(parts, dot)
135 metadata = dot + metadata
136
137 const dirTrimLimit = 4
138 cwd := fsext.DirTrim(fsext.PrettyPath(com.Config().WorkingDir()), dirTrimLimit)
139 cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "β¦")
140 cwd = t.Header.WorkingDir.Render(cwd)
141
142 return cwd + metadata
143}