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.
@@ -72,49 +72,51 @@ const LEGACY_KEYBINDING_NAME_MIGRATIONS: Record<string, string> = {
72
72
  deleteSessionNoninvasive: "app.session.deleteNoninvasive",
73
73
  };
74
74
 
75
- const APP_KEYBINDING_DEFAULTS: KeybindingDefaults = {
76
- "app.interrupt": "escape",
77
- "app.clear": "ctrl+c",
78
- "app.exit": "ctrl+d",
79
- "app.suspend": process.platform === "win32" ? [] : "ctrl+z",
80
- "app.thinking.cycle": "shift+tab",
81
- "app.model.cycleForward": "ctrl+p",
82
- "app.model.cycleBackward": "shift+ctrl+p",
83
- "app.model.select": "ctrl+l",
84
- "app.tools.expand": "ctrl+o",
85
- "app.thinking.toggle": "ctrl+t",
86
- "app.session.toggleNamedFilter": "ctrl+n",
87
- "app.editor.external": "ctrl+g",
88
- "app.message.followUp": "alt+enter",
89
- "app.message.dequeue": "alt+up",
90
- "app.clipboard.pasteImage": process.platform === "win32" ? "alt+v" : "ctrl+v",
91
- "app.session.new": [],
92
- "app.session.tree": [],
93
- "app.session.fork": [],
94
- "app.session.resume": [],
95
- "app.tree.foldOrUp": ["ctrl+left", "alt+left"],
96
- "app.tree.unfoldOrDown": ["ctrl+right", "alt+right"],
97
- "app.tree.editLabel": "shift+l",
98
- "app.tree.toggleLabelTimestamp": "shift+t",
99
- "app.session.togglePath": "ctrl+p",
100
- "app.session.toggleSort": "ctrl+s",
101
- "app.session.rename": "ctrl+r",
102
- "app.session.delete": "ctrl+d",
103
- "app.session.deleteNoninvasive": "ctrl+backspace",
104
- "app.models.save": "ctrl+s",
105
- "app.models.enableAll": "ctrl+a",
106
- "app.models.clearAll": "ctrl+x",
107
- "app.models.toggleProvider": "ctrl+p",
108
- "app.models.reorderUp": "alt+up",
109
- "app.models.reorderDown": "alt+down",
110
- "app.tree.filter.default": "ctrl+d",
111
- "app.tree.filter.noTools": "ctrl+t",
112
- "app.tree.filter.userOnly": "ctrl+u",
113
- "app.tree.filter.labeledOnly": "ctrl+l",
114
- "app.tree.filter.all": "ctrl+a",
115
- "app.tree.filter.cycleForward": "ctrl+o",
116
- "app.tree.filter.cycleBackward": "shift+ctrl+o",
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
- ...APP_KEYBINDING_DEFAULTS,
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
- export function runPowerShellCommand(
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
- ): PowerShellCommandResult {
131
- if (process.platform !== "win32") {
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
- ok: false,
159
- stdout: result.stdout ?? "",
160
- stderr: result.stderr ?? "",
161
- missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
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
- ok: result.status === 0,
168
- stdout: result.stdout ?? "",
169
- stderr: result.stderr ?? "",
170
- missingCommand: false,
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
- 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
- };
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 { 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";
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 implements ClipboardImageProvider {
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
- 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(
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
- ["-e", PNGF_SCRIPT],
71
- context,
72
- this.commandExists,
46
+ options,
73
47
  );
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
- }
48
+ }
83
49
 
84
- if (!result.ok || result.stdout.length === 0) {
85
- return { available: true, image: null };
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 { available: true, image: null };
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
  }