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/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 line.startsWith(SIXEL_IMAGE_LINE_MARKER) || line.includes(SIXEL_IMAGE_LINE_MARKER);
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 {
@@ -3,10 +3,16 @@ import type { KeyId } from "@mariozechner/pi-tui";
3
3
 
4
4
  import type { PasteImageHandler } from "./types.js";
5
5
 
6
- const IMAGE_PASTE_SHORTCUTS: KeyId[] = ["alt+v", "ctrl+alt+v"];
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 IMAGE_PASTE_SHORTCUTS) {
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,
@@ -130,11 +130,56 @@ function getDefaultWindowsSources(homeDirectory: string): RecentImageSource[] {
130
130
  ];
131
131
  }
132
132
 
133
- function dedupeSources(sources: readonly RecentImageSource[]): RecentImageSource[] {
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 = process.platform === "win32" ? source.path.toLowerCase() : source.path;
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
- cacheSource,
241
- ...configuredPaths.map((pathValue) => ({
242
- path: pathValue,
243
- filterScreenshotNames: false,
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, ...getDefaultWindowsSources(homeDirectory)]);
308
+ return dedupeSources([cacheSource, ...getPlatformDefaultSources(platform, homeDirectory)], platform);
249
309
  }
250
310
 
251
- function dedupeCandidates(candidates: readonly RecentImageCandidate[]): RecentImageCandidate[] {
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 = process.platform === "win32" ? candidate.path.toLowerCase() : candidate.path;
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((left, right) => right.modifiedAtMs - left.modifiedAtMs);
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)}...\\${fileName}`;
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
+ }