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
 81// Renderer returns the attachment renderer so callers can update its
 82// styles in place.
 83func (m *Attachments) Renderer() *Renderer { return m.renderer }
 84
 85func NewRenderer(normalStyle, deletingStyle, imageStyle, textStyle, skillStyle lipgloss.Style) *Renderer {
 86	return &Renderer{
 87		normalStyle:   normalStyle,
 88		textStyle:     textStyle,
 89		imageStyle:    imageStyle,
 90		skillStyle:    skillStyle,
 91		deletingStyle: deletingStyle,
 92	}
 93}
 94
 95// SetStyles updates the renderer styles in place.
 96func (r *Renderer) SetStyles(normalStyle, deletingStyle, imageStyle, textStyle, skillStyle lipgloss.Style) {
 97	r.normalStyle = normalStyle
 98	r.textStyle = textStyle
 99	r.imageStyle = imageStyle
100	r.skillStyle = skillStyle
101	r.deletingStyle = deletingStyle
102}
103
104type Renderer struct {
105	normalStyle, textStyle, imageStyle, skillStyle, deletingStyle lipgloss.Style
106}
107
108func (r *Renderer) Render(attachments []message.Attachment, deleting bool, width int) string {
109	var chips []string
110
111	maxItemWidth := lipgloss.Width(r.imageStyle.String() + r.normalStyle.Render(strings.Repeat("x", maxFilename)))
112	fits := int(math.Floor(float64(width)/float64(maxItemWidth))) - 1
113
114	for i, att := range attachments {
115		filename := filepath.Base(att.FileName)
116		// Truncate if needed.
117		if ansi.StringWidth(filename) > maxFilename {
118			filename = ansi.Truncate(filename, maxFilename, "…")
119		}
120
121		if deleting {
122			chips = append(
123				chips,
124				r.deletingStyle.Render(fmt.Sprintf("%d", i)),
125				r.normalStyle.Render(filename),
126			)
127		} else {
128			chips = append(
129				chips,
130				r.icon(att).String(),
131				r.normalStyle.Render(filename),
132			)
133		}
134
135		if i == fits && len(attachments) > i {
136			chips = append(chips, lipgloss.NewStyle().Width(maxItemWidth).Render(fmt.Sprintf("%d more…", len(attachments)-fits)))
137			break
138		}
139	}
140
141	return lipgloss.JoinHorizontal(lipgloss.Left, chips...)
142}
143
144func (r *Renderer) icon(a message.Attachment) lipgloss.Style {
145	if a.IsImage() {
146		return r.imageStyle
147	}
148	if a.IsMarkdown() {
149		return r.skillStyle
150	}
151	return r.textStyle
152}