header.go 3.8 KB

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