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/keybindings.ts
CHANGED
|
@@ -72,49 +72,51 @@ const LEGACY_KEYBINDING_NAME_MIGRATIONS: Record<string, string> = {
|
|
|
72
72
|
deleteSessionNoninvasive: "app.session.deleteNoninvasive",
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
75
|
+
function getAppKeybindingDefaults(platform: NodeJS.Platform): KeybindingDefaults {
|
|
76
|
+
return {
|
|
77
|
+
"app.interrupt": "escape",
|
|
78
|
+
"app.clear": "ctrl+c",
|
|
79
|
+
"app.exit": "ctrl+d",
|
|
80
|
+
"app.suspend": platform === "win32" ? [] : "ctrl+z",
|
|
81
|
+
"app.thinking.cycle": "shift+tab",
|
|
82
|
+
"app.model.cycleForward": "ctrl+p",
|
|
83
|
+
"app.model.cycleBackward": "shift+ctrl+p",
|
|
84
|
+
"app.model.select": "ctrl+l",
|
|
85
|
+
"app.tools.expand": "ctrl+o",
|
|
86
|
+
"app.thinking.toggle": "ctrl+t",
|
|
87
|
+
"app.session.toggleNamedFilter": "ctrl+n",
|
|
88
|
+
"app.editor.external": "ctrl+g",
|
|
89
|
+
"app.message.followUp": "alt+enter",
|
|
90
|
+
"app.message.dequeue": "alt+up",
|
|
91
|
+
"app.clipboard.pasteImage": platform === "win32" ? "alt+v" : "ctrl+v",
|
|
92
|
+
"app.session.new": [],
|
|
93
|
+
"app.session.tree": [],
|
|
94
|
+
"app.session.fork": [],
|
|
95
|
+
"app.session.resume": [],
|
|
96
|
+
"app.tree.foldOrUp": ["ctrl+left", "alt+left"],
|
|
97
|
+
"app.tree.unfoldOrDown": ["ctrl+right", "alt+right"],
|
|
98
|
+
"app.tree.editLabel": "shift+l",
|
|
99
|
+
"app.tree.toggleLabelTimestamp": "shift+t",
|
|
100
|
+
"app.session.togglePath": "ctrl+p",
|
|
101
|
+
"app.session.toggleSort": "ctrl+s",
|
|
102
|
+
"app.session.rename": "ctrl+r",
|
|
103
|
+
"app.session.delete": "ctrl+d",
|
|
104
|
+
"app.session.deleteNoninvasive": "ctrl+backspace",
|
|
105
|
+
"app.models.save": "ctrl+s",
|
|
106
|
+
"app.models.enableAll": "ctrl+a",
|
|
107
|
+
"app.models.clearAll": "ctrl+x",
|
|
108
|
+
"app.models.toggleProvider": "ctrl+p",
|
|
109
|
+
"app.models.reorderUp": "alt+up",
|
|
110
|
+
"app.models.reorderDown": "alt+down",
|
|
111
|
+
"app.tree.filter.default": "ctrl+d",
|
|
112
|
+
"app.tree.filter.noTools": "ctrl+t",
|
|
113
|
+
"app.tree.filter.userOnly": "ctrl+u",
|
|
114
|
+
"app.tree.filter.labeledOnly": "ctrl+l",
|
|
115
|
+
"app.tree.filter.all": "ctrl+a",
|
|
116
|
+
"app.tree.filter.cycleForward": "ctrl+o",
|
|
117
|
+
"app.tree.filter.cycleBackward": "shift+ctrl+o",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
118
120
|
|
|
119
121
|
export interface RegisterImagePasteKeybindingsOptions {
|
|
120
122
|
config: ImageToolsConfig;
|
|
@@ -186,7 +188,7 @@ function normalizeUserKeybindings(rawConfig: Record<string, unknown> | undefined
|
|
|
186
188
|
return userKeybindings;
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
function getBuiltinKeybindingDefaults(): KeybindingDefaults {
|
|
191
|
+
function getBuiltinKeybindingDefaults(platform: NodeJS.Platform): KeybindingDefaults {
|
|
190
192
|
const tuiDefaults: KeybindingDefaults = {};
|
|
191
193
|
for (const [keybinding, definition] of Object.entries(TUI_KEYBINDINGS)) {
|
|
192
194
|
tuiDefaults[keybinding] = definition.defaultKeys;
|
|
@@ -194,12 +196,12 @@ function getBuiltinKeybindingDefaults(): KeybindingDefaults {
|
|
|
194
196
|
|
|
195
197
|
return {
|
|
196
198
|
...tuiDefaults,
|
|
197
|
-
...
|
|
199
|
+
...getAppKeybindingDefaults(platform),
|
|
198
200
|
};
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
function getConfiguredBuiltinShortcuts(): Set<string> {
|
|
202
|
-
const defaults = getBuiltinKeybindingDefaults();
|
|
203
|
+
function getConfiguredBuiltinShortcuts(platform: NodeJS.Platform): Set<string> {
|
|
204
|
+
const defaults = getBuiltinKeybindingDefaults(platform);
|
|
203
205
|
const userKeybindings = normalizeUserKeybindings(readKeybindingsConfig());
|
|
204
206
|
const builtinShortcuts = new Set<string>();
|
|
205
207
|
|
|
@@ -224,8 +226,8 @@ function getImagePasteShortcutCandidates(platform: NodeJS.Platform): KeyId[] {
|
|
|
224
226
|
return ["ctrl+v", "alt+v", "ctrl+alt+v"];
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
function removeBuiltinConflicts(shortcuts: readonly KeyId[]): KeyId[] {
|
|
228
|
-
const builtinShortcuts = getConfiguredBuiltinShortcuts();
|
|
229
|
+
function removeBuiltinConflicts(shortcuts: readonly KeyId[], platform: NodeJS.Platform): KeyId[] {
|
|
230
|
+
const builtinShortcuts = getConfiguredBuiltinShortcuts(platform);
|
|
229
231
|
|
|
230
232
|
return shortcuts.filter((shortcut) => !builtinShortcuts.has(normalizeShortcutKey(shortcut)));
|
|
231
233
|
}
|
|
@@ -238,7 +240,7 @@ export function getImagePasteShortcuts(
|
|
|
238
240
|
config.shortcuts.avoidBuiltinConflicts || config.shortcuts.suppressBuiltinConflictWarnings;
|
|
239
241
|
const candidates = config.shortcuts.pasteImage ?? getImagePasteShortcutCandidates(platform);
|
|
240
242
|
|
|
241
|
-
return shouldAvoidBuiltinConflicts ? removeBuiltinConflicts(candidates) : [...candidates];
|
|
243
|
+
return shouldAvoidBuiltinConflicts ? removeBuiltinConflicts(candidates, platform) : [...candidates];
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
export function registerImagePasteKeybindings(
|
package/src/powershell.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface RunPowerShellCommandOptions {
|
|
|
31
31
|
timeout: number;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
const ALLOWED_BUFFERED_COMMANDS = new Set(["img2sixel", "powershell", "powershell.exe"]);
|
|
35
|
+
|
|
34
36
|
function encodePowerShell(script: string): string {
|
|
35
37
|
return Buffer.from(script, "utf16le").toString("base64");
|
|
36
38
|
}
|
|
@@ -41,9 +43,20 @@ export function runBufferedCommand(
|
|
|
41
43
|
options: RunBufferedCommandOptions,
|
|
42
44
|
): Promise<BufferedCommandResult> {
|
|
43
45
|
return new Promise((resolve) => {
|
|
46
|
+
const normalizedCommand = command.trim().toLowerCase();
|
|
47
|
+
if (!ALLOWED_BUFFERED_COMMANDS.has(normalizedCommand)) {
|
|
48
|
+
resolve({
|
|
49
|
+
status: null,
|
|
50
|
+
stdout: Buffer.alloc(0),
|
|
51
|
+
stderr: Buffer.from(`Command is not allowlisted for pi-image-tools buffered execution: ${command}`),
|
|
52
|
+
error: new Error(`Command is not allowlisted for pi-image-tools buffered execution: ${command}`),
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
let child: ReturnType<typeof spawn>;
|
|
45
58
|
try {
|
|
46
|
-
child = spawn(command, [...args], {
|
|
59
|
+
child = spawn(command, [...args], { // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- command is checked against ALLOWED_BUFFERED_COMMANDS immediately above; args are fixed by Sixel/Image preview code and shell is disabled.
|
|
47
60
|
windowsHide: options.windowsHide,
|
|
48
61
|
});
|
|
49
62
|
} catch (error) {
|
|
@@ -124,27 +137,68 @@ export function runBufferedCommand(
|
|
|
124
137
|
});
|
|
125
138
|
}
|
|
126
139
|
|
|
127
|
-
|
|
140
|
+
const POWERSHELL_NON_WINDOWS_REASON = "PowerShell is only available through pi-image-tools on Windows.";
|
|
141
|
+
|
|
142
|
+
function nonWindowsPowerShellResult(): PowerShellCommandResult {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
stdout: "",
|
|
146
|
+
stderr: "",
|
|
147
|
+
missingCommand: false,
|
|
148
|
+
reason: POWERSHELL_NON_WINDOWS_REASON,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildPowerShellCommandArgs(
|
|
128
153
|
script: string,
|
|
129
154
|
options: RunPowerShellCommandOptions,
|
|
130
|
-
):
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
ok: false,
|
|
134
|
-
stdout: "",
|
|
135
|
-
stderr: "",
|
|
136
|
-
missingCommand: false,
|
|
137
|
-
reason: "PowerShell is only available through pi-image-tools on Windows.",
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const commandArgs = [
|
|
155
|
+
): string[] {
|
|
156
|
+
return [
|
|
142
157
|
"-NoProfile",
|
|
143
158
|
"-NonInteractive",
|
|
144
159
|
...(options.sta ? ["-STA"] : []),
|
|
145
160
|
...(options.encoded ? ["-EncodedCommand", encodePowerShell(script)] : ["-Command", script]),
|
|
146
161
|
...(options.args ?? []),
|
|
147
162
|
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildPowerShellResultFromError(
|
|
166
|
+
error: Error,
|
|
167
|
+
stdout: string,
|
|
168
|
+
stderr: string,
|
|
169
|
+
): PowerShellCommandResult {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
stdout,
|
|
173
|
+
stderr,
|
|
174
|
+
missingCommand: isErrnoException(error) && error.code === "ENOENT",
|
|
175
|
+
reason: getErrorMessage(error),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildPowerShellResultFromStatus(
|
|
180
|
+
status: number | null,
|
|
181
|
+
stdout: string,
|
|
182
|
+
stderr: string,
|
|
183
|
+
): PowerShellCommandResult {
|
|
184
|
+
return {
|
|
185
|
+
ok: status === 0,
|
|
186
|
+
stdout,
|
|
187
|
+
stderr,
|
|
188
|
+
missingCommand: false,
|
|
189
|
+
reason: status === 0 ? undefined : `PowerShell exited with code ${status}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function runPowerShellCommand(
|
|
194
|
+
script: string,
|
|
195
|
+
options: RunPowerShellCommandOptions,
|
|
196
|
+
): PowerShellCommandResult {
|
|
197
|
+
if (process.platform !== "win32") {
|
|
198
|
+
return nonWindowsPowerShellResult();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const commandArgs = buildPowerShellCommandArgs(script, options);
|
|
148
202
|
|
|
149
203
|
const result = spawnSync("powershell.exe", commandArgs, {
|
|
150
204
|
encoding: "utf8",
|
|
@@ -154,22 +208,18 @@ export function runPowerShellCommand(
|
|
|
154
208
|
});
|
|
155
209
|
|
|
156
210
|
if (result.error) {
|
|
157
|
-
return
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
reason: getErrorMessage(result.error),
|
|
163
|
-
};
|
|
211
|
+
return buildPowerShellResultFromError(
|
|
212
|
+
result.error,
|
|
213
|
+
result.stdout ?? "",
|
|
214
|
+
result.stderr ?? "",
|
|
215
|
+
);
|
|
164
216
|
}
|
|
165
217
|
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
reason: result.status === 0 ? undefined : `PowerShell exited with code ${result.status}`,
|
|
172
|
-
};
|
|
218
|
+
return buildPowerShellResultFromStatus(
|
|
219
|
+
result.status,
|
|
220
|
+
result.stdout ?? "",
|
|
221
|
+
result.stderr ?? "",
|
|
222
|
+
);
|
|
173
223
|
}
|
|
174
224
|
|
|
175
225
|
export async function runPowerShellCommandAsync(
|
|
@@ -177,22 +227,10 @@ export async function runPowerShellCommandAsync(
|
|
|
177
227
|
options: RunPowerShellCommandOptions,
|
|
178
228
|
): Promise<PowerShellCommandResult> {
|
|
179
229
|
if (process.platform !== "win32") {
|
|
180
|
-
return
|
|
181
|
-
ok: false,
|
|
182
|
-
stdout: "",
|
|
183
|
-
stderr: "",
|
|
184
|
-
missingCommand: false,
|
|
185
|
-
reason: "PowerShell is only available through pi-image-tools on Windows.",
|
|
186
|
-
};
|
|
230
|
+
return nonWindowsPowerShellResult();
|
|
187
231
|
}
|
|
188
232
|
|
|
189
|
-
const commandArgs =
|
|
190
|
-
"-NoProfile",
|
|
191
|
-
"-NonInteractive",
|
|
192
|
-
...(options.sta ? ["-STA"] : []),
|
|
193
|
-
...(options.encoded ? ["-EncodedCommand", encodePowerShell(script)] : ["-Command", script]),
|
|
194
|
-
...(options.args ?? []),
|
|
195
|
-
];
|
|
233
|
+
const commandArgs = buildPowerShellCommandArgs(script, options);
|
|
196
234
|
|
|
197
235
|
const result = await runBufferedCommand("powershell.exe", commandArgs, {
|
|
198
236
|
timeout: options.timeout,
|
|
@@ -203,20 +241,8 @@ export async function runPowerShellCommandAsync(
|
|
|
203
241
|
const stderr = result.stderr.toString("utf8");
|
|
204
242
|
|
|
205
243
|
if (result.error) {
|
|
206
|
-
return
|
|
207
|
-
ok: false,
|
|
208
|
-
stdout,
|
|
209
|
-
stderr,
|
|
210
|
-
missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
|
|
211
|
-
reason: getErrorMessage(result.error),
|
|
212
|
-
};
|
|
244
|
+
return buildPowerShellResultFromError(result.error, stdout, stderr);
|
|
213
245
|
}
|
|
214
246
|
|
|
215
|
-
return
|
|
216
|
-
ok: result.status === 0,
|
|
217
|
-
stdout,
|
|
218
|
-
stderr,
|
|
219
|
-
missingCommand: false,
|
|
220
|
-
reason: result.status === 0 ? undefined : `PowerShell exited with code ${result.status}`,
|
|
221
|
-
};
|
|
247
|
+
return buildPowerShellResultFromStatus(result.status, stdout, stderr);
|
|
222
248
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DebugLogger } from "./debug-logger.js";
|
|
2
|
+
import { getErrorMessage } from "./errors.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Best-effort debug logging for Pi preview code.
|
|
6
|
+
*
|
|
7
|
+
* `DebugLogger.log` already swallows its own failures, and `getErrorMessage`
|
|
8
|
+
* never throws, so callers need no try/catch guard. Consolidating the call
|
|
9
|
+
* here keeps both preview renderers consistent and avoids duplicated guards.
|
|
10
|
+
*/
|
|
11
|
+
export function logPreviewEvent(
|
|
12
|
+
logger: DebugLogger | undefined,
|
|
13
|
+
event: string,
|
|
14
|
+
fields: Record<string, unknown> = {},
|
|
15
|
+
): void {
|
|
16
|
+
logger?.log(event, fields);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Best-effort debug logging for a caught error inside a Pi event handler.
|
|
21
|
+
*/
|
|
22
|
+
export function logPreviewHandlerError(
|
|
23
|
+
logger: DebugLogger | undefined,
|
|
24
|
+
event: string,
|
|
25
|
+
error: unknown,
|
|
26
|
+
): void {
|
|
27
|
+
logPreviewEvent(logger, event, { error: getErrorMessage(error) });
|
|
28
|
+
}
|
|
@@ -1,68 +1,86 @@
|
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export interface
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
return {
|
|
53
|
-
ok: false,
|
|
54
|
-
stdout:
|
|
55
|
-
stderr:
|
|
56
|
-
missingCommand:
|
|
57
|
-
status:
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
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
|
+
const ALLOWED_CLIPBOARD_COMMANDS = new Set([
|
|
10
|
+
"osascript",
|
|
11
|
+
"pngpaste",
|
|
12
|
+
"reattach-to-user-namespace",
|
|
13
|
+
"wl-paste",
|
|
14
|
+
"xclip",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export interface CommandResult {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
stdout: Buffer;
|
|
20
|
+
stderr: Buffer;
|
|
21
|
+
missingCommand: boolean;
|
|
22
|
+
status: number | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CommandRunOptions {
|
|
26
|
+
environment?: NodeJS.ProcessEnv;
|
|
27
|
+
maxBuffer?: number;
|
|
28
|
+
timeout: number;
|
|
29
|
+
windowsHide?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CommandRunner = (
|
|
33
|
+
command: string,
|
|
34
|
+
args: readonly string[],
|
|
35
|
+
options: CommandRunOptions,
|
|
36
|
+
) => CommandResult;
|
|
37
|
+
|
|
38
|
+
function toBuffer(value: unknown): Buffer {
|
|
39
|
+
if (Buffer.isBuffer(value)) {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (value instanceof Uint8Array) {
|
|
44
|
+
return Buffer.from(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Buffer.from(value === undefined || value === null ? "" : String(value), "utf8");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const defaultCommandRunner: CommandRunner = (command, args, options) => {
|
|
51
|
+
if (!ALLOWED_CLIPBOARD_COMMANDS.has(command.trim().toLowerCase())) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
stdout: Buffer.alloc(0),
|
|
55
|
+
stderr: Buffer.from(`Command is not allowlisted for clipboard image providers: ${command}`),
|
|
56
|
+
missingCommand: false,
|
|
57
|
+
status: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = spawnSync(command, [...args], { // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- command is checked against ALLOWED_CLIPBOARD_COMMANDS immediately above; provider args are fixed arrays and shell is disabled.
|
|
62
|
+
env: options.environment,
|
|
63
|
+
maxBuffer: options.maxBuffer ?? MAX_BUFFER_BYTES,
|
|
64
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
65
|
+
timeout: options.timeout,
|
|
66
|
+
windowsHide: options.windowsHide,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (result.error) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
stdout: toBuffer(result.stdout),
|
|
73
|
+
stderr: toBuffer(result.stderr),
|
|
74
|
+
missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
|
|
75
|
+
status: result.status ?? null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ok: result.status === 0,
|
|
81
|
+
stdout: toBuffer(result.stdout),
|
|
82
|
+
stderr: toBuffer(result.stderr),
|
|
83
|
+
missingCommand: false,
|
|
84
|
+
status: result.status ?? null,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
1
|
+
import { NamespacedCommandProvider, providerEmptyImage, providerImageResult, type ClipboardReadResult, type CommandExists, type CommandResult, type CommandRunner } from "./provider-helpers.js";
|
|
9
2
|
|
|
10
3
|
const PNGF_SCRIPT = `try
|
|
11
4
|
set imageData to the clipboard as «class PNGf»
|
|
@@ -40,62 +33,30 @@ function parseAppleScriptPngfData(stdout: Buffer): Uint8Array | null {
|
|
|
40
33
|
return bytes.length > 0 ? new Uint8Array(bytes) : null;
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
export class OsascriptPngfProvider
|
|
44
|
-
readonly capabilities;
|
|
45
|
-
private readonly commandRunner: CommandRunner;
|
|
46
|
-
private readonly commandExists: CommandExists;
|
|
47
|
-
|
|
36
|
+
export class OsascriptPngfProvider extends NamespacedCommandProvider {
|
|
48
37
|
constructor(options: OsascriptPngfProviderOptions = {}) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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(
|
|
38
|
+
super(
|
|
39
|
+
{
|
|
40
|
+
id: "mac-osascript-pngf",
|
|
41
|
+
name: "osascript PNGf",
|
|
42
|
+
platforms: ["darwin"],
|
|
43
|
+
priority: options.priority ?? 30,
|
|
44
|
+
},
|
|
69
45
|
"osascript",
|
|
70
|
-
|
|
71
|
-
context,
|
|
72
|
-
this.commandExists,
|
|
46
|
+
options,
|
|
73
47
|
);
|
|
74
|
-
|
|
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
|
-
}
|
|
48
|
+
}
|
|
83
49
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
50
|
+
protected buildArgs(): readonly string[] {
|
|
51
|
+
return ["-e", PNGF_SCRIPT];
|
|
52
|
+
}
|
|
87
53
|
|
|
54
|
+
protected readFromResult(result: CommandResult): ClipboardReadResult {
|
|
88
55
|
const bytes = parseAppleScriptPngfData(result.stdout);
|
|
89
56
|
if (!bytes) {
|
|
90
|
-
return
|
|
57
|
+
return providerEmptyImage();
|
|
91
58
|
}
|
|
92
59
|
|
|
93
|
-
return
|
|
94
|
-
available: true,
|
|
95
|
-
image: {
|
|
96
|
-
bytes,
|
|
97
|
-
mimeType: "image/png",
|
|
98
|
-
},
|
|
99
|
-
};
|
|
60
|
+
return providerImageResult(bytes, "image/png");
|
|
100
61
|
}
|
|
101
62
|
}
|