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