user.go

  1package chat
  2
  3import (
  4	"strings"
  5
  6	tea "charm.land/bubbletea/v2"
  7	"charm.land/lipgloss/v2"
  8	"github.com/charmbracelet/crush/internal/message"
  9	"github.com/charmbracelet/crush/internal/ui/attachments"
 10	"github.com/charmbracelet/crush/internal/ui/common"
 11	"github.com/charmbracelet/crush/internal/ui/styles"
 12)
 13
 14// UserMessageItem represents a user message in the chat UI.
 15type UserMessageItem struct {
 16	*highlightableMessageItem
 17	*cachedMessageItem
 18	*focusableMessageItem
 19
 20	attachments *attachments.Renderer
 21	message     *message.Message
 22	sty         *styles.Styles
 23}
 24
 25// NewUserMessageItem creates a new UserMessageItem.
 26func NewUserMessageItem(sty *styles.Styles, message *message.Message, attachments *attachments.Renderer) MessageItem {
 27	return &UserMessageItem{
 28		highlightableMessageItem: defaultHighlighter(sty),
 29		cachedMessageItem:        &cachedMessageItem{},
 30		focusableMessageItem:     &focusableMessageItem{},
 31		attachments:              attachments,
 32		message:                  message,
 33		sty:                      sty,
 34	}
 35}
 36
 37// RawRender implements [MessageItem].
 38func (m *UserMessageItem) RawRender(width int) string {
 39	cappedWidth := cappedMessageWidth(width)
 40
 41	content, height, ok := m.getCachedRender(cappedWidth)
 42	// cache hit
 43	if ok {
 44		return m.renderHighlighted(content, cappedWidth, height)
 45	}
 46
 47	renderer := common.MarkdownRenderer(m.sty, cappedWidth)
 48
 49	msgContent := strings.TrimSpace(m.message.Content().Text)
 50	result, err := renderer.Render(msgContent)
 51	if err != nil {
 52		content = msgContent
 53	} else {
 54		content = strings.TrimSuffix(result, "\n")
 55	}
 56
 57	if len(m.message.BinaryContent()) > 0 {
 58		attachmentsStr := m.renderAttachments(cappedWidth)
 59		if content == "" {
 60			content = attachmentsStr
 61		} else {
 62			content = strings.Join([]string{content, "", attachmentsStr}, "\n")
 63		}
 64	}
 65
 66	height = lipgloss.Height(content)
 67	m.setCachedRender(content, cappedWidth, height)
 68	return m.renderHighlighted(content, cappedWidth, height)
 69}
 70
 71// Render implements MessageItem.
 72func (m *UserMessageItem) Render(width int) string {
 73	// Bypass the prefix cache while a highlight range is active so
 74	// selection drags reflect immediately without invalidating the
 75	// cache. Highlight changes are intentionally applied "above" the
 76	// prefix cache.
 77	useCache := !m.isHighlighted()
 78	var key uint64
 79	if m.focused {
 80		key = 1
 81	}
 82	if useCache {
 83		if cached, ok := m.getCachedPrefixedRender(width, key); ok {
 84			return cached
 85		}
 86	}
 87	var prefix string
 88	if m.focused {
 89		prefix = m.sty.Messages.UserFocused.Render()
 90	} else {
 91		prefix = m.sty.Messages.UserBlurred.Render()
 92	}
 93	lines := strings.Split(m.RawRender(width), "\n")
 94	for i, line := range lines {
 95		lines[i] = prefix + line
 96	}
 97	out := strings.Join(lines, "\n")
 98	if useCache {
 99		m.setCachedPrefixedRender(out, width, key)
100	}
101	return out
102}
103
104// ID implements MessageItem.
105func (m *UserMessageItem) ID() string {
106	return m.message.ID
107}
108
109// renderAttachments renders attachments.
110func (m *UserMessageItem) renderAttachments(width int) string {
111	var attachments []message.Attachment
112	for _, at := range m.message.BinaryContent() {
113		attachments = append(attachments, message.Attachment{
114			FileName: at.Path,
115			MimeType: at.MIMEType,
116		})
117	}
118	return m.attachments.Render(attachments, false, width)
119}
120
121// HandleKeyEvent implements KeyEventHandler.
122func (m *UserMessageItem) HandleKeyEvent(key tea.KeyMsg) (bool, tea.Cmd) {
123	if k := key.String(); k == "c" || k == "y" {
124		text := m.message.Content().Text
125		return true, common.CopyToClipboard(text, "Message copied to clipboard")
126	}
127	return false, nil
128}