header.go 3.6 KB

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