pi-image-tools 1.3.1 → 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.
@@ -1,5 +1,6 @@
1
1
  import { runPowerShellCommand, type PowerShellCommandResult, type RunPowerShellCommandOptions } from "../powershell.js";
2
2
  import { MAX_BUFFER_BYTES, READ_TIMEOUT_MS } from "./command-runner.js";
3
+ import { mapProviderReadFallback, providerEmptyImage, providerImageResult } from "./provider-helpers.js";
3
4
  import type { ClipboardImageProvider, ClipboardProviderContext, ClipboardReadResult } from "./types.js";
4
5
 
5
6
  export type PowerShellRunner = (
@@ -62,34 +63,25 @@ export class PowerShellFormsProvider implements ClipboardImageProvider {
62
63
  timeout: READ_TIMEOUT_MS,
63
64
  });
64
65
 
65
- if (result.missingCommand) {
66
- return { available: false, image: null };
67
- }
68
-
69
- if (!result.ok) {
70
- return { available: true, image: null };
66
+ const fallback = mapProviderReadFallback(result, false);
67
+ if (fallback) {
68
+ return fallback;
71
69
  }
72
70
 
73
71
  const base64 = result.stdout.trim();
74
72
  if (!base64) {
75
- return { available: true, image: null };
73
+ return providerEmptyImage();
76
74
  }
77
75
 
78
76
  try {
79
77
  const bytes = Buffer.from(base64, "base64");
80
78
  if (bytes.length === 0) {
81
- return { available: true, image: null };
79
+ return providerEmptyImage();
82
80
  }
83
81
 
84
- return {
85
- available: true,
86
- image: {
87
- bytes: new Uint8Array(bytes),
88
- mimeType: "image/png",
89
- },
90
- };
82
+ return providerImageResult(new Uint8Array(bytes), "image/png");
91
83
  } catch {
92
- return { available: true, image: null };
84
+ return providerEmptyImage();
93
85
  }
94
86
  }
95
87
  }
@@ -0,0 +1,196 @@
1
+ import { buildNamespaceWrappedCommand, defaultCommandExists, type CommandExists } from "../shell-environment.js";
2
+ import {
3
+ defaultCommandRunner,
4
+ MAX_BUFFER_BYTES,
5
+ READ_TIMEOUT_MS,
6
+ type CommandResult,
7
+ type CommandRunner,
8
+ } from "./command-runner.js";
9
+ import type {
10
+ ClipboardImageProvider,
11
+ ClipboardProviderContext,
12
+ ClipboardReadResult,
13
+ ProviderCapabilities,
14
+ } from "./types.js";
15
+
16
+ // Re-export the symbols command-based providers reference in their read
17
+ // overrides and option types. Routing every concrete provider's import
18
+ // through this one module — the shared `CommandRunnerProviderOptions`
19
+ // interface, the `LIST_TYPES_TIMEOUT_MS` constant, and the result types —
20
+ // keeps their import blocks short and avoids duplicated multi-line import
21
+ // boilerplate across providers.
22
+ export type { CommandExists } from "../shell-environment.js";
23
+ export type { CommandResult, CommandRunner } from "./command-runner.js";
24
+ export { LIST_TYPES_TIMEOUT_MS } from "./command-runner.js";
25
+ export type { ClipboardProviderContext, ClipboardReadResult } from "./types.js";
26
+
27
+ /**
28
+ * Minimal read-result shape shared by command-based and PowerShell-based
29
+ * clipboard providers. Both `CommandResult` (Buffer stdout) and
30
+ * `PowerShellCommandResult` (string stdout) satisfy this structural contract.
31
+ */
32
+ interface ProviderReadResult {
33
+ missingCommand: boolean;
34
+ ok: boolean;
35
+ stdout: { length: number };
36
+ }
37
+
38
+ /** Result returned when the provider's command is not installed. */
39
+ export function providerUnavailable(): { available: false; image: null } {
40
+ return { available: false, image: null };
41
+ }
42
+
43
+ /** Result returned when the clipboard is available but holds no image. */
44
+ export function providerEmptyImage(): { available: true; image: null } {
45
+ return { available: true, image: null };
46
+ }
47
+
48
+ /** Result returned when a provider successfully read an image. */
49
+ export function providerImageResult(bytes: Uint8Array, mimeType: string): ClipboardReadResult {
50
+ return { available: true, image: { bytes, mimeType } };
51
+ }
52
+
53
+ /**
54
+ * Shared `isAvailable` implementation for providers that delegate to an
55
+ * external command whose presence is verified via `commandExists`.
56
+ */
57
+ export function createCommandAvailabilityChecker(
58
+ commandName: string,
59
+ commandExists: CommandExists = defaultCommandExists,
60
+ ): (context: ClipboardProviderContext) => boolean {
61
+ return (context: ClipboardProviderContext): boolean => {
62
+ try {
63
+ return commandExists(commandName, context);
64
+ } catch {
65
+ return false;
66
+ }
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Maps a command read result to the standard unavailable/empty sentinel.
72
+ * Returns `null` when the caller should continue processing the result.
73
+ */
74
+ export function mapProviderReadFallback(
75
+ result: ProviderReadResult,
76
+ requireNonEmptyStdout = true,
77
+ ): { available: false; image: null } | { available: true; image: null } | null {
78
+ if (result.missingCommand) {
79
+ return providerUnavailable();
80
+ }
81
+
82
+ const isEmpty = !result.ok || (requireNonEmptyStdout && result.stdout.length === 0);
83
+ return isEmpty ? providerEmptyImage() : null;
84
+ }
85
+
86
+ /**
87
+ * Parses a command's stdout — a newline-separated list of MIME types as
88
+ * emitted by `wl-paste --list-types` / `xclip -t TARGETS -o` — into a
89
+ * trimmed, non-empty list. Shared by the Linux command-based providers so
90
+ * the parse chain is not duplicated per provider.
91
+ */
92
+ export function parseMimeTypeList(stdout: Buffer): string[] {
93
+ return stdout
94
+ .toString("utf8")
95
+ .split(/\r?\n/)
96
+ .map((mimeType) => mimeType.trim())
97
+ .filter((mimeType) => mimeType.length > 0);
98
+ }
99
+
100
+ /** Options shared by namespace-wrapped (macOS) command providers. */
101
+ export interface NamespacedCommandProviderOptions {
102
+ priority?: number;
103
+ commandRunner?: CommandRunner;
104
+ commandExists?: CommandExists;
105
+ }
106
+
107
+ /** Options shared by plain command-runner (Linux) providers. */
108
+ export interface CommandRunnerProviderOptions {
109
+ priority?: number;
110
+ commandRunner?: CommandRunner;
111
+ }
112
+
113
+ /**
114
+ * Base for macOS providers that read the clipboard via a namespace-wrapped
115
+ * command (osascript/pngpaste). Encapsulates the commandRunner/commandExists
116
+ * wiring, the command-availability check, and the shared read preamble
117
+ * (namespace wrap -> run -> fallback), so concrete providers only describe
118
+ * their command args and how to turn stdout into an image.
119
+ */
120
+ export abstract class NamespacedCommandProvider implements ClipboardImageProvider {
121
+ readonly capabilities: ProviderCapabilities;
122
+ private readonly commandRunner: CommandRunner;
123
+ private readonly commandExists: CommandExists;
124
+ private readonly isAvailableFn: (context: ClipboardProviderContext) => boolean;
125
+
126
+ constructor(
127
+ capabilities: ProviderCapabilities,
128
+ private readonly commandName: string,
129
+ options: NamespacedCommandProviderOptions = {},
130
+ ) {
131
+ this.capabilities = capabilities;
132
+ this.commandRunner = options.commandRunner ?? defaultCommandRunner;
133
+ this.commandExists = options.commandExists ?? defaultCommandExists;
134
+ this.isAvailableFn = createCommandAvailabilityChecker(this.commandName, this.commandExists);
135
+ }
136
+
137
+ isAvailable(context: ClipboardProviderContext): boolean {
138
+ return this.isAvailableFn(context);
139
+ }
140
+
141
+ read(context: ClipboardProviderContext): ClipboardReadResult {
142
+ const wrapped = buildNamespaceWrappedCommand(this.commandName, this.buildArgs(), context, this.commandExists);
143
+ const result = this.commandRunner(wrapped.command, wrapped.args, {
144
+ environment: context.environment,
145
+ maxBuffer: MAX_BUFFER_BYTES,
146
+ timeout: READ_TIMEOUT_MS,
147
+ });
148
+
149
+ const fallback = mapProviderReadFallback(result);
150
+ if (fallback) {
151
+ return fallback;
152
+ }
153
+
154
+ return this.readFromResult(result);
155
+ }
156
+
157
+ protected abstract buildArgs(): readonly string[];
158
+ protected abstract readFromResult(result: CommandResult): ClipboardReadResult;
159
+ }
160
+
161
+ /**
162
+ * Base for Linux providers that read the clipboard via a fixed command (no
163
+ * namespace wrapping). Provides the commandRunner field, an always-available
164
+ * `isAvailable`, and a shared `runCommand` helper so concrete providers only
165
+ * implement `read`.
166
+ */
167
+ export abstract class CommandRunnerProvider implements ClipboardImageProvider {
168
+ readonly capabilities: ProviderCapabilities;
169
+ protected readonly commandRunner: CommandRunner;
170
+
171
+ constructor(capabilities: ProviderCapabilities, options: CommandRunnerProviderOptions = {}) {
172
+ this.capabilities = capabilities;
173
+ this.commandRunner = options.commandRunner ?? defaultCommandRunner;
174
+ }
175
+
176
+ isAvailable(_context: ClipboardProviderContext): boolean {
177
+ return true;
178
+ }
179
+
180
+ protected runCommand(
181
+ command: string,
182
+ args: readonly string[],
183
+ context: ClipboardProviderContext,
184
+ timeout: number = READ_TIMEOUT_MS,
185
+ ): CommandResult {
186
+ return this.commandRunner(command, args, {
187
+ environment: context.environment,
188
+ maxBuffer: MAX_BUFFER_BYTES,
189
+ timeout,
190
+ });
191
+ }
192
+
193
+ abstract read(context: ClipboardProviderContext): ClipboardReadResult;
194
+ }
195
+
196
+ export type { ClipboardImageProvider };
@@ -1,81 +1,46 @@
1
- import { normalizeMimeType, selectPreferredImageMimeType } from "../image-mime.js";
2
- import {
3
- defaultCommandRunner,
4
- LIST_TYPES_TIMEOUT_MS,
5
- MAX_BUFFER_BYTES,
6
- READ_TIMEOUT_MS,
7
- type CommandRunner,
8
- } from "./command-runner.js";
9
- import type { ClipboardImageProvider, ClipboardProviderContext, ClipboardReadResult } from "./types.js";
10
-
11
- export interface WlPasteProviderOptions {
12
- priority?: number;
13
- commandRunner?: CommandRunner;
14
- }
15
-
16
- export class WlPasteProvider implements ClipboardImageProvider {
17
- readonly capabilities;
18
- private readonly commandRunner: CommandRunner;
19
-
20
- constructor(options: WlPasteProviderOptions = {}) {
21
- this.capabilities = {
22
- id: "wl-paste",
23
- name: "wl-paste",
24
- platforms: ["linux"],
25
- priority: options.priority ?? 10,
26
- };
27
- this.commandRunner = options.commandRunner ?? defaultCommandRunner;
28
- }
29
-
30
- isAvailable(_context: ClipboardProviderContext): boolean {
31
- return true;
32
- }
33
-
34
- read(context: ClipboardProviderContext): ClipboardReadResult {
35
- const listTypes = this.commandRunner("wl-paste", ["--list-types"], {
36
- environment: context.environment,
37
- maxBuffer: MAX_BUFFER_BYTES,
38
- timeout: LIST_TYPES_TIMEOUT_MS,
39
- });
40
- if (listTypes.missingCommand) {
41
- return { available: false, image: null };
42
- }
43
-
44
- if (!listTypes.ok) {
45
- return { available: true, image: null };
46
- }
47
-
48
- const mimeTypes = listTypes.stdout
49
- .toString("utf8")
50
- .split(/\r?\n/)
51
- .map((mimeType) => mimeType.trim())
52
- .filter((mimeType) => mimeType.length > 0);
53
-
54
- const selectedMimeType = selectPreferredImageMimeType(mimeTypes);
55
- if (!selectedMimeType) {
56
- return { available: true, image: null };
57
- }
58
-
59
- const imageData = this.commandRunner(
60
- "wl-paste",
61
- ["--type", selectedMimeType, "--no-newline"],
62
- {
63
- environment: context.environment,
64
- maxBuffer: MAX_BUFFER_BYTES,
65
- timeout: READ_TIMEOUT_MS,
66
- },
67
- );
68
-
69
- if (!imageData.ok || imageData.stdout.length === 0) {
70
- return { available: true, image: null };
71
- }
72
-
73
- return {
74
- available: true,
75
- image: {
76
- bytes: new Uint8Array(imageData.stdout),
77
- mimeType: normalizeMimeType(selectedMimeType),
78
- },
79
- };
80
- }
81
- }
1
+ import { CommandRunnerProvider, providerEmptyImage, providerImageResult, providerUnavailable, LIST_TYPES_TIMEOUT_MS, parseMimeTypeList, type CommandRunnerProviderOptions, type ClipboardProviderContext, type ClipboardReadResult } from "./provider-helpers.js";
2
+ import { normalizeMimeType, selectPreferredImageMimeType } from "../image-mime.js";
3
+
4
+ export class WlPasteProvider extends CommandRunnerProvider {
5
+ constructor(options: CommandRunnerProviderOptions = {}) {
6
+ super(
7
+ {
8
+ id: "wl-paste",
9
+ name: "wl-paste",
10
+ platforms: ["linux"],
11
+ priority: options.priority ?? 10,
12
+ },
13
+ options,
14
+ );
15
+ }
16
+
17
+ read(context: ClipboardProviderContext): ClipboardReadResult {
18
+ const listTypes = this.runCommand("wl-paste", ["--list-types"], context, LIST_TYPES_TIMEOUT_MS);
19
+ if (listTypes.missingCommand) {
20
+ return providerUnavailable();
21
+ }
22
+
23
+ if (!listTypes.ok) {
24
+ return providerEmptyImage();
25
+ }
26
+
27
+ const mimeTypes = parseMimeTypeList(listTypes.stdout);
28
+
29
+ const selectedMimeType = selectPreferredImageMimeType(mimeTypes);
30
+ if (!selectedMimeType) {
31
+ return providerEmptyImage();
32
+ }
33
+
34
+ const imageData = this.runCommand(
35
+ "wl-paste",
36
+ ["--type", selectedMimeType, "--no-newline"],
37
+ context,
38
+ );
39
+
40
+ if (!imageData.ok || imageData.stdout.length === 0) {
41
+ return providerEmptyImage();
42
+ }
43
+
44
+ return providerImageResult(new Uint8Array(imageData.stdout), normalizeMimeType(selectedMimeType));
45
+ }
46
+ }
@@ -1,87 +1,51 @@
1
- import { normalizeMimeType, selectPreferredImageMimeType, SUPPORTED_IMAGE_MIME_TYPES } from "../image-mime.js";
2
- import {
3
- defaultCommandRunner,
4
- LIST_TYPES_TIMEOUT_MS,
5
- MAX_BUFFER_BYTES,
6
- READ_TIMEOUT_MS,
7
- type CommandRunner,
8
- } from "./command-runner.js";
9
- import type { ClipboardImageProvider, ClipboardProviderContext, ClipboardReadResult } from "./types.js";
10
-
11
- export interface XclipProviderOptions {
12
- priority?: number;
13
- commandRunner?: CommandRunner;
14
- }
15
-
16
- export class XclipProvider implements ClipboardImageProvider {
17
- readonly capabilities;
18
- private readonly commandRunner: CommandRunner;
19
-
20
- constructor(options: XclipProviderOptions = {}) {
21
- this.capabilities = {
22
- id: "xclip",
23
- name: "xclip",
24
- platforms: ["linux"],
25
- priority: options.priority ?? 20,
26
- };
27
- this.commandRunner = options.commandRunner ?? defaultCommandRunner;
28
- }
29
-
30
- isAvailable(_context: ClipboardProviderContext): boolean {
31
- return true;
32
- }
33
-
34
- read(context: ClipboardProviderContext): ClipboardReadResult {
35
- const targets = this.commandRunner(
36
- "xclip",
37
- ["-selection", "clipboard", "-t", "TARGETS", "-o"],
38
- {
39
- environment: context.environment,
40
- maxBuffer: MAX_BUFFER_BYTES,
41
- timeout: LIST_TYPES_TIMEOUT_MS,
42
- },
43
- );
44
-
45
- if (targets.missingCommand) {
46
- return { available: false, image: null };
47
- }
48
-
49
- const advertisedMimeTypes = targets.ok
50
- ? targets.stdout
51
- .toString("utf8")
52
- .split(/\r?\n/)
53
- .map((mimeType) => mimeType.trim())
54
- .filter((mimeType) => mimeType.length > 0)
55
- : [];
56
-
57
- const preferredMimeType =
58
- advertisedMimeTypes.length > 0 ? selectPreferredImageMimeType(advertisedMimeTypes) : null;
59
- const mimeTypesToTry = preferredMimeType
60
- ? [preferredMimeType, ...SUPPORTED_IMAGE_MIME_TYPES]
61
- : [...SUPPORTED_IMAGE_MIME_TYPES];
62
-
63
- for (const mimeType of mimeTypesToTry) {
64
- const imageData = this.commandRunner(
65
- "xclip",
66
- ["-selection", "clipboard", "-t", mimeType, "-o"],
67
- {
68
- environment: context.environment,
69
- maxBuffer: MAX_BUFFER_BYTES,
70
- timeout: READ_TIMEOUT_MS,
71
- },
72
- );
73
-
74
- if (imageData.ok && imageData.stdout.length > 0) {
75
- return {
76
- available: true,
77
- image: {
78
- bytes: new Uint8Array(imageData.stdout),
79
- mimeType: normalizeMimeType(mimeType),
80
- },
81
- };
82
- }
83
- }
84
-
85
- return { available: true, image: null };
86
- }
87
- }
1
+ import { CommandRunnerProvider, providerEmptyImage, providerImageResult, providerUnavailable, LIST_TYPES_TIMEOUT_MS, parseMimeTypeList, type CommandRunnerProviderOptions, type ClipboardProviderContext, type ClipboardReadResult } from "./provider-helpers.js";
2
+ import { normalizeMimeType, selectPreferredImageMimeType, SUPPORTED_IMAGE_MIME_TYPES } from "../image-mime.js";
3
+
4
+ export class XclipProvider extends CommandRunnerProvider {
5
+ constructor(options: CommandRunnerProviderOptions = {}) {
6
+ super(
7
+ {
8
+ id: "xclip",
9
+ name: "xclip",
10
+ platforms: ["linux"],
11
+ priority: options.priority ?? 20,
12
+ },
13
+ options,
14
+ );
15
+ }
16
+
17
+ read(context: ClipboardProviderContext): ClipboardReadResult {
18
+ const targets = this.runCommand(
19
+ "xclip",
20
+ ["-selection", "clipboard", "-t", "TARGETS", "-o"],
21
+ context,
22
+ LIST_TYPES_TIMEOUT_MS,
23
+ );
24
+
25
+ if (targets.missingCommand) {
26
+ return providerUnavailable();
27
+ }
28
+
29
+ const advertisedMimeTypes = targets.ok ? parseMimeTypeList(targets.stdout) : [];
30
+
31
+ const preferredMimeType =
32
+ advertisedMimeTypes.length > 0 ? selectPreferredImageMimeType(advertisedMimeTypes) : null;
33
+ const mimeTypesToTry = preferredMimeType
34
+ ? [preferredMimeType, ...SUPPORTED_IMAGE_MIME_TYPES]
35
+ : [...SUPPORTED_IMAGE_MIME_TYPES];
36
+
37
+ for (const mimeType of mimeTypesToTry) {
38
+ const imageData = this.runCommand(
39
+ "xclip",
40
+ ["-selection", "clipboard", "-t", mimeType, "-o"],
41
+ context,
42
+ );
43
+
44
+ if (imageData.ok && imageData.stdout.length > 0) {
45
+ return providerImageResult(new Uint8Array(imageData.stdout), normalizeMimeType(mimeType));
46
+ }
47
+ }
48
+
49
+ return providerEmptyImage();
50
+ }
51
+ }