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 <onedustycy@gmail.com>
This commit is contained in:
Christian Klotz 2026-09-28 20:59:29 +01:00 • committed by GitHub
parent 3883fcb1f3
commit fd889a2741
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 272 additions and 14 deletions

View file

@ -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.exit` | `ctrl+d` | Exit (when editor empty) |
| `app.suspend` | `ctrl+z` (None on Windows) | Suspend to background | | `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.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. 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.

View file

@ -141,7 +141,7 @@ export const KEYBINDINGS = {
}, },
"app.clipboard.pasteImage": { "app.clipboard.pasteImage": {
defaultKeys: windowsKeybindings ? "alt+v" : "ctrl+v", 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.new": { defaultKeys: [], description: "Start a new session" },
"app.session.tree": { defaultKeys: [], description: "Open session tree" }, "app.session.tree": { defaultKeys: [], description: "Open session tree" },

View file

@ -117,7 +117,7 @@ import type { TruncationResult } from "../../core/tools/truncate.ts";
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.ts"; import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.ts";
import { getUsageCostBreakdown } from "../../core/usage-totals.ts"; import { getUsageCostBreakdown } from "../../core/usage-totals.ts";
import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.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 { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts";
import { parseGitUrl } from "../../utils/git.ts"; import { parseGitUrl } from "../../utils/git.ts";
import { getCwdRelativePath } from "../../utils/paths.ts"; import { getCwdRelativePath } from "../../utils/paths.ts";
@ -200,6 +200,10 @@ interface Expandable {
setExpanded(expanded: boolean): void; setExpanded(expanded: boolean): void;
} }
interface ActiveEditor extends EditorComponent {
getCursor?(): { line: number; col: number };
}
interface WorkingStatusEditor extends EditorComponent { interface WorkingStatusEditor extends EditorComponent {
readonly embedWorkingStatus: boolean; readonly embedWorkingStatus: boolean;
setWorkingStatusIndicator(indicator: StatusIndicator | undefined): void; setWorkingStatusIndicator(indicator: StatusIndicator | undefined): void;
@ -428,7 +432,7 @@ export class InteractiveMode {
private pendingMessagesContainer: Container; private pendingMessagesContainer: Container;
private statusContainer: Container; private statusContainer: Container;
private defaultEditor: CustomEditor; private defaultEditor: CustomEditor;
private editor: EditorComponent; private editor: ActiveEditor;
private editorComponentFactory: EditorFactory | undefined; private editorComponentFactory: EditorFactory | undefined;
private autocompleteProvider: AutocompleteProvider | undefined; private autocompleteProvider: AutocompleteProvider | undefined;
private autocompleteProviderWrappers: AutocompleteProviderFactory[] = []; private autocompleteProviderWrappers: AutocompleteProviderFactory[] = [];
@ -994,7 +998,7 @@ export class InteractiveMode {
rawKeyHint("!!", "to run bash (no context)"), rawKeyHint("!!", "to run bash (no context)"),
hint("app.message.followUp", "to queue follow-up"), hint("app.message.followUp", "to queue follow-up"),
hint("app.message.dequeue", "to edit all queued messages"), 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"), rawKeyHint("drop files", "to attach"),
].join("\n"); ].join("\n");
const compactInstructions = () => const compactInstructions = () =>
@ -3017,8 +3021,8 @@ export class InteractiveMode {
} }
}; };
// Handle clipboard paste (triggered on Ctrl+V). Images are attached by path; // Handle clipboard paste (triggered on Ctrl+V). Copied files use their original paths,
// otherwise, paste plain text from the system clipboard. // images are attached via temporary files, and plain text is the final fallback.
this.defaultEditor.onPasteImage = () => { this.defaultEditor.onPasteImage = () => {
void this.handleClipboardPaste(); void this.handleClipboardPaste();
}; };
@ -3040,6 +3044,23 @@ export class InteractiveMode {
private async handleClipboardPaste(): Promise<void> { private async handleClipboardPaste(): Promise<void> {
try { 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(); const image = await readClipboardImage();
if (image) { if (image) {
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
@ -3058,8 +3079,8 @@ export class InteractiveMode {
this.editor.insertTextAtCursor?.(text); this.editor.insertTextAtCursor?.(text);
this.ui.requestRender(); this.ui.requestRender();
} }
} catch { } catch (error) {
// Silently ignore clipboard errors (may not have permission, etc.) 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 | | \`${copyMessage}\` | Copy selection or last assistant message |
| \`${followUp}\` | Queue follow-up message | | \`${followUp}\` | Queue follow-up message |
| \`${dequeue}\` | Restore queued messages | | \`${dequeue}\` | Restore queued messages |
| \`${pasteImage}\` | Paste image or text from clipboard | | \`${pasteImage}\` | Paste files on macOS, images, or text from clipboard |
| \`/\` | Slash commands | | \`/\` | Slash commands |
| \`!\` | Run bash command | | \`!\` | Run bash command |
| \`!!\` | Run bash command (excluded from context) | | \`!!\` | Run bash command (excluded from context) |

View file

@ -71,6 +71,12 @@ export async function readClipboardText(): Promise<string | null> {
} }
} }
/** Read file paths, such as Finder file copies, from the native clipboard. */
export async function readClipboardFilePaths(): Promise<string[] | null> {
const paths = await getNativeClipboard()?.getFilePaths?.();
return paths?.length ? paths : null;
}
export async function copyToClipboard(text: string): Promise<void> { export async function copyToClipboard(text: string): Promise<void> {
const p = platform(); const p = platform();
const env = process.env; const env = process.env;

View file

@ -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) })); vi.mock("../src/utils/clipboard-command.ts", () => ({ runClipboardCommand: vi.fn(async () => undefined) }));
afterEach(() => vi.unstubAllEnvs()); afterEach(() => vi.unstubAllEnvs());
@ -19,6 +23,7 @@ test("native image errors abort paste without reading text or changing the edito
vi.stubEnv("TERMUX_VERSION", ""); vi.stubEnv("TERMUX_VERSION", "");
const context = { const context = {
editor: { insertTextAtCursor: vi.fn() }, editor: { insertTextAtCursor: vi.fn() },
showError: vi.fn(),
ui: { requestRender: vi.fn() }, ui: { requestRender: vi.fn() },
}; };
const prototype = InteractiveMode.prototype as unknown as { 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(readClipboardText).not.toHaveBeenCalled();
expect(context.editor.insertTextAtCursor).not.toHaveBeenCalled(); expect(context.editor.insertTextAtCursor).not.toHaveBeenCalled();
expect(context.ui.requestRender).not.toHaveBeenCalled(); expect(context.ui.requestRender).not.toHaveBeenCalled();
expect(context.showError).toHaveBeenCalledExactlyOnceWith(
"Failed to paste from clipboard: Native clipboard operation failed",
);
}); });

View file

@ -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<string[] | null>>(),
readClipboardImage: vi.fn<() => Promise<{ bytes: Uint8Array; mimeType: string } | null>>(),
readClipboardText: vi.fn<() => Promise<string | null>>(),
}));
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<void>;
};
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<void>;
};
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<void>;
};
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<void>;
};
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<void>;
};
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();
});

View file

@ -21,6 +21,7 @@ import { initTheme } from "../src/modes/interactive/theme/theme.ts";
const clipboardMocks = vi.hoisted(() => ({ const clipboardMocks = vi.hoisted(() => ({
copyToClipboard: vi.fn<(text: string) => Promise<void>>(), copyToClipboard: vi.fn<(text: string) => Promise<void>>(),
readClipboardFilePaths: vi.fn<() => Promise<string[] | null>>(),
readClipboardText: vi.fn<() => Promise<string | null>>(), readClipboardText: vi.fn<() => Promise<string | null>>(),
})); }));

View file

@ -7,8 +7,17 @@
#include <string.h> #include <string.h>
#endif #endif
typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE } clipboard_operation; typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE, CLIPBOARD_FILES } clipboard_operation;
typedef enum { CLIPBOARD_UNAVAILABLE, CLIPBOARD_EMPTY, CLIPBOARD_UTF8, CLIPBOARD_LATIN1, CLIPBOARD_UTF16, CLIPBOARD_BUFFER } clipboard_format; // 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 { typedef struct {
napi_async_work work; napi_async_work work;
@ -63,6 +72,33 @@ static void PI_NAPI_CALL execute_clipboard_work(napi_env env, void* data) {
clipboard_execute(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) { static void PI_NAPI_CALL complete_clipboard_work(napi_env env, int status, void* data) {
clipboard_job* job = data; clipboard_job* job = data;
napi_create_string_utf8_fn create_string = (napi_create_string_utf8_fn)node_symbol("napi_create_string_utf8"); 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->error && job->operation != CLIPBOARD_WRITE) {
if (job->format == CLIPBOARD_EMPTY) { if (job->format == CLIPBOARD_EMPTY) {
result = null_value(env); 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) { } else if (job->format == CLIPBOARD_BUFFER) {
napi_create_buffer_copy_fn create_buffer = (napi_create_buffer_copy_fn)node_symbol("napi_create_buffer_copy"); 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); status = create_buffer(env, job->length, job->data, 0, &result);

View file

@ -1,6 +1,6 @@
# Darwin native prebuilds # 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 ## Building

View file

@ -66,6 +66,40 @@ static void clipboard_execute(clipboard_job* job) {
} else { } else {
clipboard_copy(job, text.UTF8String, [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding], CLIPBOARD_UTF8); 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 { } else {
if (![pasteboard availableTypeFromArray:@[ NSPasteboardTypePNG, NSPasteboardTypeTIFF ]]) { if (![pasteboard availableTypeFromArray:@[ NSPasteboardTypePNG, NSPasteboardTypeTIFF ]]) {
job->format = CLIPBOARD_EMPTY; 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) { 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, "isModifierPressed", is_modifier_pressed);
set_function_export(env, exports, "getText", get_clipboard_text); set_function_export(env, exports, "getText", get_clipboard_text);
set_function_export(env, exports, "setText", set_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, "getImage", get_clipboard_image);
set_function_export(env, exports, "getFilePaths", get_clipboard_file_paths);
return exports; return exports;
} }

View file

@ -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_execute_callback)(napi_env, void*);
typedef void (PI_NAPI_CALL *napi_async_complete_callback)(napi_env, int, 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_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_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_create_promise_fn)(napi_env, napi_deferred*, napi_value*);
typedef int (PI_NAPI_CALL *napi_settle_deferred_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_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_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_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*); typedef int (PI_NAPI_CALL *napi_throw_error_fn)(napi_env, const char*, const char*);
static void* node_symbol(const char* name) { static void* node_symbol(const char* name) {

View file

@ -11,6 +11,8 @@ export interface NativeClipboard {
getText(): Promise<string | null | undefined>; getText(): Promise<string | null | undefined>;
/** Undefined means unavailable, null means no image; transfer failures reject. */ /** Undefined means unavailable, null means no image; transfer failures reject. */
getImage(): Promise<Uint8Array | null | undefined>; getImage(): Promise<Uint8Array | null | undefined>;
/** POSIX paths of file URLs on the clipboard. Undefined means unsupported, null means no files. */
getFilePaths?(): Promise<string[] | null | undefined>;
/** Linux uses command-line tools to retain clipboard ownership instead. */ /** Linux uses command-line tools to retain clipboard ownership instead. */
setText?(text: string): Promise<void>; setText?(text: string): Promise<void>;
} }