attachments.go

  1package attachments
  2
  3import (
  4	"fmt"
  5	"math"
  6	"path/filepath"
  7	"slices"
  8	"strings"
  9
 10	"charm.land/bubbles/v2/key"
 11	tea "charm.land/bubbletea/v2"
 12	"charm.land/lipgloss/v2"
 13	"github.com/charmbracelet/crush/internal/message"
 14	"github.com/charmbracelet/x/ansi"
 15)
 16
 17const maxFilename = 15
 18
 19type Keymap struct {
 20	DeleteMode,
 21	DeleteAll,
 22	Escape key.Binding
 23}
 24
 25func New(renderer *Renderer, keyMap Keymap) *Attachments {
 26	return &Attachments{
 27		keyMap:   keyMap,
 28		renderer: renderer,
 29	}
 30}
 31
 32type Attachments struct {
 33	renderer *Renderer
 34	keyMap   Keymap
 35	list     []message.Attachment
 36	deleting bool
 37}
 38
 39func (m *Attachments) List() []message.Attachment { return m.list }
 40func (m *Attachments) Reset()                     { m.list = nil }
 41
 42func (m *Attachments) Update(msg tea.Msg) bool {
 43	switch msg := msg.(type) {
 44	case message.Attachment:
 45		m.list = append(m.list, msg)
 46		return true
 47	case tea.KeyPressMsg:
 48		switch {
 49		case key.Matches(msg, m.keyMap.DeleteMode):
 50			if len(m.list) > 0 {
 51				m.deleting = true
 52			}
 53			return true
 54		case m.deleting && key.Matches(msg, m.keyMap.Escape):
 55			m.deleting = false
 56			return true
 57		case m.deleting && key.Matches(msg, m.keyMap.DeleteAll):
 58			m.deleting = false
 59			m.list = nil
 60			return true
 61		case m.deleting:
 62			// Handle digit keys for individual attachment deletion.
 63			r := msg.Code
 64			if r >= '0' && r <= '9' {
 65				num := int(r - '0')
 66				if num < len(m.list) {
 67					m.list = slices.Delete(m.list, num, num+1)
 68				}
 69				m.deleting = false
 70			}
 71			return true
 72		}
 73	}
 74	return false
 75}
 76
 77func (m *Attachments) Render(width int) string {
 78	return m.renderer.Render(m.list, m.deleting, width)
 79}
 80
 81func NewRenderer(normalStyle, deletingStyle, imageStyle, textStyle lipgloss.Style) *Renderer {
 82	return &Renderer{
 83		normalStyle:   normalStyle,
 84		textStyle:     textStyle,
 85		imageStyle:    imageStyle,
 86		deletingStyle: deletingStyle,
 87	}
 88}
 89
 90type Renderer struct {
 91	normalStyle, textStyle, imageStyle, deletingStyle lipgloss.Style
 92}
 93
 94func (r *Renderer) Render(attachments []message.Attachment, deleting bool, width int) string {
 95	var chips []string
 96
 97	maxItemWidth := lipgloss.Width(r.imageStyle.String() + r.normalStyle.Render(strings.Repeat("x", maxFilename)))
 98	fits := int(math.Floor(float64(width)/float64(maxItemWidth))) - 1
 99
100	for i, att := range attachments {
101		filename := filepath.Base(att.FileName)
102		// Truncate if needed.
103		if ansi.StringWidth(filename) > maxFilename {
104			filename = ansi.Truncate(filename, maxFilename, "…")
105		}
106
107		if deleting {
108			chips = append(
109				chips,
110				r.deletingStyle.Render(fmt.Sprintf("%d", i)),
111				r.normalStyle.Render(filename),
112			)
113		} else {
114			chips = append(
115				chips,
116				r.icon(att).String(),
117				r.normalStyle.Render(filename),
118			)
119		}
120
121		if i == fits && len(attachments) > i {
122			chips = append(chips, lipgloss.NewStyle().Width(maxItemWidth).Render(fmt.Sprintf("%d more…", len(attachments)-fits)))
123			break
124		}
125	}
126
127	return lipgloss.JoinHorizontal(lipgloss.Left, chips...)
128}
129
130func (r *Renderer) icon(a message.Attachment) lipgloss.Style {
131	if a.IsImage() {
132		return r.imageStyle
133	}
134	return r.textStyle
135}