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 "github.com/charmbracelet/x/ansi"
16)
17
18const (
19 headerDiag = "β±"
20 minHeaderDiags = 3
21 leftPadding = 1
22 rightPadding = 1
23)
24
25// renderCompactHeader renders the compact header for the given session.
26func renderCompactHeader(
27 com *common.Common,
28 session *session.Session,
29 lspClients *csync.Map[string, *lsp.Client],
30 detailsOpen bool,
31 width int,
32) string {
33 if session == nil || session.ID == "" {
34 return ""
35 }
36
37 t := com.Styles
38
39 var b strings.Builder
40
41 b.WriteString(t.Header.Charm.Render("Charmβ’"))
42 b.WriteString(" ")
43 b.WriteString(styles.ApplyBoldForegroundGrad(t, "CRUSH", t.Secondary, t.Primary))
44 b.WriteString(" ")
45
46 availDetailWidth := width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minHeaderDiags
47 details := renderHeaderDetails(com, session, lspClients, detailsOpen, availDetailWidth)
48
49 remainingWidth := width -
50 lipgloss.Width(b.String()) -
51 lipgloss.Width(details) -
52 leftPadding -
53 rightPadding
54
55 if remainingWidth > 0 {
56 b.WriteString(t.Header.Diagonals.Render(
57 strings.Repeat(headerDiag, max(minHeaderDiags, remainingWidth)),
58 ))
59 b.WriteString(" ")
60 }
61
62 b.WriteString(details)
63
64 return t.Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
65}
66
67// renderHeaderDetails renders the details section of the header.
68func renderHeaderDetails(
69 com *common.Common,
70 session *session.Session,
71 lspClients *csync.Map[string, *lsp.Client],
72 detailsOpen bool,
73 availWidth int,
74) string {
75 t := com.Styles
76
77 var parts []string
78
79 errorCount := 0
80 for l := range lspClients.Seq() {
81 errorCount += l.GetDiagnosticCounts().Error
82 }
83
84 if errorCount > 0 {
85 parts = append(parts, t.LSP.ErrorDiagnostic.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
86 }
87
88 agentCfg := config.Get().Agents[config.AgentCoder]
89 model := config.Get().GetModelByType(agentCfg.Model)
90 percentage := (float64(session.CompletionTokens+session.PromptTokens) / float64(model.ContextWindow)) * 100
91 formattedPercentage := t.Header.Percentage.Render(fmt.Sprintf("%d%%", int(percentage)))
92 parts = append(parts, formattedPercentage)
93
94 const keystroke = "ctrl+d"
95 if detailsOpen {
96 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" close"))
97 } else {
98 parts = append(parts, t.Header.Keystroke.Render(keystroke)+t.Header.KeystrokeTip.Render(" open "))
99 }
100
101 dot := t.Header.Separator.Render(" β’ ")
102 metadata := strings.Join(parts, dot)
103 metadata = dot + metadata
104
105 const dirTrimLimit = 4
106 cfg := com.Config()
107 cwd := fsext.DirTrim(fsext.PrettyPath(cfg.WorkingDir()), dirTrimLimit)
108 cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "β¦")
109 cwd = t.Header.WorkingDir.Render(cwd)
110
111 return cwd + metadata
112}