pi-image-tools 1.3.0 → 1.3.1

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,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,81 @@
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 { 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,87 +1,87 @@
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 { 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
+ }
@@ -462,6 +462,7 @@ export function loadRecentImage(
462
462
  ): ClipboardImage {
463
463
  assertImageWithinByteLimit(candidate.sizeBytes, `Recent image ${candidate.name}`, environment);
464
464
  const raw = readFileSync(candidate.path);
465
+ assertImageWithinByteLimit(raw.length, `Recent image ${candidate.name}`, environment);
465
466
  if (raw.length === 0) {
466
467
  throw new Error(`File is empty: ${candidate.path}`);
467
468
  }
@@ -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
+ }