pi-image-tools 1.3.0 → 1.4.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 +135 -115
- package/README.md +16 -8
- package/package.json +13 -4
- package/src/clipboard.ts +99 -89
- package/src/config.ts +5 -2
- package/src/debug-logger.ts +4 -2
- package/src/image-preview.ts +25 -32
- package/src/image-size.ts +62 -63
- package/src/image-transcode.ts +206 -0
- package/src/index.ts +23 -27
- package/src/inline-user-preview.ts +512 -523
- package/src/keybindings.ts +52 -50
- package/src/powershell.ts +82 -56
- package/src/preview-logging.ts +28 -0
- package/src/providers/command-runner.ts +86 -68
- package/src/providers/mac-osascript-pngf.ts +17 -56
- package/src/providers/mac-osascript-publicpng.ts +16 -55
- package/src/providers/mac-pngpaste.ts +16 -55
- package/src/providers/native-module.ts +84 -84
- package/src/providers/powershell-forms.ts +87 -95
- package/src/providers/provider-helpers.ts +196 -0
- package/src/providers/registry.ts +88 -88
- package/src/providers/types.ts +24 -24
- package/src/providers/wl-paste.ts +21 -56
- package/src/providers/xclip.ts +21 -57
- package/src/recent-images.ts +55 -83
- package/src/shell-environment.ts +75 -75
package/src/recent-images.ts
CHANGED
|
@@ -37,6 +37,17 @@ interface RecentImageSource {
|
|
|
37
37
|
filterScreenshotNames: boolean;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function source(
|
|
41
|
+
homeDirectory: string,
|
|
42
|
+
segments: readonly string[],
|
|
43
|
+
filterScreenshotNames: boolean,
|
|
44
|
+
): RecentImageSource {
|
|
45
|
+
return {
|
|
46
|
+
path: join(homeDirectory, ...segments),
|
|
47
|
+
filterScreenshotNames,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
export interface RecentImageCandidate {
|
|
41
52
|
path: string;
|
|
42
53
|
name: string;
|
|
@@ -106,64 +117,27 @@ function parseConfiguredSources(environment: NodeJS.ProcessEnv, homeDirectory: s
|
|
|
106
117
|
.filter((value) => value.length > 0);
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
filterScreenshotNames: false,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
path: join(homeDirectory, "Pictures"),
|
|
134
|
-
filterScreenshotNames: true,
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
path: join(homeDirectory, "Downloads"),
|
|
138
|
-
filterScreenshotNames: true,
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
path: join(homeDirectory, "Desktop"),
|
|
142
|
-
filterScreenshotNames: true,
|
|
143
|
-
},
|
|
144
|
-
];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function getDefaultMacSources(homeDirectory: string): RecentImageSource[] {
|
|
148
|
-
return [
|
|
149
|
-
{
|
|
150
|
-
path: join(homeDirectory, "Desktop"),
|
|
151
|
-
filterScreenshotNames: true,
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
path: join(homeDirectory, "Downloads"),
|
|
155
|
-
filterScreenshotNames: true,
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
path: join(homeDirectory, "Pictures", "Screenshots"),
|
|
159
|
-
filterScreenshotNames: false,
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
path: join(homeDirectory, "Pictures"),
|
|
163
|
-
filterScreenshotNames: true,
|
|
164
|
-
},
|
|
165
|
-
];
|
|
166
|
-
}
|
|
120
|
+
type DefaultSourceSpec = readonly [segments: readonly string[], filterScreenshotNames: boolean];
|
|
121
|
+
|
|
122
|
+
const DEFAULT_PLATFORM_SOURCES: Readonly<Partial<Record<NodeJS.Platform, readonly DefaultSourceSpec[]>>> = {
|
|
123
|
+
win32: [
|
|
124
|
+
[["Pictures", "Screenshots"], false],
|
|
125
|
+
[["OneDrive", "Pictures", "Screenshots"], false],
|
|
126
|
+
[["Desktop"], true],
|
|
127
|
+
],
|
|
128
|
+
linux: [
|
|
129
|
+
[["Pictures", "Screenshots"], false],
|
|
130
|
+
[["Pictures"], true],
|
|
131
|
+
[["Downloads"], true],
|
|
132
|
+
[["Desktop"], true],
|
|
133
|
+
],
|
|
134
|
+
darwin: [
|
|
135
|
+
[["Desktop"], true],
|
|
136
|
+
[["Downloads"], true],
|
|
137
|
+
[["Pictures", "Screenshots"], false],
|
|
138
|
+
[["Pictures"], true],
|
|
139
|
+
],
|
|
140
|
+
};
|
|
167
141
|
|
|
168
142
|
function dedupeSources(
|
|
169
143
|
sources: readonly RecentImageSource[],
|
|
@@ -219,24 +193,22 @@ function listRecentImagesFromSource(source: RecentImageSource): RecentImageCandi
|
|
|
219
193
|
|
|
220
194
|
const fullPath = join(source.path, name);
|
|
221
195
|
|
|
222
|
-
let stat;
|
|
223
196
|
try {
|
|
224
|
-
stat = statSync(fullPath);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
197
|
+
const stat = statSync(fullPath);
|
|
198
|
+
if (!stat.isFile()) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
228
201
|
|
|
229
|
-
|
|
202
|
+
candidates.push({
|
|
203
|
+
path: fullPath,
|
|
204
|
+
name,
|
|
205
|
+
mimeType,
|
|
206
|
+
modifiedAtMs: stat.mtimeMs,
|
|
207
|
+
sizeBytes: stat.size,
|
|
208
|
+
});
|
|
209
|
+
} catch {
|
|
230
210
|
continue;
|
|
231
211
|
}
|
|
232
|
-
|
|
233
|
-
candidates.push({
|
|
234
|
-
path: fullPath,
|
|
235
|
-
name,
|
|
236
|
-
mimeType,
|
|
237
|
-
modifiedAtMs: stat.mtimeMs,
|
|
238
|
-
sizeBytes: stat.size,
|
|
239
|
-
});
|
|
240
212
|
}
|
|
241
213
|
|
|
242
214
|
return candidates;
|
|
@@ -246,16 +218,14 @@ function getPlatformDefaultSources(
|
|
|
246
218
|
platform: NodeJS.Platform,
|
|
247
219
|
homeDirectory: string,
|
|
248
220
|
): RecentImageSource[] {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
case "linux":
|
|
253
|
-
return getDefaultLinuxSources(homeDirectory);
|
|
254
|
-
case "darwin":
|
|
255
|
-
return getDefaultMacSources(homeDirectory);
|
|
256
|
-
default:
|
|
257
|
-
return [];
|
|
221
|
+
const specs = DEFAULT_PLATFORM_SOURCES[platform];
|
|
222
|
+
if (!specs) {
|
|
223
|
+
return [];
|
|
258
224
|
}
|
|
225
|
+
|
|
226
|
+
return specs.map(([segments, filterScreenshotNames]) =>
|
|
227
|
+
source(homeDirectory, segments, filterScreenshotNames),
|
|
228
|
+
);
|
|
259
229
|
}
|
|
260
230
|
|
|
261
231
|
function buildSources(options: DiscoverRecentImagesOptions): RecentImageSource[] {
|
|
@@ -368,8 +338,9 @@ function pruneCacheDirectory(cacheDirectory: string, maxCacheFiles: number): voi
|
|
|
368
338
|
for (const staleFile of imageFiles.slice(maxCacheFiles)) {
|
|
369
339
|
try {
|
|
370
340
|
unlinkSync(staleFile.fullPath);
|
|
371
|
-
} catch {
|
|
372
|
-
//
|
|
341
|
+
} catch (error) {
|
|
342
|
+
// Cache cleanup is best-effort; a stale file is retried on the next prune.
|
|
343
|
+
void error;
|
|
373
344
|
}
|
|
374
345
|
}
|
|
375
346
|
}
|
|
@@ -462,6 +433,7 @@ export function loadRecentImage(
|
|
|
462
433
|
): ClipboardImage {
|
|
463
434
|
assertImageWithinByteLimit(candidate.sizeBytes, `Recent image ${candidate.name}`, environment);
|
|
464
435
|
const raw = readFileSync(candidate.path);
|
|
436
|
+
assertImageWithinByteLimit(raw.length, `Recent image ${candidate.name}`, environment);
|
|
465
437
|
if (raw.length === 0) {
|
|
466
438
|
throw new Error(`File is empty: ${candidate.path}`);
|
|
467
439
|
}
|
package/src/shell-environment.ts
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
export interface ShellCommandContext {
|
|
4
|
-
platform: NodeJS.Platform;
|
|
5
|
-
environment: NodeJS.ProcessEnv;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type CommandExists = (command: string, context: ShellCommandContext) => boolean;
|
|
9
|
-
|
|
10
|
-
export interface WrappedCommand {
|
|
11
|
-
command: string;
|
|
12
|
-
args: string[];
|
|
13
|
-
wrapped: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const COMMAND_EXISTS_TIMEOUT_MS = 1000;
|
|
17
|
-
const COMMAND_EXISTS_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
18
|
-
const commandExistsCache = new Map<string, boolean>();
|
|
19
|
-
|
|
20
|
-
export function hasGraphicalSession(platform: NodeJS.Platform, environment: NodeJS.ProcessEnv): boolean {
|
|
21
|
-
return platform !== "linux" || Boolean(environment.DISPLAY || environment.WAYLAND_DISPLAY);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function isWaylandSession(environment: NodeJS.ProcessEnv): boolean {
|
|
25
|
-
return Boolean(environment.WAYLAND_DISPLAY) || environment.XDG_SESSION_TYPE === "wayland";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isTmuxSession(environment: NodeJS.ProcessEnv): boolean {
|
|
29
|
-
return Boolean(environment.TMUX);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const defaultCommandExists: CommandExists = (command, context) => {
|
|
33
|
-
const lookupCommand = context.platform === "win32" ? "where" : "which";
|
|
34
|
-
const cacheKey = `${context.platform}:${lookupCommand}:${command}:${context.environment.PATH ?? ""}`;
|
|
35
|
-
const cached = commandExistsCache.get(cacheKey);
|
|
36
|
-
if (cached !== undefined) {
|
|
37
|
-
return cached;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const result = spawnSync(lookupCommand, [command], {
|
|
41
|
-
env: context.environment,
|
|
42
|
-
maxBuffer: COMMAND_EXISTS_MAX_BUFFER_BYTES,
|
|
43
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
44
|
-
timeout: COMMAND_EXISTS_TIMEOUT_MS,
|
|
45
|
-
windowsHide: true,
|
|
46
|
-
});
|
|
47
|
-
const exists = !result.error && result.status === 0;
|
|
48
|
-
commandExistsCache.set(cacheKey, exists);
|
|
49
|
-
return exists;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export function buildNamespaceWrappedCommand(
|
|
53
|
-
command: string,
|
|
54
|
-
args: readonly string[],
|
|
55
|
-
context: ShellCommandContext,
|
|
56
|
-
commandExists: CommandExists = defaultCommandExists,
|
|
57
|
-
): WrappedCommand {
|
|
58
|
-
try {
|
|
59
|
-
if (
|
|
60
|
-
context.platform !== "darwin" ||
|
|
61
|
-
!isTmuxSession(context.environment) ||
|
|
62
|
-
!commandExists("reattach-to-user-namespace", context)
|
|
63
|
-
) {
|
|
64
|
-
return { command, args: [...args], wrapped: false };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
command: "reattach-to-user-namespace",
|
|
69
|
-
args: [command, ...args],
|
|
70
|
-
wrapped: true,
|
|
71
|
-
};
|
|
72
|
-
} catch {
|
|
73
|
-
return { command, args: [...args], wrapped: false };
|
|
74
|
-
}
|
|
75
|
-
}
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface ShellCommandContext {
|
|
4
|
+
platform: NodeJS.Platform;
|
|
5
|
+
environment: NodeJS.ProcessEnv;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type CommandExists = (command: string, context: ShellCommandContext) => boolean;
|
|
9
|
+
|
|
10
|
+
export interface WrappedCommand {
|
|
11
|
+
command: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
wrapped: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const COMMAND_EXISTS_TIMEOUT_MS = 1000;
|
|
17
|
+
const COMMAND_EXISTS_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
18
|
+
const commandExistsCache = new Map<string, boolean>();
|
|
19
|
+
|
|
20
|
+
export function hasGraphicalSession(platform: NodeJS.Platform, environment: NodeJS.ProcessEnv): boolean {
|
|
21
|
+
return platform !== "linux" || Boolean(environment.DISPLAY || environment.WAYLAND_DISPLAY);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isWaylandSession(environment: NodeJS.ProcessEnv): boolean {
|
|
25
|
+
return Boolean(environment.WAYLAND_DISPLAY) || environment.XDG_SESSION_TYPE === "wayland";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isTmuxSession(environment: NodeJS.ProcessEnv): boolean {
|
|
29
|
+
return Boolean(environment.TMUX);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const defaultCommandExists: CommandExists = (command, context) => {
|
|
33
|
+
const lookupCommand = context.platform === "win32" ? "where" : "which";
|
|
34
|
+
const cacheKey = `${context.platform}:${lookupCommand}:${command}:${context.environment.PATH ?? ""}`;
|
|
35
|
+
const cached = commandExistsCache.get(cacheKey);
|
|
36
|
+
if (cached !== undefined) {
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = spawnSync(lookupCommand, [command], {
|
|
41
|
+
env: context.environment,
|
|
42
|
+
maxBuffer: COMMAND_EXISTS_MAX_BUFFER_BYTES,
|
|
43
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
44
|
+
timeout: COMMAND_EXISTS_TIMEOUT_MS,
|
|
45
|
+
windowsHide: true,
|
|
46
|
+
});
|
|
47
|
+
const exists = !result.error && result.status === 0;
|
|
48
|
+
commandExistsCache.set(cacheKey, exists);
|
|
49
|
+
return exists;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function buildNamespaceWrappedCommand(
|
|
53
|
+
command: string,
|
|
54
|
+
args: readonly string[],
|
|
55
|
+
context: ShellCommandContext,
|
|
56
|
+
commandExists: CommandExists = defaultCommandExists,
|
|
57
|
+
): WrappedCommand {
|
|
58
|
+
try {
|
|
59
|
+
if (
|
|
60
|
+
context.platform !== "darwin" ||
|
|
61
|
+
!isTmuxSession(context.environment) ||
|
|
62
|
+
!commandExists("reattach-to-user-namespace", context)
|
|
63
|
+
) {
|
|
64
|
+
return { command, args: [...args], wrapped: false };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
command: "reattach-to-user-namespace",
|
|
69
|
+
args: [command, ...args],
|
|
70
|
+
wrapped: true,
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
return { command, args: [...args], wrapped: false };
|
|
74
|
+
}
|
|
75
|
+
}
|