user.go

  1package chat
  2
  3import (
  4	"fmt"
  5	"path/filepath"
  6	"strings"
  7
  8	"charm.land/lipgloss/v2"
  9	"github.com/charmbracelet/crush/internal/message"
 10	"github.com/charmbracelet/crush/internal/ui/common"
 11	"github.com/charmbracelet/crush/internal/ui/styles"
 12	"github.com/charmbracelet/x/ansi"
 13)
 14
 15// UserMessageItem represents a user message in the chat UI.
 16type UserMessageItem struct {
 17	*highlightableMessageItem
 18	*cachedMessageItem
 19	*focusableMessageItem
 20
 21	message *message.Message
 22	sty     *styles.Styles
 23}
 24
 25// NewUserMessageItem creates a new UserMessageItem.
 26func NewUserMessageItem(sty *styles.Styles, message *message.Message) MessageItem {
 27	return &UserMessageItem{
 28		highlightableMessageItem: defaultHighlighter(sty),
 29		cachedMessageItem:        &cachedMessageItem{},
 30		focusableMessageItem:     &focusableMessageItem{},
 31		message:                  message,
 32		sty:                      sty,
 33	}
 34}
 35
 36// Render implements MessageItem.
 37func (m *UserMessageItem) Render(width int) string {
 38	cappedWidth := cappedMessageWidth(width)
 39
 40	style := m.sty.Chat.Message.UserBlurred
 41	if m.focused {
 42		style = m.sty.Chat.Message.UserFocused
 43	}
 44
 45	content, height, ok := m.getCachedRender(cappedWidth)
 46	// cache hit
 47	if ok {
 48		return style.Render(m.renderHighlighted(content, cappedWidth, height))
 49	}
 50
 51	renderer := common.MarkdownRenderer(m.sty, cappedWidth)
 52
 53	msgContent := strings.TrimSpace(m.message.Content().Text)
 54	result, err := renderer.Render(msgContent)
 55	if err != nil {
 56		content = msgContent
 57	} else {
 58		content = strings.TrimSuffix(result, "\n")
 59	}
 60
 61	if len(m.message.BinaryContent()) > 0 {
 62		attachmentsStr := m.renderAttachments(cappedWidth)
 63		content = strings.Join([]string{content, "", attachmentsStr}, "\n")
 64	}
 65
 66	height = lipgloss.Height(content)
 67	m.setCachedRender(content, cappedWidth, height)
 68	return style.Render(m.renderHighlighted(content, cappedWidth, height))
 69}
 70
 71// ID implements MessageItem.
 72func (m *UserMessageItem) ID() string {
 73	return m.message.ID
 74}
 75
 76// renderAttachments renders attachments with wrapping if they exceed the width.
 77// TODO: change the styles here so they match the new design
 78func (m *UserMessageItem) renderAttachments(width int) string {
 79	const maxFilenameWidth = 10
 80
 81	attachments := make([]string, len(m.message.BinaryContent()))
 82	for i, attachment := range m.message.BinaryContent() {
 83		filename := filepath.Base(attachment.Path)
 84		attachments[i] = m.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
 85			" %s %s ",
 86			styles.DocumentIcon,
 87			ansi.Truncate(filename, maxFilenameWidth, "…"),
 88		))
 89	}
 90
 91	// Wrap attachments into lines that fit within the width.
 92	var lines []string
 93	var currentLine []string
 94	currentWidth := 0
 95
 96	for _, att := range attachments {
 97		attWidth := lipgloss.Width(att)
 98		sepWidth := 1
 99		if len(currentLine) == 0 {
100			sepWidth = 0
101		}
102
103		if currentWidth+sepWidth+attWidth > width && len(currentLine) > 0 {
104			lines = append(lines, strings.Join(currentLine, " "))
105			currentLine = []string{att}
106			currentWidth = attWidth
107		} else {
108			currentLine = append(currentLine, att)
109			currentWidth += sepWidth + attWidth
110		}
111	}
112
113	if len(currentLine) > 0 {
114		lines = append(lines, strings.Join(currentLine, " "))
115	}
116
117	return strings.Join(lines, "\n")
118}