pi-image-tools 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +4 -4
- package/package.json +69 -69
- package/src/clipboard.ts +89 -336
- package/src/config.ts +24 -15
- package/src/index.ts +115 -32
- package/src/inline-user-preview.ts +225 -69
- package/src/keybindings.ts +10 -11
- package/src/providers/command-runner.ts +68 -0
- package/src/providers/mac-osascript-pngf.ts +101 -0
- package/src/providers/mac-osascript-publicpng.ts +77 -0
- package/src/providers/mac-pngpaste.ts +69 -0
- package/src/providers/native-module.ts +84 -0
- package/src/providers/powershell-forms.ts +95 -0
- package/src/providers/registry.ts +88 -0
- package/src/providers/types.ts +24 -0
- package/src/providers/wl-paste.ts +81 -0
- package/src/providers/xclip.ts +87 -0
- package/src/shell-environment.ts +75 -0
|
@@ -12,17 +12,11 @@ import { buildPreviewItems, type ImagePayload, type ImagePreviewItem } from "./i
|
|
|
12
12
|
import { buildSixelRenderLines, isInlineImageProtocolLine } from "./sixel-protocol.js";
|
|
13
13
|
import { setActiveTerminalImageSettingsCwd } from "./terminal-image-width.js";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type UserMessagePrototype = {
|
|
18
|
-
render: UserMessageRenderFn;
|
|
19
|
-
__piImageToolsInlineOriginalRender?: UserMessageRenderFn;
|
|
20
|
-
__piImageToolsInlinePatched?: boolean;
|
|
21
|
-
};
|
|
15
|
+
const INLINE_PREVIEW_PATCH_VERSION = "pi-image-tools-inline-preview-chat-component-v4";
|
|
16
|
+
const PREPARE_SUBMITTED_PREVIEW_TIMEOUT_MS = 2_500;
|
|
22
17
|
|
|
23
18
|
type UserMessageInstance = {
|
|
24
|
-
|
|
25
|
-
__piImageToolsInlineItems?: ImagePreviewItem[];
|
|
19
|
+
render?: (width: number) => string[];
|
|
26
20
|
};
|
|
27
21
|
|
|
28
22
|
type InteractiveModePrototype = {
|
|
@@ -31,6 +25,7 @@ type InteractiveModePrototype = {
|
|
|
31
25
|
__piImageToolsOriginalAddMessageToChat?: (message: unknown, options?: unknown) => void;
|
|
32
26
|
__piImageToolsOriginalGetUserMessageText?: (message: unknown) => string;
|
|
33
27
|
__piImageToolsPreviewPatched?: boolean;
|
|
28
|
+
__piImageToolsPreviewPatchVersion?: string;
|
|
34
29
|
};
|
|
35
30
|
|
|
36
31
|
interface UserImageContent {
|
|
@@ -46,8 +41,22 @@ interface UserMessageLike {
|
|
|
46
41
|
|
|
47
42
|
interface InteractiveModeLike {
|
|
48
43
|
chatContainer?: {
|
|
44
|
+
addChild?: (component: unknown) => void;
|
|
49
45
|
children?: unknown[];
|
|
50
46
|
};
|
|
47
|
+
ui?: {
|
|
48
|
+
requestRender?: () => void;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ImagePreviewChatComponent {
|
|
53
|
+
constructor(private readonly items: readonly ImagePreviewItem[]) {}
|
|
54
|
+
|
|
55
|
+
invalidate(): void {}
|
|
56
|
+
|
|
57
|
+
render(width: number): string[] {
|
|
58
|
+
return renderPreviewLines(this.items, width);
|
|
59
|
+
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
function buildNativeLines(item: ImagePreviewItem, width: number): string[] {
|
|
@@ -137,18 +146,13 @@ function toImageContent(value: unknown): UserImageContent | null {
|
|
|
137
146
|
};
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
if (userMessage.role !== "user") {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!Array.isArray(userMessage.content)) {
|
|
149
|
+
function extractImagePayloadsFromContent(content: unknown): ImagePayload[] {
|
|
150
|
+
if (!Array.isArray(content)) {
|
|
147
151
|
return [];
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
const payloads: ImagePayload[] = [];
|
|
151
|
-
for (const part of
|
|
155
|
+
for (const part of content) {
|
|
152
156
|
const image = toImageContent(part);
|
|
153
157
|
if (!image) {
|
|
154
158
|
continue;
|
|
@@ -164,76 +168,171 @@ function extractImagePayloads(message: unknown): ImagePayload[] {
|
|
|
164
168
|
return payloads;
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
function extractImagePayloads(message: unknown): ImagePayload[] {
|
|
172
|
+
const userMessage = toUserMessage(message);
|
|
173
|
+
if (userMessage.role !== "user") {
|
|
174
|
+
return [];
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
return
|
|
177
|
+
return extractImagePayloadsFromContent(userMessage.content);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getImagePayloadSignature(images: readonly ImagePayload[]): string {
|
|
181
|
+
return images
|
|
182
|
+
.map((image) => `${image.mimeType}:${image.data.length}:${image.data.slice(0, 32)}:${image.data.slice(-32)}`)
|
|
183
|
+
.join("|");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface PreparedPreviewItems {
|
|
187
|
+
signature: string;
|
|
188
|
+
items: ImagePreviewItem[];
|
|
173
189
|
}
|
|
174
190
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
const preparedPreviewItemsQueue: PreparedPreviewItems[] = [];
|
|
192
|
+
const inFlightPreviewItems = new Map<string, Promise<ImagePreviewItem[]>>();
|
|
193
|
+
|
|
194
|
+
function queuePreparedPreviewItems(images: readonly ImagePayload[], items: ImagePreviewItem[]): void {
|
|
195
|
+
if (images.length === 0 || items.length === 0) {
|
|
178
196
|
return;
|
|
179
197
|
}
|
|
180
198
|
|
|
181
|
-
|
|
182
|
-
|
|
199
|
+
preparedPreviewItemsQueue.push({
|
|
200
|
+
signature: getImagePayloadSignature(images),
|
|
201
|
+
items,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (preparedPreviewItemsQueue.length > 8) {
|
|
205
|
+
preparedPreviewItemsQueue.splice(0, preparedPreviewItemsQueue.length - 8);
|
|
183
206
|
}
|
|
207
|
+
}
|
|
184
208
|
|
|
185
|
-
|
|
186
|
-
|
|
209
|
+
function consumePreparedPreviewItems(images: readonly ImagePayload[]): ImagePreviewItem[] | undefined {
|
|
210
|
+
if (images.length === 0 || preparedPreviewItemsQueue.length === 0) {
|
|
211
|
+
return undefined;
|
|
187
212
|
}
|
|
188
213
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
214
|
+
const signature = getImagePayloadSignature(images);
|
|
215
|
+
const index = preparedPreviewItemsQueue.findIndex((entry) => entry.signature === signature);
|
|
216
|
+
if (index === -1) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!Array.isArray(instance.__piImageToolsInlineItems)) {
|
|
199
|
-
instance.__piImageToolsInlineItems = [];
|
|
200
|
-
}
|
|
201
|
-
}
|
|
220
|
+
const [entry] = preparedPreviewItemsQueue.splice(index, 1);
|
|
221
|
+
return entry?.items;
|
|
222
|
+
}
|
|
202
223
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
function getInFlightPreviewItems(images: readonly ImagePayload[]): Promise<ImagePreviewItem[]> | undefined {
|
|
225
|
+
if (images.length === 0) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return inFlightPreviewItems.get(getImagePayloadSignature(images));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildPreviewItemsOnce(
|
|
233
|
+
images: readonly ImagePayload[],
|
|
234
|
+
options: { cwd?: string; logger?: DebugLogger } = {},
|
|
235
|
+
): Promise<ImagePreviewItem[]> {
|
|
236
|
+
const signature = getImagePayloadSignature(images);
|
|
237
|
+
const existing = inFlightPreviewItems.get(signature);
|
|
238
|
+
if (existing) {
|
|
239
|
+
return existing;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const promise = buildPreviewItems(images, options);
|
|
243
|
+
promise.then(
|
|
244
|
+
() => inFlightPreviewItems.delete(signature),
|
|
245
|
+
() => inFlightPreviewItems.delete(signature),
|
|
246
|
+
);
|
|
247
|
+
promise.catch(() => {
|
|
248
|
+
// Consumers log contextual errors; this prevents unhandled rejections when prebuild times out.
|
|
249
|
+
});
|
|
250
|
+
inFlightPreviewItems.set(signature, promise);
|
|
251
|
+
return promise;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function waitForPreviewItemsDeadline(
|
|
255
|
+
previewItemsPromise: Promise<ImagePreviewItem[]>,
|
|
256
|
+
): Promise<ImagePreviewItem[] | undefined> {
|
|
257
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
return await Promise.race([
|
|
261
|
+
previewItemsPromise,
|
|
262
|
+
new Promise<undefined>((resolve) => {
|
|
263
|
+
timeout = setTimeout(() => resolve(undefined), PREPARE_SUBMITTED_PREVIEW_TIMEOUT_MS);
|
|
264
|
+
timeout.unref?.();
|
|
265
|
+
}),
|
|
266
|
+
]);
|
|
267
|
+
} finally {
|
|
268
|
+
if (timeout) {
|
|
269
|
+
clearTimeout(timeout);
|
|
207
270
|
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
208
273
|
|
|
209
|
-
|
|
210
|
-
|
|
274
|
+
function imagePlaceholderText(count: number): string {
|
|
275
|
+
if (count <= 1) {
|
|
276
|
+
return "[ 1 image attached]";
|
|
277
|
+
}
|
|
211
278
|
|
|
212
|
-
|
|
279
|
+
return `[ ${count} images attached]`;
|
|
213
280
|
}
|
|
214
281
|
|
|
215
|
-
function
|
|
282
|
+
function isUserMessageComponentLike(value: unknown): value is UserMessageInstance {
|
|
283
|
+
if (value instanceof UserMessageComponent) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!isRecord(value) || typeof value.render !== "function") {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const constructorName =
|
|
292
|
+
typeof value.constructor === "function" && typeof value.constructor.name === "string"
|
|
293
|
+
? value.constructor.name
|
|
294
|
+
: undefined;
|
|
295
|
+
|
|
296
|
+
return constructorName === "UserMessageComponent";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function requestInteractiveModeRender(mode: InteractiveModeLike): void {
|
|
300
|
+
try {
|
|
301
|
+
mode.ui?.requestRender?.();
|
|
302
|
+
} catch {
|
|
303
|
+
// Rendering will be retried by the next TUI update.
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function addPreviewItemsAfterLatestUserMessage(
|
|
216
308
|
mode: InteractiveModeLike,
|
|
217
309
|
fromChildIndex: number,
|
|
218
310
|
previewItems: ImagePreviewItem[],
|
|
219
|
-
):
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
311
|
+
): boolean {
|
|
312
|
+
const chatContainer = mode.chatContainer;
|
|
313
|
+
const children = chatContainer?.children;
|
|
314
|
+
if (!Array.isArray(children) || children.length === 0 || previewItems.length === 0) {
|
|
315
|
+
return false;
|
|
223
316
|
}
|
|
224
317
|
|
|
225
318
|
const start = Math.max(0, fromChildIndex);
|
|
226
319
|
for (let index = children.length - 1; index >= start; index -= 1) {
|
|
227
320
|
const child = children[index];
|
|
228
|
-
if (!(child
|
|
321
|
+
if (!isUserMessageComponentLike(child)) {
|
|
229
322
|
continue;
|
|
230
323
|
}
|
|
231
324
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
325
|
+
const previewComponent = new ImagePreviewChatComponent(previewItems);
|
|
326
|
+
const insertIndex = index + 1;
|
|
327
|
+
if (insertIndex >= children.length && typeof chatContainer?.addChild === "function") {
|
|
328
|
+
chatContainer.addChild(previewComponent);
|
|
329
|
+
} else {
|
|
330
|
+
children.splice(insertIndex, 0, previewComponent);
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
236
333
|
}
|
|
334
|
+
|
|
335
|
+
return false;
|
|
237
336
|
}
|
|
238
337
|
|
|
239
338
|
function logInlinePreviewError(
|
|
@@ -262,7 +361,10 @@ function patchInteractiveMode(logger?: DebugLogger): void {
|
|
|
262
361
|
prototype.__piImageToolsOriginalAddMessageToChat = prototype.addMessageToChat;
|
|
263
362
|
}
|
|
264
363
|
|
|
265
|
-
if (
|
|
364
|
+
if (
|
|
365
|
+
prototype.__piImageToolsPreviewPatched &&
|
|
366
|
+
prototype.__piImageToolsPreviewPatchVersion === INLINE_PREVIEW_PATCH_VERSION
|
|
367
|
+
) {
|
|
266
368
|
return;
|
|
267
369
|
}
|
|
268
370
|
|
|
@@ -299,13 +401,26 @@ function patchInteractiveMode(logger?: DebugLogger): void {
|
|
|
299
401
|
return;
|
|
300
402
|
}
|
|
301
403
|
|
|
302
|
-
|
|
404
|
+
const preparedPreviewItems = consumePreparedPreviewItems(imagePayloads);
|
|
405
|
+
if (preparedPreviewItems) {
|
|
406
|
+
if (addPreviewItemsAfterLatestUserMessage(mode, beforeCount, preparedPreviewItems)) {
|
|
407
|
+
requestInteractiveModeRender(mode);
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const previewItemsPromise = getInFlightPreviewItems(imagePayloads)
|
|
413
|
+
?? buildPreviewItemsOnce(imagePayloads, { logger });
|
|
414
|
+
|
|
415
|
+
void previewItemsPromise
|
|
303
416
|
.then((previewItems) => {
|
|
304
417
|
if (previewItems.length === 0) {
|
|
305
418
|
return;
|
|
306
419
|
}
|
|
307
420
|
|
|
308
|
-
|
|
421
|
+
if (addPreviewItemsAfterLatestUserMessage(mode, beforeCount, previewItems)) {
|
|
422
|
+
requestInteractiveModeRender(mode);
|
|
423
|
+
}
|
|
309
424
|
})
|
|
310
425
|
.catch((error: unknown) => {
|
|
311
426
|
logInlinePreviewError(logger, "inline-user-preview.build_preview_failed", error);
|
|
@@ -313,6 +428,7 @@ function patchInteractiveMode(logger?: DebugLogger): void {
|
|
|
313
428
|
};
|
|
314
429
|
|
|
315
430
|
prototype.__piImageToolsPreviewPatched = true;
|
|
431
|
+
prototype.__piImageToolsPreviewPatchVersion = INLINE_PREVIEW_PATCH_VERSION;
|
|
316
432
|
}
|
|
317
433
|
|
|
318
434
|
export interface RegisterInlineUserImagePreviewOptions {
|
|
@@ -323,15 +439,16 @@ export function registerInlineUserImagePreview(
|
|
|
323
439
|
pi: ExtensionAPI,
|
|
324
440
|
options: RegisterInlineUserImagePreviewOptions = {},
|
|
325
441
|
): void {
|
|
442
|
+
const applyPatch = (): void => {
|
|
443
|
+
try {
|
|
444
|
+
patchInteractiveMode(options.logger);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
logInlinePreviewError(options.logger, "inline-user-preview.patch_failed", error);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
326
450
|
const runPatch = (delayMs: number): void => {
|
|
327
|
-
setTimeout(
|
|
328
|
-
try {
|
|
329
|
-
patchInteractiveMode(options.logger);
|
|
330
|
-
patchUserMessageRender();
|
|
331
|
-
} catch (error) {
|
|
332
|
-
logInlinePreviewError(options.logger, "inline-user-preview.patch_failed", error);
|
|
333
|
-
}
|
|
334
|
-
}, delayMs);
|
|
451
|
+
setTimeout(applyPatch, delayMs);
|
|
335
452
|
};
|
|
336
453
|
|
|
337
454
|
const schedulePatch = (): void => {
|
|
@@ -342,18 +459,57 @@ export function registerInlineUserImagePreview(
|
|
|
342
459
|
const handleSessionEvent = (eventName: string, cwd: string | undefined): void => {
|
|
343
460
|
try {
|
|
344
461
|
setActiveTerminalImageSettingsCwd(cwd);
|
|
462
|
+
applyPatch();
|
|
345
463
|
schedulePatch();
|
|
346
464
|
} catch (error) {
|
|
347
465
|
logInlinePreviewError(options.logger, `inline-user-preview.${eventName}_failed`, error);
|
|
348
466
|
}
|
|
349
467
|
};
|
|
350
468
|
|
|
469
|
+
const prepareSubmittedImagePreview = async (
|
|
470
|
+
event: { images?: unknown },
|
|
471
|
+
cwd: string | undefined,
|
|
472
|
+
): Promise<{ imagePayloads: ImagePayload[]; previewItems: ImagePreviewItem[] } | undefined> => {
|
|
473
|
+
try {
|
|
474
|
+
setActiveTerminalImageSettingsCwd(cwd);
|
|
475
|
+
applyPatch();
|
|
476
|
+
const imagePayloads = extractImagePayloadsFromContent(event.images);
|
|
477
|
+
if (imagePayloads.length === 0) {
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const previewItemsPromise = buildPreviewItemsOnce(imagePayloads, { cwd, logger: options.logger });
|
|
482
|
+
const previewItems = await waitForPreviewItemsDeadline(previewItemsPromise);
|
|
483
|
+
if (!previewItems || previewItems.length === 0) {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return { imagePayloads, previewItems };
|
|
488
|
+
} catch (error) {
|
|
489
|
+
logInlinePreviewError(options.logger, "inline-user-preview.prepare_submitted_preview_failed", error);
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
applyPatch();
|
|
495
|
+
|
|
351
496
|
pi.on("session_start", async (_event, ctx) => {
|
|
352
497
|
handleSessionEvent("session_start", ctx.cwd);
|
|
353
498
|
});
|
|
354
499
|
|
|
355
|
-
pi.on("before_agent_start", async (
|
|
500
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
356
501
|
handleSessionEvent("before_agent_start", ctx.cwd);
|
|
502
|
+
if (!ctx.hasUI) {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const preparedPreview = await prepareSubmittedImagePreview(event, ctx.cwd);
|
|
507
|
+
if (!preparedPreview) {
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
queuePreparedPreviewItems(preparedPreview.imagePayloads, preparedPreview.previewItems);
|
|
512
|
+
return undefined;
|
|
357
513
|
});
|
|
358
514
|
|
|
359
515
|
const onSessionSwitch = pi.on as unknown as (
|
package/src/keybindings.ts
CHANGED
|
@@ -230,20 +230,15 @@ function removeBuiltinConflicts(shortcuts: readonly KeyId[]): KeyId[] {
|
|
|
230
230
|
return shortcuts.filter((shortcut) => !builtinShortcuts.has(normalizeShortcutKey(shortcut)));
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
function getImagePasteShortcuts(
|
|
233
|
+
export function getImagePasteShortcuts(
|
|
234
234
|
config: ImageToolsConfig,
|
|
235
235
|
platform: NodeJS.Platform = process.platform,
|
|
236
236
|
): KeyId[] {
|
|
237
237
|
const shouldAvoidBuiltinConflicts =
|
|
238
238
|
config.shortcuts.avoidBuiltinConflicts || config.shortcuts.suppressBuiltinConflictWarnings;
|
|
239
|
+
const candidates = config.shortcuts.pasteImage ?? getImagePasteShortcutCandidates(platform);
|
|
239
240
|
|
|
240
|
-
|
|
241
|
-
return shouldAvoidBuiltinConflicts
|
|
242
|
-
? removeBuiltinConflicts(config.shortcuts.pasteImage)
|
|
243
|
-
: config.shortcuts.pasteImage;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return removeBuiltinConflicts(getImagePasteShortcutCandidates(platform));
|
|
241
|
+
return shouldAvoidBuiltinConflicts ? removeBuiltinConflicts(candidates) : [...candidates];
|
|
247
242
|
}
|
|
248
243
|
|
|
249
244
|
export function registerImagePasteKeybindings(
|
|
@@ -251,16 +246,20 @@ export function registerImagePasteKeybindings(
|
|
|
251
246
|
handler: PasteImageHandler,
|
|
252
247
|
options: RegisterImagePasteKeybindingsOptions,
|
|
253
248
|
): void {
|
|
249
|
+
const platform = process.platform;
|
|
254
250
|
const configuredShortcuts = options.config.shortcuts.pasteImage;
|
|
255
|
-
const
|
|
256
|
-
const
|
|
251
|
+
const requestedShortcuts = configuredShortcuts ?? getImagePasteShortcutCandidates(platform);
|
|
252
|
+
const shortcuts = getImagePasteShortcuts(options.config, platform);
|
|
253
|
+
const skippedShortcuts = requestedShortcuts.filter(
|
|
257
254
|
(shortcut) => !shortcuts.some((registeredShortcut) => normalizeShortcutKey(registeredShortcut) === normalizeShortcutKey(shortcut)),
|
|
258
|
-
)
|
|
255
|
+
);
|
|
259
256
|
|
|
260
257
|
options.logger.log("keybindings.register", {
|
|
261
258
|
avoidBuiltinConflicts: options.config.shortcuts.avoidBuiltinConflicts,
|
|
262
259
|
suppressBuiltinConflictWarnings: options.config.shortcuts.suppressBuiltinConflictWarnings,
|
|
263
260
|
configured: configuredShortcuts !== undefined,
|
|
261
|
+
platform,
|
|
262
|
+
requestedShortcuts,
|
|
264
263
|
registeredShortcuts: shortcuts,
|
|
265
264
|
skippedShortcuts,
|
|
266
265
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { isErrnoException } from "../errors.js";
|
|
4
|
+
|
|
5
|
+
export const LIST_TYPES_TIMEOUT_MS = 1000;
|
|
6
|
+
export const READ_TIMEOUT_MS = 5000;
|
|
7
|
+
export const MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export interface CommandResult {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
stdout: Buffer;
|
|
12
|
+
stderr: Buffer;
|
|
13
|
+
missingCommand: boolean;
|
|
14
|
+
status: number | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CommandRunOptions {
|
|
18
|
+
environment?: NodeJS.ProcessEnv;
|
|
19
|
+
maxBuffer?: number;
|
|
20
|
+
timeout: number;
|
|
21
|
+
windowsHide?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type CommandRunner = (
|
|
25
|
+
command: string,
|
|
26
|
+
args: readonly string[],
|
|
27
|
+
options: CommandRunOptions,
|
|
28
|
+
) => CommandResult;
|
|
29
|
+
|
|
30
|
+
function toBuffer(value: unknown): Buffer {
|
|
31
|
+
if (Buffer.isBuffer(value)) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (value instanceof Uint8Array) {
|
|
36
|
+
return Buffer.from(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Buffer.from(value === undefined || value === null ? "" : String(value), "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const defaultCommandRunner: CommandRunner = (command, args, options) => {
|
|
43
|
+
const result = spawnSync(command, [...args], {
|
|
44
|
+
env: options.environment,
|
|
45
|
+
maxBuffer: options.maxBuffer ?? MAX_BUFFER_BYTES,
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
timeout: options.timeout,
|
|
48
|
+
windowsHide: options.windowsHide,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (result.error) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
stdout: toBuffer(result.stdout),
|
|
55
|
+
stderr: toBuffer(result.stderr),
|
|
56
|
+
missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
|
|
57
|
+
status: result.status ?? null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ok: result.status === 0,
|
|
63
|
+
stdout: toBuffer(result.stdout),
|
|
64
|
+
stderr: toBuffer(result.stderr),
|
|
65
|
+
missingCommand: false,
|
|
66
|
+
status: result.status ?? null,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { buildNamespaceWrappedCommand, defaultCommandExists, type CommandExists } from "../shell-environment.js";
|
|
2
|
+
import {
|
|
3
|
+
defaultCommandRunner,
|
|
4
|
+
MAX_BUFFER_BYTES,
|
|
5
|
+
READ_TIMEOUT_MS,
|
|
6
|
+
type CommandRunner,
|
|
7
|
+
} from "./command-runner.js";
|
|
8
|
+
import type { ClipboardImageProvider, ClipboardProviderContext, ClipboardReadResult } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const PNGF_SCRIPT = `try
|
|
11
|
+
set imageData to the clipboard as «class PNGf»
|
|
12
|
+
return imageData
|
|
13
|
+
on error
|
|
14
|
+
return ""
|
|
15
|
+
end try`;
|
|
16
|
+
|
|
17
|
+
export interface OsascriptPngfProviderOptions {
|
|
18
|
+
priority?: number;
|
|
19
|
+
commandRunner?: CommandRunner;
|
|
20
|
+
commandExists?: CommandExists;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseAppleScriptPngfData(stdout: Buffer): Uint8Array | null {
|
|
24
|
+
const text = stdout.toString("utf8").trim();
|
|
25
|
+
if (text.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const match = text.match(/«data\s+PNGf([0-9a-fA-F\s]+)»/i);
|
|
30
|
+
if (!match) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hex = match[1]?.replace(/\s+/g, "") ?? "";
|
|
35
|
+
if (hex.length === 0 || hex.length % 2 !== 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const bytes = Buffer.from(hex, "hex");
|
|
40
|
+
return bytes.length > 0 ? new Uint8Array(bytes) : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class OsascriptPngfProvider implements ClipboardImageProvider {
|
|
44
|
+
readonly capabilities;
|
|
45
|
+
private readonly commandRunner: CommandRunner;
|
|
46
|
+
private readonly commandExists: CommandExists;
|
|
47
|
+
|
|
48
|
+
constructor(options: OsascriptPngfProviderOptions = {}) {
|
|
49
|
+
this.capabilities = {
|
|
50
|
+
id: "mac-osascript-pngf",
|
|
51
|
+
name: "osascript PNGf",
|
|
52
|
+
platforms: ["darwin"],
|
|
53
|
+
priority: options.priority ?? 30,
|
|
54
|
+
};
|
|
55
|
+
this.commandRunner = options.commandRunner ?? defaultCommandRunner;
|
|
56
|
+
this.commandExists = options.commandExists ?? defaultCommandExists;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
isAvailable(context: ClipboardProviderContext): boolean {
|
|
60
|
+
try {
|
|
61
|
+
return this.commandExists("osascript", context);
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
read(context: ClipboardProviderContext): ClipboardReadResult {
|
|
68
|
+
const wrapped = buildNamespaceWrappedCommand(
|
|
69
|
+
"osascript",
|
|
70
|
+
["-e", PNGF_SCRIPT],
|
|
71
|
+
context,
|
|
72
|
+
this.commandExists,
|
|
73
|
+
);
|
|
74
|
+
const result = this.commandRunner(wrapped.command, wrapped.args, {
|
|
75
|
+
environment: context.environment,
|
|
76
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
77
|
+
timeout: READ_TIMEOUT_MS,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (result.missingCommand) {
|
|
81
|
+
return { available: false, image: null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!result.ok || result.stdout.length === 0) {
|
|
85
|
+
return { available: true, image: null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const bytes = parseAppleScriptPngfData(result.stdout);
|
|
89
|
+
if (!bytes) {
|
|
90
|
+
return { available: true, image: null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
available: true,
|
|
95
|
+
image: {
|
|
96
|
+
bytes,
|
|
97
|
+
mimeType: "image/png",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|