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}