@@ -2,6 +2,7 @@ package editor
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "math/rand"
78 "net/http"
@@ -29,6 +30,7 @@ import (
2930 "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
3031 "github.com/charmbracelet/crush/internal/tui/styles"
3132 "github.com/charmbracelet/crush/internal/tui/util"
33+ "github.com/charmbracelet/x/ansi"
3234)
3335
3436type Editor interface {
@@ -84,10 +86,7 @@ var DeleteKeyMaps = DeleteAttachmentKeyMaps{
8486 ),
8587}
8688
87−const (
88− maxAttachments = 5
89− maxFileResults = 25
90−)
89+const maxFileResults = 25
9190
9291type OpenEditorMsg struct {
9392 Text string
@@ -145,14 +144,14 @@ func (m *editorCmp) send() tea.Cmd {
145144 return util.CmdHandler(dialogs.OpenDialogMsg{Model: quit.NewQuitDialog()})
146145 }
147146
148− m.textarea.Reset()
149147 attachments := m.attachments
150148
151− m.attachments = nil
152149 if value == "" {
153150 return nil
154151 }
155152
153+ m.textarea.Reset()
154+ m.attachments = nil
156155 // Change the placeholder when sending a new message.
157156 m.randomizePlaceholders()
158157
@@ -176,9 +175,6 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
176175 case tea.WindowSizeMsg:
177176 return m, m.repositionCompletions
178177 case filepicker.FilePickedMsg:
179− if len(m.attachments) >= maxAttachments {
180− return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
181− }
182178 m.attachments = append(m.attachments, msg.Attachment)
183179 return m, nil
184180 case completions.CompletionsOpenedMsg:
@@ -206,6 +202,17 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
206202 m.currentQuery = ""
207203 m.completionsStartIndex = 0
208204 }
205+ content, err := os.ReadFile(item.Path)
206+ if err != nil {
207+ // if it fails, let the LLM handle it later.
208+ return m, nil
209+ }
210+ m.attachments = append(m.attachments, message.Attachment{
211+ FilePath: item.Path,
212+ FileName: filepath.Base(item.Path),
213+ MimeType: mimeOf(content),
214+ Content: content,
215+ })
209216 }
210217
211218 case commands.OpenExternalEditorMsg:
@@ -217,39 +224,30 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
217224 m.textarea.SetValue(msg.Text)
218225 m.textarea.MoveToEnd()
219226 case tea.PasteMsg:
220− path := strings.ReplaceAll(msg.Content, "\\ ", " ")
221− // try to get an image
222− path, err := filepath.Abs(strings.TrimSpace(path))
223− if err != nil {
227+ content, path, err := pasteToFile(msg)
228+ if errors.Is(err, errNotAFile) {
224229 m.textarea, cmd = m.textarea.Update(msg)
225230 return m, cmd
226231 }
227− isAllowedType := false
228− for _, ext := range filepicker.AllowedTypes {
229− if strings.HasSuffix(path, ext) {
230− isAllowedType = true
231− break
232− }
233− }
234− if !isAllowedType {
235− m.textarea, cmd = m.textarea.Update(msg)
236− return m, cmd
232+ if err != nil {
233+ return m, util.ReportError(err)
237234 }
238− tooBig, _ := filepicker.IsFileTooBig(path, filepicker.MaxAttachmentSize)
239− if tooBig {
240− m.textarea, cmd = m.textarea.Update(msg)
241− return m, cmd
235+
236+ if len(content) > maxAttachmentSize {
237+ return m, util.ReportWarn("File is too big (>5mb)")
242238 }
243239
244− content, err := os.ReadFile(path)
245− if err != nil {
246− m.textarea, cmd = m.textarea.Update(msg)
247− return m, cmd
240+ mimeType := mimeOf(content)
241+ attachment := message.Attachment{
242+ FilePath: path,
243+ FileName: filepath.Base(path),
244+ MimeType: mimeType,
245+ Content: content,
248246 }
249− mimeBufferSize := min(512, len(content))
250− mimeType := http.DetectContentType(content[:mimeBufferSize])
251− fileName := filepath.Base(path)
252− attachment := message.Attachment{FilePath: path, FileName: fileName, MimeType: mimeType, Content: content}
247+ if !attachment.IsText() && !attachment.IsImage() {
248+ return m, util.ReportWarn("Invalid file content type: " + mimeType)
249+ }
250+ m.textarea.InsertString(attachment.FileName)
253251 return m, util.CmdHandler(filepicker.FilePickedMsg{
254252 Attachment: attachment,
255253 })
@@ -427,18 +425,17 @@ func (m *editorCmp) View() string {
427425 m.textarea.Placeholder = "Yolo mode!"
428426 }
429427 if len(m.attachments) == 0 {
430− content := t.S().Base.Padding(1).Render(
428+ return t.S().Base.Padding(1).Render(
431429 m.textarea.View(),
432430 )
433− return content
434431 }
435− content := t.S().Base.Padding(0, 1, 1, 1).Render(
436− lipgloss.JoinVertical(lipgloss.Top,
432+ return t.S().Base.Padding(0, 1, 1, 1).Render(
433+ lipgloss.JoinVertical(
434+ lipgloss.Top,
437435 m.attachmentsContent(),
438436 m.textarea.View(),
439437 ),
440438 )
441− return content
442439}
443440
444441func (m *editorCmp) SetSize(width, height int) tea.Cmd {
@@ -456,24 +453,45 @@ func (m *editorCmp) GetSize() (int, int) {
456453func (m *editorCmp) attachmentsContent() string {
457454 var styledAttachments []string
458455 t := styles.CurrentTheme()
459− attachmentStyles := t.S().Base.
460− MarginLeft(1).
456+ attachmentStyle := t.S().Base.
457+ Padding(0, 1).
458+ MarginRight(1).
461459 Background(t.FgMuted).
462− Foreground(t.FgBase)
460+ Foreground(t.FgBase).
461+ Render
462+ iconStyle := t.S().Base.
463+ Foreground(t.BgSubtle).
464+ Background(t.Green).
465+ Padding(0, 1).
466+ Bold(true).
467+ Render
468+ rmStyle := t.S().Base.
469+ Padding(0, 1).
470+ Bold(true).
471+ Background(t.Red).
472+ Foreground(t.FgBase).
473+ Render
463474 for i, attachment := range m.attachments {
464− var filename string
465− if len(attachment.FileName) > 10 {
466− filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7])
467− } else {
468− filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName)
475+ filename := ansi.Truncate(filepath.Base(attachment.FileName), 10, "...")
476+ icon := styles.ImageIcon
477+ if attachment.IsText() {
478+ icon = styles.TextIcon
469479 }
470480 if m.deleteMode {
471− filename = fmt.Sprintf("%d%s", i, filename)
481+ styledAttachments = append(
482+ styledAttachments,
483+ rmStyle(fmt.Sprintf("%d", i)),
484+ attachmentStyle(filename),
485+ )
486+ continue
472487 }
473− styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
488+ styledAttachments = append(
489+ styledAttachments,
490+ iconStyle(icon),
491+ attachmentStyle(filename),
492+ )
474493 }
475− content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...)
476− return content
494+ return lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...)
477495}
478496
479497func (m *editorCmp) SetPosition(x, y int) tea.Cmd {
@@ -597,3 +615,51 @@ func New(app *app.App) Editor {
597615
598616 return e
599617}
618+
619+var maxAttachmentSize = 5 * 1024 * 1024 // 5MB
620+
621+var errNotAFile = errors.New("not a file")
622+
623+func pasteToFile(msg tea.PasteMsg) ([]byte, string, error) {
624+ content, path, err := filepathToFile(msg.Content)
625+ if err == nil {
626+ return content, path, err
627+ }
628+
629+ if strings.Count(msg.Content, "\n") > 2 {
630+ return contentToFile([]byte(msg.Content))
631+ }
632+
633+ return nil, "", errNotAFile
634+}
635+
636+func contentToFile(content []byte) ([]byte, string, error) {
637+ f, err := os.CreateTemp("", "paste_*.txt")
638+ if err != nil {
639+ return nil, "", err
640+ }
641+ if _, err := f.Write(content); err != nil {
642+ return nil, "", err
643+ }
644+ if err := f.Close(); err != nil {
645+ return nil, "", err
646+ }
647+ return content, f.Name(), nil
648+}
649+
650+func filepathToFile(name string) ([]byte, string, error) {
651+ path, err := filepath.Abs(strings.TrimSpace(strings.ReplaceAll(name, "\\", "")))
652+ if err != nil {
653+ return nil, "", err
654+ }
655+ content, err := os.ReadFile(path)
656+ if err != nil {
657+ return nil, "", err
658+ }
659+ return content, path, nil
660+}
661+
662+func mimeOf(content []byte) string {
663+ mimeBufferSize := min(512, len(content))
664+ return http.DetectContentType(content[:mimeBufferSize])
665+}