on main help sign in
fix: fix pasting files on some terminal emulators (#2106)
3c8be69Andrey Nering
internal/fsext/paste.go+27−9
@@ -1,20 +1,36 @@
1package fsext
1package fsext
2
2
3import (
3import (
4− "runtime"
4+ "os"
5 "strings"
5 "strings"
6)
6)
7
7
8−func PasteStringToPaths(s string) []string {
8+func ParsePastedFiles(s string) []string {
9+ s = strings.TrimSpace(s)
10+
11+ // NOTE: Rio on Windows adds NULL chars for some reason.
12+ s = strings.ReplaceAll(s, "\x00", "")
13+
9− switch runtime.GOOS {
14+ switch {
10− case "windows":
15+ case attemptStat(s):
11− return windowsPasteStringToPaths(s)
16+ return strings.Split(s, "\n")
17+ case os.Getenv("WT_SESSION") != "":
18+ return windowsTerminalParsePastedFiles(s)
12 default:
19 default:
13− return unixPasteStringToPaths(s)
20+ return unixParsePastedFiles(s)
21+ }
22+}
23+
24+func attemptStat(s string) bool {
25+ for path := range strings.SplitSeq(s, "\n") {
26+ if info, err := os.Stat(path); err != nil || info.IsDir() {
27+ return false
28+ }
14 }
29 }
30+ return true
15}
31}
16
32
17−func windowsPasteStringToPaths(s string) []string {
33+func windowsTerminalParsePastedFiles(s string) []string {
18 if strings.TrimSpace(s) == "" {
34 if strings.TrimSpace(s) == "" {
19 return nil
35 return nil
20 }
36 }
@@ -42,8 +58,10 @@ func windowsPasteStringToPaths(s string) []string {
42 }
58 }
43 case inQuotes:
59 case inQuotes:
44 current.WriteByte(ch)
60 current.WriteByte(ch)
61+ case ch != ' ':
62+ // Text outside quotes is not allowed
63+ return nil
45 }
64 }
46− // Skip characters outside quotes and spaces between quoted sections
47 }
65 }
48
66
49 // Add any remaining content if quotes were properly closed
67 // Add any remaining content if quotes were properly closed
@@ -59,7 +77,7 @@ func windowsPasteStringToPaths(s string) []string {
59 return paths
77 return paths
60}
78}
61
79
62−func unixPasteStringToPaths(s string) []string {
80+func unixParsePastedFiles(s string) []string {
63 if strings.TrimSpace(s) == "" {
81 if strings.TrimSpace(s) == "" {
64 return nil
82 return nil
65 }
83 }
internal/fsext/paste_test.go+6−6
@@ -6,8 +6,8 @@ import (
6 "github.com/stretchr/testify/require"
6 "github.com/stretchr/testify/require"
7)
7)
8
8
9−func TestPasteStringToPaths(t *testing.T) {
9+func TestParsePastedFiles(t *testing.T) {
10− t.Run("Windows", func(t *testing.T) {
10+ t.Run("WindowsTerminal", func(t *testing.T) {
11 tests := []struct {
11 tests := []struct {
12 name string
12 name string
13 input string
13 input string
@@ -24,7 +24,7 @@ func TestPasteStringToPaths(t *testing.T) {
24 expected: []string{`C:\path\my-screenshot-one.png`, `C:\path\my-screenshot-two.png`, `C:\path\my-screenshot-three.png`},
24 expected: []string{`C:\path\my-screenshot-one.png`, `C:\path\my-screenshot-two.png`, `C:\path\my-screenshot-three.png`},
25 },
25 },
26 {
26 {
27− name: "sigle with spaces",
27+ name: "single with spaces",
28 input: `"C:\path\my screenshot one.png"`,
28 input: `"C:\path\my screenshot one.png"`,
29 expected: []string{`C:\path\my screenshot one.png`},
29 expected: []string{`C:\path\my screenshot one.png`},
30 },
30 },
@@ -46,7 +46,7 @@ func TestPasteStringToPaths(t *testing.T) {
46 {
46 {
47 name: "text outside quotes",
47 name: "text outside quotes",
48 input: `"C:\path\file.png" some random text "C:\path\file2.png"`,
48 input: `"C:\path\file.png" some random text "C:\path\file2.png"`,
49− expected: []string{`C:\path\file.png`, `C:\path\file2.png`},
49+ expected: nil,
50 },
50 },
51 {
51 {
52 name: "multiple spaces between paths",
52 name: "multiple spaces between paths",
@@ -66,7 +66,7 @@ func TestPasteStringToPaths(t *testing.T) {
66 }
66 }
67 for _, tt := range tests {
67 for _, tt := range tests {
68 t.Run(tt.name, func(t *testing.T) {
68 t.Run(tt.name, func(t *testing.T) {
69− result := windowsPasteStringToPaths(tt.input)
69+ result := windowsTerminalParsePastedFiles(tt.input)
70 require.Equal(t, tt.expected, result)
70 require.Equal(t, tt.expected, result)
71 })
71 })
72 }
72 }
@@ -141,7 +141,7 @@ func TestPasteStringToPaths(t *testing.T) {
141 }
141 }
142 for _, tt := range tests {
142 for _, tt := range tests {
143 t.Run(tt.name, func(t *testing.T) {
143 t.Run(tt.name, func(t *testing.T) {
144− result := unixPasteStringToPaths(tt.input)
144+ result := unixParsePastedFiles(tt.input)
145 require.Equal(t, tt.expected, result)
145 require.Equal(t, tt.expected, result)
146 })
146 })
147 }
147 }
internal/ui/model/ui.go+4−1
@@ -2915,7 +2915,7 @@ func (m *UI) handlePasteMsg(msg tea.PasteMsg) tea.Cmd {
2915 // Attempt to parse pasted content as file paths. If possible to parse,
2915 // Attempt to parse pasted content as file paths. If possible to parse,
2916 // all files exist and are valid, add as attachments.
2916 // all files exist and are valid, add as attachments.
2917 // Otherwise, paste as text.
2917 // Otherwise, paste as text.
2918− paths := fsext.PasteStringToPaths(msg.Content)
2918+ paths := fsext.ParsePastedFiles(msg.Content)
2919 allExistsAndValid := func() bool {
2919 allExistsAndValid := func() bool {
2920 for _, path := range paths {
2920 for _, path := range paths {
2921 if _, err := os.Stat(path); os.IsNotExist(err) {
2921 if _, err := os.Stat(path); os.IsNotExist(err) {
@@ -2956,6 +2956,9 @@ func (m *UI) handleFilePathPaste(path string) tea.Cmd {
2956 if err != nil {
2956 if err != nil {
2957 return uiutil.ReportError(err)
2957 return uiutil.ReportError(err)
2958 }
2958 }
2959+ if fileInfo.IsDir() {
2960+ return uiutil.ReportWarn("Cannot attach a directory")
2961+ }
2959 if fileInfo.Size() > common.MaxAttachmentSize {
2962 if fileInfo.Size() > common.MaxAttachmentSize {
2960 return uiutil.ReportWarn("File is too big (>5mb)")
2963 return uiutil.ReportWarn("File is too big (>5mb)")
2961 }
2964 }