pi-image-tools 1.0.3 → 1.0.5
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 +37 -22
- package/LICENSE +21 -21
- package/README.md +244 -202
- package/config/config.example.json +3 -3
- package/index.ts +3 -3
- package/package.json +9 -1
- package/src/clipboard.ts +290 -22
- package/src/image-preview.ts +469 -469
- package/src/index.ts +0 -8
- package/src/inline-user-preview.ts +10 -1
- package/src/keybindings.ts +8 -2
- package/src/recent-images.ts +89 -18
- package/src/types.ts +20 -20
package/src/index.ts
CHANGED
|
@@ -33,10 +33,6 @@ function getErrorMessage(error: unknown): string {
|
|
|
33
33
|
return "Unknown error";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function isWindowsPlatform(platform: NodeJS.Platform = process.platform): boolean {
|
|
37
|
-
return platform === "win32";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
36
|
function imageToBase64(image: ClipboardImage): string {
|
|
41
37
|
return Buffer.from(image.bytes).toString("base64");
|
|
42
38
|
}
|
|
@@ -143,10 +139,6 @@ function showRecentSelectionPreview(
|
|
|
143
139
|
}
|
|
144
140
|
|
|
145
141
|
export default function imageToolsExtension(pi: ExtensionAPI): void {
|
|
146
|
-
if (!isWindowsPlatform()) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
142
|
const pendingImages: PendingImage[] = [];
|
|
151
143
|
|
|
152
144
|
registerInlineUserImagePreview(pi);
|
|
@@ -8,6 +8,8 @@ import { Image, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
|
8
8
|
import { buildPreviewItems, type ImagePayload, type ImagePreviewItem } from "./image-preview.js";
|
|
9
9
|
|
|
10
10
|
const SIXEL_IMAGE_LINE_MARKER = "\x1b_Gm=0;\x1b\\";
|
|
11
|
+
const KITTY_IMAGE_LINE_MARKER = "\x1b_G";
|
|
12
|
+
const ITERM_IMAGE_LINE_MARKER = "\x1b]1337;File=";
|
|
11
13
|
|
|
12
14
|
type UserMessageRenderFn = (width: number) => string[];
|
|
13
15
|
|
|
@@ -78,7 +80,14 @@ function buildNativeLines(item: ImagePreviewItem, width: number): string[] {
|
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
function isInlineImageLine(line: string): boolean {
|
|
81
|
-
return
|
|
83
|
+
return (
|
|
84
|
+
line.startsWith(SIXEL_IMAGE_LINE_MARKER) ||
|
|
85
|
+
line.includes(SIXEL_IMAGE_LINE_MARKER) ||
|
|
86
|
+
line.startsWith(KITTY_IMAGE_LINE_MARKER) ||
|
|
87
|
+
line.includes(KITTY_IMAGE_LINE_MARKER) ||
|
|
88
|
+
line.startsWith(ITERM_IMAGE_LINE_MARKER) ||
|
|
89
|
+
line.includes(ITERM_IMAGE_LINE_MARKER)
|
|
90
|
+
);
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
function fitLineToWidth(line: string, width: number): string {
|
package/src/keybindings.ts
CHANGED
|
@@ -3,10 +3,16 @@ import type { KeyId } from "@mariozechner/pi-tui";
|
|
|
3
3
|
|
|
4
4
|
import type { PasteImageHandler } from "./types.js";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function getImagePasteShortcuts(platform: NodeJS.Platform = process.platform): KeyId[] {
|
|
7
|
+
if (platform === "win32") {
|
|
8
|
+
return ["alt+v", "ctrl+alt+v"];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return ["ctrl+v", "alt+v", "ctrl+alt+v"];
|
|
12
|
+
}
|
|
7
13
|
|
|
8
14
|
export function registerImagePasteKeybindings(pi: ExtensionAPI, handler: PasteImageHandler): void {
|
|
9
|
-
for (const shortcut of
|
|
15
|
+
for (const shortcut of getImagePasteShortcuts()) {
|
|
10
16
|
pi.registerShortcut(shortcut, {
|
|
11
17
|
description: "Attach clipboard image to draft (send when ready)",
|
|
12
18
|
handler,
|
package/src/recent-images.ts
CHANGED
|
@@ -130,11 +130,56 @@ function getDefaultWindowsSources(homeDirectory: string): RecentImageSource[] {
|
|
|
130
130
|
];
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
function
|
|
133
|
+
function getDefaultLinuxSources(homeDirectory: string): RecentImageSource[] {
|
|
134
|
+
return [
|
|
135
|
+
{
|
|
136
|
+
path: join(homeDirectory, "Pictures", "Screenshots"),
|
|
137
|
+
filterScreenshotNames: false,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
path: join(homeDirectory, "Pictures"),
|
|
141
|
+
filterScreenshotNames: true,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
path: join(homeDirectory, "Downloads"),
|
|
145
|
+
filterScreenshotNames: true,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
path: join(homeDirectory, "Desktop"),
|
|
149
|
+
filterScreenshotNames: true,
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getDefaultMacSources(homeDirectory: string): RecentImageSource[] {
|
|
155
|
+
return [
|
|
156
|
+
{
|
|
157
|
+
path: join(homeDirectory, "Desktop"),
|
|
158
|
+
filterScreenshotNames: true,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
path: join(homeDirectory, "Downloads"),
|
|
162
|
+
filterScreenshotNames: true,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
path: join(homeDirectory, "Pictures", "Screenshots"),
|
|
166
|
+
filterScreenshotNames: false,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
path: join(homeDirectory, "Pictures"),
|
|
170
|
+
filterScreenshotNames: true,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function dedupeSources(
|
|
176
|
+
sources: readonly RecentImageSource[],
|
|
177
|
+
platform: NodeJS.Platform,
|
|
178
|
+
): RecentImageSource[] {
|
|
134
179
|
const deduped = new Map<string, RecentImageSource>();
|
|
135
180
|
|
|
136
181
|
for (const source of sources) {
|
|
137
|
-
const key =
|
|
182
|
+
const key = platform === "win32" ? source.path.toLowerCase() : source.path;
|
|
138
183
|
if (!deduped.has(key)) {
|
|
139
184
|
deduped.set(key, source);
|
|
140
185
|
}
|
|
@@ -220,15 +265,27 @@ function listRecentImagesFromSource(source: RecentImageSource): RecentImageCandi
|
|
|
220
265
|
return candidates;
|
|
221
266
|
}
|
|
222
267
|
|
|
268
|
+
function getPlatformDefaultSources(
|
|
269
|
+
platform: NodeJS.Platform,
|
|
270
|
+
homeDirectory: string,
|
|
271
|
+
): RecentImageSource[] {
|
|
272
|
+
switch (platform) {
|
|
273
|
+
case "win32":
|
|
274
|
+
return getDefaultWindowsSources(homeDirectory);
|
|
275
|
+
case "linux":
|
|
276
|
+
return getDefaultLinuxSources(homeDirectory);
|
|
277
|
+
case "darwin":
|
|
278
|
+
return getDefaultMacSources(homeDirectory);
|
|
279
|
+
default:
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
223
284
|
function buildSources(options: DiscoverRecentImagesOptions): RecentImageSource[] {
|
|
224
285
|
const platform = options.platform ?? process.platform;
|
|
225
286
|
const homeDirectory = options.homeDirectory ?? homedir();
|
|
226
287
|
const environment = options.environment ?? process.env;
|
|
227
288
|
|
|
228
|
-
if (platform !== "win32") {
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
231
|
-
|
|
232
289
|
const cacheSource: RecentImageSource = {
|
|
233
290
|
path: getRecentImageCacheDirectory(environment),
|
|
234
291
|
filterScreenshotNames: false,
|
|
@@ -236,23 +293,29 @@ function buildSources(options: DiscoverRecentImagesOptions): RecentImageSource[]
|
|
|
236
293
|
|
|
237
294
|
const configuredPaths = parseConfiguredSources(environment, homeDirectory);
|
|
238
295
|
if (configuredPaths.length > 0) {
|
|
239
|
-
return dedupeSources(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
296
|
+
return dedupeSources(
|
|
297
|
+
[
|
|
298
|
+
cacheSource,
|
|
299
|
+
...configuredPaths.map((pathValue) => ({
|
|
300
|
+
path: pathValue,
|
|
301
|
+
filterScreenshotNames: false,
|
|
302
|
+
})),
|
|
303
|
+
],
|
|
304
|
+
platform,
|
|
305
|
+
);
|
|
246
306
|
}
|
|
247
307
|
|
|
248
|
-
return dedupeSources([cacheSource, ...
|
|
308
|
+
return dedupeSources([cacheSource, ...getPlatformDefaultSources(platform, homeDirectory)], platform);
|
|
249
309
|
}
|
|
250
310
|
|
|
251
|
-
function dedupeCandidates(
|
|
311
|
+
function dedupeCandidates(
|
|
312
|
+
candidates: readonly RecentImageCandidate[],
|
|
313
|
+
platform: NodeJS.Platform,
|
|
314
|
+
): RecentImageCandidate[] {
|
|
252
315
|
const deduped = new Map<string, RecentImageCandidate>();
|
|
253
316
|
|
|
254
317
|
for (const candidate of candidates) {
|
|
255
|
-
const key =
|
|
318
|
+
const key = platform === "win32" ? candidate.path.toLowerCase() : candidate.path;
|
|
256
319
|
|
|
257
320
|
const existing = deduped.get(key);
|
|
258
321
|
if (!existing || candidate.modifiedAtMs > existing.modifiedAtMs) {
|
|
@@ -264,6 +327,7 @@ function dedupeCandidates(candidates: readonly RecentImageCandidate[]): RecentIm
|
|
|
264
327
|
}
|
|
265
328
|
|
|
266
329
|
export function discoverRecentImages(options: DiscoverRecentImagesOptions = {}): RecentImageDiscovery {
|
|
330
|
+
const platform = options.platform ?? process.platform;
|
|
267
331
|
const sources = buildSources(options);
|
|
268
332
|
const searchedDirectories = sources.map((source) => source.path);
|
|
269
333
|
const maxItems = options.maxItems ?? DEFAULT_MAX_RECENT_IMAGES;
|
|
@@ -276,7 +340,9 @@ export function discoverRecentImages(options: DiscoverRecentImagesOptions = {}):
|
|
|
276
340
|
}
|
|
277
341
|
|
|
278
342
|
const allCandidates = sources.flatMap((source) => listRecentImagesFromSource(source));
|
|
279
|
-
const sorted = dedupeCandidates(allCandidates).sort(
|
|
343
|
+
const sorted = dedupeCandidates(allCandidates, platform).sort(
|
|
344
|
+
(left, right) => right.modifiedAtMs - left.modifiedAtMs,
|
|
345
|
+
);
|
|
280
346
|
|
|
281
347
|
return {
|
|
282
348
|
candidates: sorted.slice(0, Math.max(1, maxItems)),
|
|
@@ -402,6 +468,10 @@ function formatSize(sizeBytes: number): string {
|
|
|
402
468
|
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`;
|
|
403
469
|
}
|
|
404
470
|
|
|
471
|
+
function detectPathSeparator(pathValue: string): string {
|
|
472
|
+
return pathValue.includes("\\") ? "\\" : "/";
|
|
473
|
+
}
|
|
474
|
+
|
|
405
475
|
function abbreviatePath(pathValue: string, maxChars: number): string {
|
|
406
476
|
if (pathValue.length <= maxChars) {
|
|
407
477
|
return pathValue;
|
|
@@ -412,8 +482,9 @@ function abbreviatePath(pathValue: string, maxChars: number): string {
|
|
|
412
482
|
return `...${fileName.slice(-(maxChars - 3))}`;
|
|
413
483
|
}
|
|
414
484
|
|
|
485
|
+
const separator = detectPathSeparator(pathValue);
|
|
415
486
|
const headLength = maxChars - fileName.length - 4;
|
|
416
|
-
return `${pathValue.slice(0, headLength)}
|
|
487
|
+
return `${pathValue.slice(0, headLength)}...${separator}${fileName}`;
|
|
417
488
|
}
|
|
418
489
|
|
|
419
490
|
export function formatRecentImageLabel(candidate: RecentImageCandidate, nowMs = Date.now()): string {
|
package/src/types.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
export type PasteContext = ExtensionContext | ExtensionCommandContext;
|
|
4
|
-
|
|
5
|
-
export interface ClipboardImage {
|
|
6
|
-
bytes: Uint8Array;
|
|
7
|
-
mimeType: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ClipboardModule {
|
|
11
|
-
hasImage: () => boolean;
|
|
12
|
-
getImageBinary: () => Promise<Array<number> | Uint8Array>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type PasteImageHandler = (ctx: PasteContext) => Promise<void>;
|
|
16
|
-
|
|
17
|
-
export interface PasteImageCommandHandlers {
|
|
18
|
-
fromClipboard: PasteImageHandler;
|
|
19
|
-
fromRecent: PasteImageHandler;
|
|
20
|
-
}
|
|
1
|
+
import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type PasteContext = ExtensionContext | ExtensionCommandContext;
|
|
4
|
+
|
|
5
|
+
export interface ClipboardImage {
|
|
6
|
+
bytes: Uint8Array;
|
|
7
|
+
mimeType: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ClipboardModule {
|
|
11
|
+
hasImage: () => boolean;
|
|
12
|
+
getImageBinary: () => Promise<Array<number> | Uint8Array>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PasteImageHandler = (ctx: PasteContext) => Promise<void>;
|
|
16
|
+
|
|
17
|
+
export interface PasteImageCommandHandlers {
|
|
18
|
+
fromClipboard: PasteImageHandler;
|
|
19
|
+
fromRecent: PasteImageHandler;
|
|
20
|
+
}
|