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