header.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package header
  2. import (
  3. "fmt"
  4. "strings"
  5. tea "github.com/charmbracelet/bubbletea/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/pubsub"
  11. "github.com/charmbracelet/crush/internal/session"
  12. "github.com/charmbracelet/crush/internal/tui/styles"
  13. "github.com/charmbracelet/crush/internal/tui/util"
  14. "github.com/charmbracelet/lipgloss/v2"
  15. "github.com/charmbracelet/x/ansi"
  16. "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
  17. )
  18. type Header interface {
  19. util.Model
  20. SetSession(session session.Session) tea.Cmd
  21. SetWidth(width int) tea.Cmd
  22. SetDetailsOpen(open bool)
  23. ShowingDetails() bool
  24. }
  25. type header struct {
  26. width int
  27. session session.Session
  28. lspClients *csync.Map[string, *lsp.Client]
  29. detailsOpen bool
  30. }
  31. func New(lspClients *csync.Map[string, *lsp.Client]) Header {
  32. return &header{
  33. lspClients: lspClients,
  34. width: 0,
  35. }
  36. }
  37. func (h *header) Init() tea.Cmd {
  38. return nil
  39. }
  40. func (h *header) Update(msg tea.Msg) (util.Model, tea.Cmd) {
  41. switch msg := msg.(type) {
  42. case pubsub.Event[session.Session]:
  43. if msg.Type == pubsub.UpdatedEvent {
  44. if h.session.ID == msg.Payload.ID {
  45. h.session = msg.Payload
  46. }
  47. }
  48. }
  49. return h, nil
  50. }
  51. func (h *header) View() string {
  52. if h.session.ID == "" {
  53. return ""
  54. }
  55. const (
  56. gap = " "
  57. diag = "╱"
  58. minDiags = 3
  59. leftPadding = 1
  60. rightPadding = 1
  61. )
  62. t := styles.CurrentTheme()
  63. var b strings.Builder
  64. b.WriteString(t.S().Base.Foreground(t.Secondary).Render("Charm™"))
  65. b.WriteString(gap)
  66. b.WriteString(styles.ApplyBoldForegroundGrad("CRUSH", t.Secondary, t.Primary))
  67. b.WriteString(gap)
  68. availDetailWidth := h.width - leftPadding - rightPadding - lipgloss.Width(b.String()) - minDiags
  69. details := h.details(availDetailWidth)
  70. remainingWidth := h.width -
  71. lipgloss.Width(b.String()) -
  72. lipgloss.Width(details) -
  73. leftPadding -
  74. rightPadding
  75. if remainingWidth > 0 {
  76. b.WriteString(t.S().Base.Foreground(t.Primary).Render(
  77. strings.Repeat(diag, max(minDiags, remainingWidth)),
  78. ))
  79. b.WriteString(gap)
  80. }
  81. b.WriteString(details)
  82. return t.S().Base.Padding(0, rightPadding, 0, leftPadding).Render(b.String())
  83. }
  84. func (h *header) details(availWidth int) string {
  85. s := styles.CurrentTheme().S()
  86. var parts []string
  87. errorCount := 0
  88. for l := range h.lspClients.Seq() {
  89. for _, diagnostics := range l.GetDiagnostics() {
  90. for _, diagnostic := range diagnostics {
  91. if diagnostic.Severity == protocol.SeverityError {
  92. errorCount++
  93. }
  94. }
  95. }
  96. }
  97. if errorCount > 0 {
  98. parts = append(parts, s.Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
  99. }
  100. agentCfg := config.Get().Agents[config.AgentCoder]
  101. model := config.Get().GetModelByType(agentCfg.Model)
  102. percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
  103. formattedPercentage := s.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
  104. parts = append(parts, formattedPercentage)
  105. const keystroke = "ctrl+d"
  106. if h.detailsOpen {
  107. parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" close"))
  108. } else {
  109. parts = append(parts, s.Muted.Render(keystroke)+s.Subtle.Render(" open "))
  110. }
  111. dot := s.Subtle.Render(" • ")
  112. metadata := strings.Join(parts, dot)
  113. metadata = dot + metadata
  114. // Truncate cwd if necessary, and insert it at the beginning.
  115. const dirTrimLimit = 4
  116. cwd := fsext.DirTrim(fsext.PrettyPath(config.Get().WorkingDir()), dirTrimLimit)
  117. cwd = ansi.Truncate(cwd, max(0, availWidth-lipgloss.Width(metadata)), "…")
  118. cwd = s.Muted.Render(cwd)
  119. return cwd + metadata
  120. }
  121. func (h *header) SetDetailsOpen(open bool) {
  122. h.detailsOpen = open
  123. }
  124. // SetSession implements Header.
  125. func (h *header) SetSession(session session.Session) tea.Cmd {
  126. h.session = session
  127. return nil
  128. }
  129. // SetWidth implements Header.
  130. func (h *header) SetWidth(width int) tea.Cmd {
  131. h.width = width
  132. return nil
  133. }
  134. // ShowingDetails implements Header.
  135. func (h *header) ShowingDetails() bool {
  136. return h.detailsOpen
  137. }