@@ -0,0 +1,149 @@
+package fsext
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestPasteStringToPaths(t *testing.T) {
+ t.Run("Windows", func(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected []string
+ }{
+ {
+ name: "single path",
+ input: `"C:\path\my-screenshot-one.png"`,
+ expected: []string{`C:\path\my-screenshot-one.png`},
+ },
+ {
+ name: "multiple paths no spaces",
+ input: `"C:\path\my-screenshot-one.png" "C:\path\my-screenshot-two.png" "C:\path\my-screenshot-three.png"`,
+ expected: []string{`C:\path\my-screenshot-one.png`, `C:\path\my-screenshot-two.png`, `C:\path\my-screenshot-three.png`},
+ },
+ {
+ name: "sigle with spaces",
+ input: `"C:\path\my screenshot one.png"`,
+ expected: []string{`C:\path\my screenshot one.png`},
+ },
+ {
+ name: "multiple paths with spaces",
+ input: `"C:\path\my screenshot one.png" "C:\path\my screenshot two.png" "C:\path\my screenshot three.png"`,
+ expected: []string{`C:\path\my screenshot one.png`, `C:\path\my screenshot two.png`, `C:\path\my screenshot three.png`},
+ },
+ {
+ name: "empty string",
+ input: "",
+ expected: nil,
+ },
+ {
+ name: "unclosed quotes",
+ input: `"C:\path\file.png`,
+ expected: nil,
+ },
+ {
+ name: "text outside quotes",
+ input: `"C:\path\file.png" some random text "C:\path\file2.png"`,
+ expected: []string{`C:\path\file.png`, `C:\path\file2.png`},
+ },
+ {
+ name: "multiple spaces between paths",
+ input: `"C:\path\file1.png" "C:\path\file2.png"`,
+ expected: []string{`C:\path\file1.png`, `C:\path\file2.png`},
+ },
+ {
+ name: "just whitespace",
+ input: " ",
+ expected: nil,
+ },
+ {
+ name: "consecutive quoted sections",
+ input: `"C:\path1""C:\path2"`,
+ expected: []string{`C:\path1`, `C:\path2`},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := windowsPasteStringToPaths(tt.input)
+ require.Equal(t, tt.expected, result)
+ })
+ }
+ })
+
+ t.Run("Unix", func(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected []string
+ }{
+ {
+ name: "single path",
+ input: `/path/my-screenshot.png`,
+ expected: []string{"/path/my-screenshot.png"},
+ },
+ {
+ name: "multiple paths no spaces",
+ input: `/path/screenshot-one.png /path/screenshot-two.png /path/screenshot-three.png`,
+ expected: []string{"/path/screenshot-one.png", "/path/screenshot-two.png", "/path/screenshot-three.png"},
+ },
+ {
+ name: "sigle with spaces",
+ input: `/path/my\ screenshot\ one.png`,
+ expected: []string{"/path/my screenshot one.png"},
+ },
+ {
+ name: "multiple paths with spaces",
+ input: `/path/my\ screenshot\ one.png /path/my\ screenshot\ two.png /path/my\ screenshot\ three.png`,
+ expected: []string{"/path/my screenshot one.png", "/path/my screenshot two.png", "/path/my screenshot three.png"},
+ },
+ {
+ name: "empty string",
+ input: "",
+ expected: nil,
+ },
+ {
+ name: "double backslash escapes",
+ input: `/path/my\\file.png`,
+ expected: []string{"/path/my\\file.png"},
+ },
+ {
+ name: "trailing backslash",
+ input: `/path/file\`,
+ expected: []string{`/path/file\`},
+ },
+ {
+ name: "multiple consecutive escaped spaces",
+ input: `/path/file\ \ with\ \ many\ \ spaces.png`,
+ expected: []string{"/path/file with many spaces.png"},
+ },
+ {
+ name: "multiple unescaped spaces",
+ input: `/path/file1.png /path/file2.png`,
+ expected: []string{"/path/file1.png", "/path/file2.png"},
+ },
+ {
+ name: "just whitespace",
+ input: " ",
+ expected: nil,
+ },
+ {
+ name: "tab characters",
+ input: "/path/file1.png\t/path/file2.png",
+ expected: []string{"/path/file1.png\t/path/file2.png"},
+ },
+ {
+ name: "newlines in input",
+ input: "/path/file1.png\n/path/file2.png",
+ expected: []string{"/path/file1.png\n/path/file2.png"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := unixPasteStringToPaths(tt.input)
+ require.Equal(t, tt.expected, result)
+ })
+ }
+ })
+}
@@ -29,6 +29,7 @@ import (
"github.com/charmbracelet/crush/internal/commands"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/filetracker"
+ "github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/history"
"github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/message"
@@ -2817,29 +2818,45 @@ func (m *UI) handlePasteMsg(msg tea.PasteMsg) tea.Cmd {
}
}
- var cmd tea.Cmd
- path := strings.ReplaceAll(msg.Content, "\\ ", " ")
- // Try to get an image.
- path, err := filepath.Abs(strings.TrimSpace(path))
- if err != nil {
- m.textarea, cmd = m.textarea.Update(msg)
- return cmd
- }
+ // Attempt to parse pasted content as file paths. If possible to parse,
+ // all files exist and are valid, add as attachments.
+ // Otherwise, paste as text.
+ paths := fsext.PasteStringToPaths(msg.Content)
+ allExistsAndValid := func() bool {
+ for _, path := range paths {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return false
+ }
- // Check if file has an allowed image extension.
- isAllowedType := false
- lowerPath := strings.ToLower(path)
- for _, ext := range common.AllowedImageTypes {
- if strings.HasSuffix(lowerPath, ext) {
- isAllowedType = true
- break
+ lowerPath := strings.ToLower(path)
+ isValid := false
+ for _, ext := range common.AllowedImageTypes {
+ if strings.HasSuffix(lowerPath, ext) {
+ isValid = true
+ break
+ }
+ }
+ if !isValid {
+ return false
+ }
}
+ return true
}
- if !isAllowedType {
+ if !allExistsAndValid() {
+ var cmd tea.Cmd
m.textarea, cmd = m.textarea.Update(msg)
return cmd
}
+ var cmds []tea.Cmd
+ for _, path := range paths {
+ cmds = append(cmds, m.handleFilePathPaste(path))
+ }
+ return tea.Batch(cmds...)
+}
+
+// handleFilePathPaste handles a pasted file path.
+func (m *UI) handleFilePathPaste(path string) tea.Cmd {
return func() tea.Msg {
fileInfo, err := os.Stat(path)
if err != nil {