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.
@@ -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,88 +1,88 @@
1
- import type {
2
- ClipboardImageProvider,
3
- ClipboardProviderContext,
4
- ClipboardReadResult,
5
- } from "./types.js";
6
- import type { ClipboardImage } from "../types.js";
7
-
8
- export interface ClipboardProviderAttempt {
9
- providerId: string;
10
- available: boolean;
11
- imageFound: boolean;
12
- skipped: boolean;
13
- }
14
-
15
- export interface ClipboardProviderRegistryReadResult {
16
- image: ClipboardImage | null;
17
- attempts: ClipboardProviderAttempt[];
18
- }
19
-
20
- function supportsPlatform(provider: ClipboardImageProvider, platform: NodeJS.Platform): boolean {
21
- const { platforms } = provider.capabilities;
22
- return platforms === "*" || platforms.includes(platform);
23
- }
24
-
25
- function byPriority(left: ClipboardImageProvider, right: ClipboardImageProvider): number {
26
- return left.capabilities.priority - right.capabilities.priority;
27
- }
28
-
29
- export class ClipboardProviderRegistry {
30
- private readonly providers: ClipboardImageProvider[] = [];
31
-
32
- constructor(providers: readonly ClipboardImageProvider[] = []) {
33
- for (const provider of providers) {
34
- this.register(provider);
35
- }
36
- }
37
-
38
- register(provider: ClipboardImageProvider): this {
39
- this.providers.push(provider);
40
- return this;
41
- }
42
-
43
- getProviders(platform: NodeJS.Platform): ClipboardImageProvider[] {
44
- return this.providers.filter((provider) => supportsPlatform(provider, platform)).sort(byPriority);
45
- }
46
-
47
- async getEligible(context: ClipboardProviderContext): Promise<ClipboardImageProvider[]> {
48
- const eligible: ClipboardImageProvider[] = [];
49
- for (const provider of this.getProviders(context.platform)) {
50
- if (await provider.isAvailable(context)) {
51
- eligible.push(provider);
52
- }
53
- }
54
-
55
- return eligible;
56
- }
57
-
58
- async read(context: ClipboardProviderContext): Promise<ClipboardProviderRegistryReadResult> {
59
- const attempts: ClipboardProviderAttempt[] = [];
60
-
61
- for (const provider of this.getProviders(context.platform)) {
62
- const isAvailable = await provider.isAvailable(context);
63
- if (!isAvailable) {
64
- attempts.push({
65
- providerId: provider.capabilities.id,
66
- available: false,
67
- imageFound: false,
68
- skipped: true,
69
- });
70
- continue;
71
- }
72
-
73
- const result: ClipboardReadResult = await provider.read(context);
74
- attempts.push({
75
- providerId: provider.capabilities.id,
76
- available: result.available,
77
- imageFound: result.image !== null,
78
- skipped: false,
79
- });
80
-
81
- if (result.image) {
82
- return { image: result.image, attempts };
83
- }
84
- }
85
-
86
- return { image: null, attempts };
87
- }
88
- }
1
+ import type {
2
+ ClipboardImageProvider,
3
+ ClipboardProviderContext,
4
+ ClipboardReadResult,
5
+ } from "./types.js";
6
+ import type { ClipboardImage } from "../types.js";
7
+
8
+ export interface ClipboardProviderAttempt {
9
+ providerId: string;
10
+ available: boolean;
11
+ imageFound: boolean;
12
+ skipped: boolean;
13
+ }
14
+
15
+ export interface ClipboardProviderRegistryReadResult {
16
+ image: ClipboardImage | null;
17
+ attempts: ClipboardProviderAttempt[];
18
+ }
19
+
20
+ function supportsPlatform(provider: ClipboardImageProvider, platform: NodeJS.Platform): boolean {
21
+ const { platforms } = provider.capabilities;
22
+ return platforms === "*" || platforms.includes(platform);
23
+ }
24
+
25
+ function byPriority(left: ClipboardImageProvider, right: ClipboardImageProvider): number {
26
+ return left.capabilities.priority - right.capabilities.priority;
27
+ }
28
+
29
+ export class ClipboardProviderRegistry {
30
+ private readonly providers: ClipboardImageProvider[] = [];
31
+
32
+ constructor(providers: readonly ClipboardImageProvider[] = []) {
33
+ for (const provider of providers) {
34
+ this.register(provider);
35
+ }
36
+ }
37
+
38
+ register(provider: ClipboardImageProvider): this {
39
+ this.providers.push(provider);
40
+ return this;
41
+ }
42
+
43
+ getProviders(platform: NodeJS.Platform): ClipboardImageProvider[] {
44
+ return this.providers.filter((provider) => supportsPlatform(provider, platform)).sort(byPriority);
45
+ }
46
+
47
+ async getEligible(context: ClipboardProviderContext): Promise<ClipboardImageProvider[]> {
48
+ const eligible: ClipboardImageProvider[] = [];
49
+ for (const provider of this.getProviders(context.platform)) {
50
+ if (await provider.isAvailable(context)) {
51
+ eligible.push(provider);
52
+ }
53
+ }
54
+
55
+ return eligible;
56
+ }
57
+
58
+ async read(context: ClipboardProviderContext): Promise<ClipboardProviderRegistryReadResult> {
59
+ const attempts: ClipboardProviderAttempt[] = [];
60
+
61
+ for (const provider of this.getProviders(context.platform)) {
62
+ const isAvailable = await provider.isAvailable(context);
63
+ if (!isAvailable) {
64
+ attempts.push({
65
+ providerId: provider.capabilities.id,
66
+ available: false,
67
+ imageFound: false,
68
+ skipped: true,
69
+ });
70
+ continue;
71
+ }
72
+
73
+ const result: ClipboardReadResult = await provider.read(context);
74
+ attempts.push({
75
+ providerId: provider.capabilities.id,
76
+ available: result.available,
77
+ imageFound: result.image !== null,
78
+ skipped: false,
79
+ });
80
+
81
+ if (result.image) {
82
+ return { image: result.image, attempts };
83
+ }
84
+ }
85
+
86
+ return { image: null, attempts };
87
+ }
88
+ }
@@ -1,24 +1,24 @@
1
- import type { ClipboardImage } from "../types.js";
2
-
3
- export interface ClipboardReadResult {
4
- available: boolean;
5
- image: ClipboardImage | null;
6
- }
7
-
8
- export interface ClipboardProviderContext {
9
- platform: NodeJS.Platform;
10
- environment: NodeJS.ProcessEnv;
11
- }
12
-
13
- export interface ProviderCapabilities {
14
- readonly id: string;
15
- readonly name: string;
16
- readonly platforms: readonly NodeJS.Platform[] | "*";
17
- readonly priority: number;
18
- }
19
-
20
- export interface ClipboardImageProvider {
21
- readonly capabilities: ProviderCapabilities;
22
- isAvailable(context: ClipboardProviderContext): boolean | Promise<boolean>;
23
- read(context: ClipboardProviderContext): ClipboardReadResult | Promise<ClipboardReadResult>;
24
- }
1
+ import type { ClipboardImage } from "../types.js";
2
+
3
+ export interface ClipboardReadResult {
4
+ available: boolean;
5
+ image: ClipboardImage | null;
6
+ }
7
+
8
+ export interface ClipboardProviderContext {
9
+ platform: NodeJS.Platform;
10
+ environment: NodeJS.ProcessEnv;
11
+ }
12
+
13
+ export interface ProviderCapabilities {
14
+ readonly id: string;
15
+ readonly name: string;
16
+ readonly platforms: readonly NodeJS.Platform[] | "*";
17
+ readonly priority: number;
18
+ }
19
+
20
+ export interface ClipboardImageProvider {
21
+ readonly capabilities: ProviderCapabilities;
22
+ isAvailable(context: ClipboardProviderContext): boolean | Promise<boolean>;
23
+ read(context: ClipboardProviderContext): ClipboardReadResult | Promise<ClipboardReadResult>;
24
+ }
@@ -1,81 +1,46 @@
1
+ import { CommandRunnerProvider, providerEmptyImage, providerImageResult, providerUnavailable, LIST_TYPES_TIMEOUT_MS, parseMimeTypeList, type CommandRunnerProviderOptions, type ClipboardProviderContext, type ClipboardReadResult } from "./provider-helpers.js";
1
2
  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
3
 
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;
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
+ );
32
15
  }
33
16
 
34
17
  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
- });
18
+ const listTypes = this.runCommand("wl-paste", ["--list-types"], context, LIST_TYPES_TIMEOUT_MS);
40
19
  if (listTypes.missingCommand) {
41
- return { available: false, image: null };
20
+ return providerUnavailable();
42
21
  }
43
22
 
44
23
  if (!listTypes.ok) {
45
- return { available: true, image: null };
24
+ return providerEmptyImage();
46
25
  }
47
26
 
48
- const mimeTypes = listTypes.stdout
49
- .toString("utf8")
50
- .split(/\r?\n/)
51
- .map((mimeType) => mimeType.trim())
52
- .filter((mimeType) => mimeType.length > 0);
27
+ const mimeTypes = parseMimeTypeList(listTypes.stdout);
53
28
 
54
29
  const selectedMimeType = selectPreferredImageMimeType(mimeTypes);
55
30
  if (!selectedMimeType) {
56
- return { available: true, image: null };
31
+ return providerEmptyImage();
57
32
  }
58
33
 
59
- const imageData = this.commandRunner(
34
+ const imageData = this.runCommand(
60
35
  "wl-paste",
61
36
  ["--type", selectedMimeType, "--no-newline"],
62
- {
63
- environment: context.environment,
64
- maxBuffer: MAX_BUFFER_BYTES,
65
- timeout: READ_TIMEOUT_MS,
66
- },
37
+ context,
67
38
  );
68
39
 
69
40
  if (!imageData.ok || imageData.stdout.length === 0) {
70
- return { available: true, image: null };
41
+ return providerEmptyImage();
71
42
  }
72
43
 
73
- return {
74
- available: true,
75
- image: {
76
- bytes: new Uint8Array(imageData.stdout),
77
- mimeType: normalizeMimeType(selectedMimeType),
78
- },
79
- };
44
+ return providerImageResult(new Uint8Array(imageData.stdout), normalizeMimeType(selectedMimeType));
80
45
  }
81
46
  }
@@ -1,58 +1,32 @@
1
+ import { CommandRunnerProvider, providerEmptyImage, providerImageResult, providerUnavailable, LIST_TYPES_TIMEOUT_MS, parseMimeTypeList, type CommandRunnerProviderOptions, type ClipboardProviderContext, type ClipboardReadResult } from "./provider-helpers.js";
1
2
  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
3
 
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;
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
+ );
32
15
  }
33
16
 
34
17
  read(context: ClipboardProviderContext): ClipboardReadResult {
35
- const targets = this.commandRunner(
18
+ const targets = this.runCommand(
36
19
  "xclip",
37
20
  ["-selection", "clipboard", "-t", "TARGETS", "-o"],
38
- {
39
- environment: context.environment,
40
- maxBuffer: MAX_BUFFER_BYTES,
41
- timeout: LIST_TYPES_TIMEOUT_MS,
42
- },
21
+ context,
22
+ LIST_TYPES_TIMEOUT_MS,
43
23
  );
44
24
 
45
25
  if (targets.missingCommand) {
46
- return { available: false, image: null };
26
+ return providerUnavailable();
47
27
  }
48
28
 
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
- : [];
29
+ const advertisedMimeTypes = targets.ok ? parseMimeTypeList(targets.stdout) : [];
56
30
 
57
31
  const preferredMimeType =
58
32
  advertisedMimeTypes.length > 0 ? selectPreferredImageMimeType(advertisedMimeTypes) : null;
@@ -61,27 +35,17 @@ export class XclipProvider implements ClipboardImageProvider {
61
35
  : [...SUPPORTED_IMAGE_MIME_TYPES];
62
36
 
63
37
  for (const mimeType of mimeTypesToTry) {
64
- const imageData = this.commandRunner(
38
+ const imageData = this.runCommand(
65
39
  "xclip",
66
40
  ["-selection", "clipboard", "-t", mimeType, "-o"],
67
- {
68
- environment: context.environment,
69
- maxBuffer: MAX_BUFFER_BYTES,
70
- timeout: READ_TIMEOUT_MS,
71
- },
41
+ context,
72
42
  );
73
43
 
74
44
  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
- };
45
+ return providerImageResult(new Uint8Array(imageData.stdout), normalizeMimeType(mimeType));
82
46
  }
83
47
  }
84
48
 
85
- return { available: true, image: null };
49
+ return providerEmptyImage();
86
50
  }
87
51
  }