From fd889a2741891ee45116cb6131052d7fad220886 Mon Sep 17 00:00:00 2001 From: Christian Klotz Date: Mon, 28 Sep 2026 20:59:29 +0100 Subject: [PATCH] fix(coding-agent,tui): paste Finder file paths instead of icons (#10136) Read Finder file URLs before icon image data on macOS. Quote paths in bash mode and reject terminal control characters before rendering. Fixes #9999 Co-authored-by: 1dustycy --- packages/coding-agent/docs/keybindings.md | 2 +- packages/coding-agent/src/core/keybindings.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 37 ++++- packages/coding-agent/src/utils/clipboard.ts | 6 + .../clipboard-image-native-errors.test.ts | 10 +- .../test/clipboard-paste-file-paths.test.ts | 141 ++++++++++++++++++ .../coding-agent/test/interactive-tui.test.ts | 1 + packages/tui/native/clipboard.h | 42 +++++- packages/tui/native/darwin/README.md | 2 +- .../darwin-arm64/darwin-platform.node | Bin 51480 -> 52024 bytes .../prebuilds/darwin-x64/darwin-platform.node | Bin 18144 -> 22784 bytes .../tui/native/darwin/src/darwin-platform.m | 39 +++++ packages/tui/native/napi.h | 2 + packages/tui/src/native-platform.ts | 2 + 14 files changed, 272 insertions(+), 14 deletions(-) create mode 100644 packages/coding-agent/test/clipboard-paste-file-paths.test.ts diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index eb25606b3..440c130ec 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -125,7 +125,7 @@ In fullscreen mode, these actions control the transcript and take precedence ove | `app.exit` | `ctrl+d` | Exit (when editor empty) | | `app.suspend` | `ctrl+z` (None on Windows) | Suspend to background | | `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) | -| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste image or text from clipboard | +| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard | On native Windows, `app.suspend` has no default because Windows terminals do not support Unix job control. If you assign it manually, Pi shows a status message instead of suspending. WSL uses the normal `ctrl+z` and `fg` behavior. diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index 9136b07b9..f67cbe5e3 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -141,7 +141,7 @@ export const KEYBINDINGS = { }, "app.clipboard.pasteImage": { defaultKeys: windowsKeybindings ? "alt+v" : "ctrl+v", - description: "Paste image from clipboard (text fallback)", + description: "Paste files on macOS, images, or text from clipboard", }, "app.session.new": { defaultKeys: [], description: "Start a new session" }, "app.session.tree": { defaultKeys: [], description: "Open session tree" }, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 01e254b29..607eec41e 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -117,7 +117,7 @@ import type { TruncationResult } from "../../core/tools/truncate.ts"; import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.ts"; import { getUsageCostBreakdown } from "../../core/usage-totals.ts"; import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.ts"; -import { copyToClipboard, readClipboardText } from "../../utils/clipboard.ts"; +import { copyToClipboard, readClipboardFilePaths, readClipboardText } from "../../utils/clipboard.ts"; import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts"; import { parseGitUrl } from "../../utils/git.ts"; import { getCwdRelativePath } from "../../utils/paths.ts"; @@ -200,6 +200,10 @@ interface Expandable { setExpanded(expanded: boolean): void; } +interface ActiveEditor extends EditorComponent { + getCursor?(): { line: number; col: number }; +} + interface WorkingStatusEditor extends EditorComponent { readonly embedWorkingStatus: boolean; setWorkingStatusIndicator(indicator: StatusIndicator | undefined): void; @@ -428,7 +432,7 @@ export class InteractiveMode { private pendingMessagesContainer: Container; private statusContainer: Container; private defaultEditor: CustomEditor; - private editor: EditorComponent; + private editor: ActiveEditor; private editorComponentFactory: EditorFactory | undefined; private autocompleteProvider: AutocompleteProvider | undefined; private autocompleteProviderWrappers: AutocompleteProviderFactory[] = []; @@ -994,7 +998,7 @@ export class InteractiveMode { rawKeyHint("!!", "to run bash (no context)"), hint("app.message.followUp", "to queue follow-up"), hint("app.message.dequeue", "to edit all queued messages"), - hint("app.clipboard.pasteImage", "to paste image (with text fallback)"), + hint("app.clipboard.pasteImage", "to paste files on macOS, images, or text"), rawKeyHint("drop files", "to attach"), ].join("\n"); const compactInstructions = () => @@ -3017,8 +3021,8 @@ export class InteractiveMode { } }; - // Handle clipboard paste (triggered on Ctrl+V). Images are attached by path; - // otherwise, paste plain text from the system clipboard. + // Handle clipboard paste (triggered on Ctrl+V). Copied files use their original paths, + // images are attached via temporary files, and plain text is the final fallback. this.defaultEditor.onPasteImage = () => { void this.handleClipboardPaste(); }; @@ -3040,6 +3044,23 @@ export class InteractiveMode { private async handleClipboardPaste(): Promise { try { + const filePaths = await readClipboardFilePaths(); + if (filePaths) { + if (filePaths.some((filePath) => /\p{Cc}/u.test(filePath))) { + throw new Error("Clipboard file path contains control characters"); + } + const paths = this.isBashMode ? filePaths.map(quoteIfNeeded).join(" ") : filePaths.join("\n"); + const cursor = this.editor.getCursor?.(); + const currentLine = cursor ? (this.editor.getText().split("\n")[cursor.line] ?? "") : ""; + const characterBeforeCursor = cursor && cursor.col > 0 ? currentLine[cursor.col - 1] : ""; + const characterAfterCursor = cursor ? currentLine[cursor.col] : ""; + const leadingSpace = characterBeforeCursor && !/\s/.test(characterBeforeCursor) ? " " : ""; + const trailingSpace = characterAfterCursor && !/\s/.test(characterAfterCursor) ? " " : ""; + this.editor.insertTextAtCursor?.(`${leadingSpace}${paths}${trailingSpace}`); + this.ui.requestRender(); + return; + } + const image = await readClipboardImage(); if (image) { const tmpDir = os.tmpdir(); @@ -3058,8 +3079,8 @@ export class InteractiveMode { this.editor.insertTextAtCursor?.(text); this.ui.requestRender(); } - } catch { - // Silently ignore clipboard errors (may not have permission, etc.) + } catch (error) { + this.showError(`Failed to paste from clipboard: ${error instanceof Error ? error.message : String(error)}`); } } @@ -6636,7 +6657,7 @@ export class InteractiveMode { | \`${copyMessage}\` | Copy selection or last assistant message | | \`${followUp}\` | Queue follow-up message | | \`${dequeue}\` | Restore queued messages | -| \`${pasteImage}\` | Paste image or text from clipboard | +| \`${pasteImage}\` | Paste files on macOS, images, or text from clipboard | | \`/\` | Slash commands | | \`!\` | Run bash command | | \`!!\` | Run bash command (excluded from context) | diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index e14111794..926d9f9f9 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -71,6 +71,12 @@ export async function readClipboardText(): Promise { } } +/** Read file paths, such as Finder file copies, from the native clipboard. */ +export async function readClipboardFilePaths(): Promise { + const paths = await getNativeClipboard()?.getFilePaths?.(); + return paths?.length ? paths : null; +} + export async function copyToClipboard(text: string): Promise { const p = platform(); const env = process.env; diff --git a/packages/coding-agent/test/clipboard-image-native-errors.test.ts b/packages/coding-agent/test/clipboard-image-native-errors.test.ts index ef64da4dc..1ef9bd949 100644 --- a/packages/coding-agent/test/clipboard-image-native-errors.test.ts +++ b/packages/coding-agent/test/clipboard-image-native-errors.test.ts @@ -11,7 +11,11 @@ vi.mock("@earendil-works/pi-tui", async (importOriginal) => ({ }, }), })); -vi.mock("../src/utils/clipboard.ts", () => ({ copyToClipboard: vi.fn(), readClipboardText })); +vi.mock("../src/utils/clipboard.ts", () => ({ + copyToClipboard: vi.fn(), + readClipboardFilePaths: vi.fn(async () => null), + readClipboardText, +})); vi.mock("../src/utils/clipboard-command.ts", () => ({ runClipboardCommand: vi.fn(async () => undefined) })); afterEach(() => vi.unstubAllEnvs()); @@ -19,6 +23,7 @@ test("native image errors abort paste without reading text or changing the edito vi.stubEnv("TERMUX_VERSION", ""); const context = { editor: { insertTextAtCursor: vi.fn() }, + showError: vi.fn(), ui: { requestRender: vi.fn() }, }; const prototype = InteractiveMode.prototype as unknown as { @@ -28,4 +33,7 @@ test("native image errors abort paste without reading text or changing the edito expect(readClipboardText).not.toHaveBeenCalled(); expect(context.editor.insertTextAtCursor).not.toHaveBeenCalled(); expect(context.ui.requestRender).not.toHaveBeenCalled(); + expect(context.showError).toHaveBeenCalledExactlyOnceWith( + "Failed to paste from clipboard: Native clipboard operation failed", + ); }); diff --git a/packages/coding-agent/test/clipboard-paste-file-paths.test.ts b/packages/coding-agent/test/clipboard-paste-file-paths.test.ts new file mode 100644 index 000000000..fb141fb20 --- /dev/null +++ b/packages/coding-agent/test/clipboard-paste-file-paths.test.ts @@ -0,0 +1,141 @@ +import { beforeEach, expect, test, vi } from "vitest"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; + +const mocks = vi.hoisted(() => ({ + readClipboardFilePaths: vi.fn<() => Promise>(), + readClipboardImage: vi.fn<() => Promise<{ bytes: Uint8Array; mimeType: string } | null>>(), + readClipboardText: vi.fn<() => Promise>(), +})); + +vi.mock("../src/utils/clipboard.ts", () => ({ + copyToClipboard: vi.fn(), + readClipboardFilePaths: mocks.readClipboardFilePaths, + readClipboardText: mocks.readClipboardText, +})); +vi.mock("../src/utils/clipboard-image.ts", () => ({ + extensionForImageMimeType: () => "png", + readClipboardImage: mocks.readClipboardImage, +})); + +beforeEach(() => vi.resetAllMocks()); + +test("Finder file paths take precedence over their icon image", async () => { + // Regression test for #9999. + const filePaths = ["/tmp/screenshot.png", "/tmp/My Photos/photo.png"]; + mocks.readClipboardFilePaths.mockResolvedValue(filePaths); + mocks.readClipboardImage.mockResolvedValue({ + bytes: new Uint8Array([0x89, 0x50, 0x4e, 0x47]), + mimeType: "image/png", + }); + mocks.readClipboardText.mockResolvedValue(null); + const insertTextAtCursor = vi.fn<(text: string) => void>(); + const context = { + editor: { insertTextAtCursor }, + showError: vi.fn(), + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + expect(insertTextAtCursor).toHaveBeenCalledExactlyOnceWith(filePaths.join("\n")); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); +}); + +test("clipboard file paths containing terminal control characters are rejected", async () => { + const insertTextAtCursor = vi.fn<(text: string) => void>(); + const showError = vi.fn<(message: string) => void>(); + mocks.readClipboardFilePaths.mockResolvedValue(["/tmp/photo\x1b]0;unsafe\x07.png"]); + const context = { + editor: { insertTextAtCursor }, + showError, + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + + expect(insertTextAtCursor).not.toHaveBeenCalled(); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); + expect(showError).toHaveBeenCalledExactlyOnceWith( + "Failed to paste from clipboard: Clipboard file path contains control characters", + ); +}); + +test("native file-path errors are shown without falling through to the icon image", async () => { + const showError = vi.fn<(message: string) => void>(); + mocks.readClipboardFilePaths.mockRejectedValue(new Error("Native clipboard file read failed")); + const context = { + editor: { insertTextAtCursor: vi.fn() }, + showError, + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + + expect(context.editor.insertTextAtCursor).not.toHaveBeenCalled(); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); + expect(showError).toHaveBeenCalledExactlyOnceWith( + "Failed to paste from clipboard: Native clipboard file read failed", + ); +}); + +test.each([ + ["punctuation", "Review:", 7], + ["Unicode text", "確認", 2], +])("clipboard file paths are separated from preceding %s", async (_description, editorText, cursorCol) => { + const insertTextAtCursor = vi.fn<(text: string) => void>(); + mocks.readClipboardFilePaths.mockResolvedValue(["/tmp/photo.png"]); + const context = { + editor: { + getCursor: () => ({ line: 0, col: cursorCol }), + getText: () => editorText, + insertTextAtCursor, + }, + showError: vi.fn(), + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + + expect(insertTextAtCursor).toHaveBeenCalledExactlyOnceWith(" /tmp/photo.png"); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); +}); + +test("bash mode shell-quotes file paths and inserts them as arguments", async () => { + const insertTextAtCursor = vi.fn<(text: string) => void>(); + mocks.readClipboardFilePaths.mockResolvedValue([ + "/tmp/My Photos/photo.png", + "/tmp/$(touch hacked).png", + "/tmp/plain.png", + ]); + const context = { + editor: { + getCursor: () => ({ line: 0, col: 3 }), + getText: () => "catDEST", + insertTextAtCursor, + }, + isBashMode: true, + showError: vi.fn(), + ui: { requestRender: vi.fn() }, + }; + const prototype = InteractiveMode.prototype as unknown as { + handleClipboardPaste(this: typeof context): Promise; + }; + + await prototype.handleClipboardPaste.call(context); + + expect(insertTextAtCursor).toHaveBeenCalledExactlyOnceWith( + " '/tmp/My Photos/photo.png' '/tmp/$(touch hacked).png' /tmp/plain.png ", + ); + expect(mocks.readClipboardImage).not.toHaveBeenCalled(); +}); diff --git a/packages/coding-agent/test/interactive-tui.test.ts b/packages/coding-agent/test/interactive-tui.test.ts index 63ce7d35a..b9b18f8b2 100644 --- a/packages/coding-agent/test/interactive-tui.test.ts +++ b/packages/coding-agent/test/interactive-tui.test.ts @@ -21,6 +21,7 @@ import { initTheme } from "../src/modes/interactive/theme/theme.ts"; const clipboardMocks = vi.hoisted(() => ({ copyToClipboard: vi.fn<(text: string) => Promise>(), + readClipboardFilePaths: vi.fn<() => Promise>(), readClipboardText: vi.fn<() => Promise>(), })); diff --git a/packages/tui/native/clipboard.h b/packages/tui/native/clipboard.h index 0987b4ed6..4e7d0a70f 100644 --- a/packages/tui/native/clipboard.h +++ b/packages/tui/native/clipboard.h @@ -7,8 +7,17 @@ #include #endif -typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE } clipboard_operation; -typedef enum { CLIPBOARD_UNAVAILABLE, CLIPBOARD_EMPTY, CLIPBOARD_UTF8, CLIPBOARD_LATIN1, CLIPBOARD_UTF16, CLIPBOARD_BUFFER } clipboard_format; +typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE, CLIPBOARD_FILES } clipboard_operation; +// CLIPBOARD_PATHS stores concatenated, NUL-terminated UTF-8 paths in clipboard_job.data. +typedef enum { + CLIPBOARD_UNAVAILABLE, + CLIPBOARD_EMPTY, + CLIPBOARD_UTF8, + CLIPBOARD_LATIN1, + CLIPBOARD_UTF16, + CLIPBOARD_BUFFER, + CLIPBOARD_PATHS +} clipboard_format; typedef struct { napi_async_work work; @@ -63,6 +72,33 @@ static void PI_NAPI_CALL execute_clipboard_work(napi_env env, void* data) { clipboard_execute(data); } +static int create_clipboard_paths_value( + napi_env env, + const clipboard_job* job, + napi_create_string_utf8_fn create_string, + napi_value* result +) { + napi_create_array_fn create_array = (napi_create_array_fn)node_symbol("napi_create_array"); + napi_set_element_fn set_element = (napi_set_element_fn)node_symbol("napi_set_element"); + if (!create_array || !set_element || create_array(env, result) != 0) return 1; + + const char* cursor = job->data; + const char* end = cursor + job->length; + for (uint32_t index = 0; cursor < end; index++) { + const char* terminator = cursor; + while (terminator < end && *terminator != '\0') terminator++; + if (terminator == end) return 1; + + napi_value item = 0; + if (create_string(env, cursor, (size_t)(terminator - cursor), &item) != 0 || + set_element(env, *result, index, item) != 0) { + return 1; + } + cursor = terminator + 1; + } + return 0; +} + static void PI_NAPI_CALL complete_clipboard_work(napi_env env, int status, void* data) { clipboard_job* job = data; napi_create_string_utf8_fn create_string = (napi_create_string_utf8_fn)node_symbol("napi_create_string_utf8"); @@ -71,6 +107,8 @@ static void PI_NAPI_CALL complete_clipboard_work(napi_env env, int status, void* if (!job->error && job->operation != CLIPBOARD_WRITE) { if (job->format == CLIPBOARD_EMPTY) { result = null_value(env); + } else if (job->format == CLIPBOARD_PATHS) { + status = create_clipboard_paths_value(env, job, create_string, &result); } else if (job->format == CLIPBOARD_BUFFER) { napi_create_buffer_copy_fn create_buffer = (napi_create_buffer_copy_fn)node_symbol("napi_create_buffer_copy"); status = create_buffer(env, job->length, job->data, 0, &result); diff --git a/packages/tui/native/darwin/README.md b/packages/tui/native/darwin/README.md index 2eb5c722e..74c5c5911 100644 --- a/packages/tui/native/darwin/README.md +++ b/packages/tui/native/darwin/README.md @@ -1,6 +1,6 @@ # Darwin native prebuilds -Provides modifier-key state and asynchronous text/image clipboard access using AppKit. +Provides modifier-key state and asynchronous text, image, and file-path clipboard access using AppKit. ## Building diff --git a/packages/tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node b/packages/tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node index f453d0c06d5f17cad59ed232f6280564569e5708..3294b6bffd36eaab70d61134e9955992f31f9a8f 100755 GIT binary patch delta 5690 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 CDtt%) delta 3651 zcmZu!4Nz3q6~6n}Wr1Z~Kvs7_Wg(*2miPbXE80sH1W?C$1n=}(;GB&=IB-YfZgO%6s-ghCE=^frZ z?>oQuoO|xuOZz*fp^&N58}A|iJ3L)a=1DVSyY?4!5t2Fe#q0xaTjP9)}7`d8^;XFeosL>YuIJQl!B*GjYLvk`Ms@F5u^u5W*r#cvyLRfs;Uo3NX<^ONzfs<0a^M=$qEZod>Wgwz@fZSeiY+Q8YgF zM>IbwuK0E=A#%uG34!KDB67(2t-<;wkYBkYE0;BvAp}t#x>4~V! z`4v;aQ;Dn;4#=V0vk++x3n4z`Jm7f-35R`vml(WhrtauBCzWw0tw-x4S7{ex=%1qV zXV}MB)>RryKG@sx!b!F#S-Q47hR%)2%-(0?tJF@SRKsRjEj%i#_78CeWHnk|R6Nk) zkUP%FtG#QyRo&GFkcOyU3GN zg&kuMCAQ7ZyUY}S4Utbd2fHsAWEC;;`-;I9-6va4Ac7B!(WhHO9Xh4el^X z=TjK>wP?B^HbbH@v?lh@r1v01*@ta%DCbf%onlW0lcmDK3x@l9^1rhWMx!-pLZs&NAj4f3U9f$%)7Ew?)Wmp zhelB?J@JTXY`r!EYOE$|?y5A>26oP1Dj~Wv$aIuG&Bo;y5Zkd!fn%?_ERMa0@ZND( zas=J1x76J9G_3Lh>ln4ddoFt_|cu&aLD$88P8{xS97*QF00d%AVDeh zLAgz}jH35tH44&NNWu0ipX-CWdL6-^Goz42#!(NCu%ID}3GY5-Sw?s#cWC(oH;#kx zJi}Obo@OlMQNWwo2!9^der0oyU%95u0^hGbV&(d6*Zj)Yx>IE~Y@Nrs;x$cz5*FG7 zK5UrIAg@`@CmxpJDlqlIdg&>@qC+SvXS7T+R=HEeLCHOQWmt1rFM8vstVm!c3#PIJ z`R0D*G3vhuJ`v0cAzY#=Sp9CeGa^0*DL?LK3`qkdXkx#beikDiX%26b&)UuO9!KyF;zK#N0m_<=BL~AIyu&FP4ohl*A)=}| zv7F1&=rdLk<_GjVFB&D>OD5A%bHXWXz1MZ;0ul8zbBs>j!j7J5@iI2w@4ghoM={aY zy3;1KySU?XoTpF1w_`E3eKzk&YB-oo}8-(0rWGMbNd zfHMGX*2vCs{&GV*W3=URKz8016%Mo{<2#q}z;_PI>_K;h7v9%(t@;@l@bmw`p>@g1 zpjWx&!|@K~uE(Bf7w_phzDWqBg^)TLYM?N9wb>3|U93};Co=F|>e!J%9J}~#gmNQL z3XR^Q!&9=_zJZQEl>RqH90fxx^I~|E)h4uI*#OIc{UjVjF|=$hm~ZgZ9WJe#2fU&k zWVGX)>w#3@^JVz#*byf+A6B18)cZT`njJ6fWBm}azro15#GUfvrVsqeUHx-}XoI@; zml4G7sO#rlvC$ve+>dDNj1BnILNfdm^nSy#K0HD{otm`fG~S1;V7|hb?aWwha>;6` zM-CMmi$`}Qpcc$3{M%7o&+26*G71u8jPNZ?>pM5%quPh(9Xu!TbWyK6(dxqwG@Q=RO~EnqD>yJzT1%x7g^4q}`EWlqp<8Ba**=EHAlGqKBs6p#3vyVr$nnuU)#Oapm@B z>lV^iQ(EaFPXTTBWJ|S)^n_cF?$}xVBT`UfM6{M}iIs`lXOHxej z^gziH`w%vn4^!vle7dJ%YFcMveqc*Y{kp*BrVXWa4Yg#et5a|q>o(P`ZK|VHD+-eS zWdb)4_;G2$#&yjbw=}G6+@2W-kgW?k>HQTS(>bN_7Sc%?D`{$J296D(<)w3{H(iL> zV~TY+&RC{qPd#Ei9g)xS@5VJSk(lYrrIl?sl*VAcrZ`aeGe-9Wd}adv%)l%ZAGa{W zc-)`WaEARi2K$l5-#DJjaocx{>*Q}yz=!=!f^utOUeZMl@Rl9vtZJ?>S Qr6y3bv7xrE5w7|8FDewwoB#j- diff --git a/packages/tui/native/darwin/src/darwin-platform.m b/packages/tui/native/darwin/src/darwin-platform.m index 38823357f..5cdd8f912 100644 --- a/packages/tui/native/darwin/src/darwin-platform.m +++ b/packages/tui/native/darwin/src/darwin-platform.m @@ -66,6 +66,40 @@ static void clipboard_execute(clipboard_job* job) { } else { clipboard_copy(job, text.UTF8String, [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding], CLIPBOARD_UTF8); } + } else if (job->operation == CLIPBOARD_FILES) { + NSArray* urls = [pasteboard readObjectsForClasses:@[[NSURL class]] + options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}]; + size_t total = 0; + for (NSURL* url in urls) { + const char* path = url.fileSystemRepresentation; + if (!path) continue; + size_t length = strlen(path); + if (length == SIZE_MAX || total > SIZE_MAX - length - 1) { + job->error = "Clipboard file paths are too large"; + return; + } + total += length + 1; + } + if (total == 0) { + job->format = CLIPBOARD_EMPTY; + return; + } + char* paths = clipboard_alloc(total); + if (!paths) { + job->error = "Out of memory"; + return; + } + char* cursor = paths; + for (NSURL* url in urls) { + const char* path = url.fileSystemRepresentation; + if (!path) continue; + size_t length = strlen(path); + memcpy(cursor, path, length + 1); + cursor += length + 1; + } + job->data = paths; + job->length = total; + job->format = CLIPBOARD_PATHS; } else { if (![pasteboard availableTypeFromArray:@[ NSPasteboardTypePNG, NSPasteboardTypeTIFF ]]) { job->format = CLIPBOARD_EMPTY; @@ -84,10 +118,15 @@ static void clipboard_execute(clipboard_job* job) { } } +static napi_value PI_NAPI_CALL get_clipboard_file_paths(napi_env env, napi_callback_info info) { + return queue_clipboard(env, info, CLIPBOARD_FILES); +} + PI_NAPI_EXPORT napi_value napi_register_module_v1(napi_env env, napi_value exports) { set_function_export(env, exports, "isModifierPressed", is_modifier_pressed); set_function_export(env, exports, "getText", get_clipboard_text); set_function_export(env, exports, "setText", set_clipboard_text); set_function_export(env, exports, "getImage", get_clipboard_image); + set_function_export(env, exports, "getFilePaths", get_clipboard_file_paths); return exports; } diff --git a/packages/tui/native/napi.h b/packages/tui/native/napi.h index 93c985c67..816a8202e 100644 --- a/packages/tui/native/napi.h +++ b/packages/tui/native/napi.h @@ -26,6 +26,7 @@ typedef void* napi_deferred; typedef void (PI_NAPI_CALL *napi_async_execute_callback)(napi_env, void*); typedef void (PI_NAPI_CALL *napi_async_complete_callback)(napi_env, int, void*); typedef int (PI_NAPI_CALL *napi_create_async_work_fn)(napi_env, napi_value, napi_value, napi_async_execute_callback, napi_async_complete_callback, void*, napi_async_work*); +typedef int (PI_NAPI_CALL *napi_create_array_fn)(napi_env, napi_value*); typedef int (PI_NAPI_CALL *napi_async_work_fn)(napi_env, napi_async_work); typedef int (PI_NAPI_CALL *napi_create_promise_fn)(napi_env, napi_deferred*, napi_value*); typedef int (PI_NAPI_CALL *napi_settle_deferred_fn)(napi_env, napi_deferred, napi_value); @@ -41,6 +42,7 @@ typedef int (PI_NAPI_CALL *napi_get_value_fn)(napi_env, napi_value*); typedef int (PI_NAPI_CALL *napi_get_value_string_utf8_fn)(napi_env, napi_value, char*, size_t, size_t*); typedef int (PI_NAPI_CALL *napi_get_value_string_utf16_fn)(napi_env, napi_value, uint16_t*, size_t, size_t*); typedef int (PI_NAPI_CALL *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value); +typedef int (PI_NAPI_CALL *napi_set_element_fn)(napi_env, napi_value, uint32_t, napi_value); typedef int (PI_NAPI_CALL *napi_throw_error_fn)(napi_env, const char*, const char*); static void* node_symbol(const char* name) { diff --git a/packages/tui/src/native-platform.ts b/packages/tui/src/native-platform.ts index f5e82c035..3a6580a44 100644 --- a/packages/tui/src/native-platform.ts +++ b/packages/tui/src/native-platform.ts @@ -11,6 +11,8 @@ export interface NativeClipboard { getText(): Promise; /** Undefined means unavailable, null means no image; transfer failures reject. */ getImage(): Promise; + /** POSIX paths of file URLs on the clipboard. Undefined means unsupported, null means no files. */ + getFilePaths?(): Promise; /** Linux uses command-line tools to retain clipboard ownership instead. */ setText?(text: string): Promise; }