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}