pi-image-tools 1.2.1 → 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.
- package/CHANGELOG.md +23 -0
- package/README.md +10 -7
- package/package.json +69 -69
- package/src/clipboard.ts +54 -291
- package/src/config.ts +24 -15
- package/src/image-size.ts +62 -63
- package/src/image-transcode.ts +198 -0
- package/src/index.ts +115 -32
- package/src/inline-user-preview.ts +225 -69
- package/src/keybindings.ts +61 -60
- package/src/providers/command-runner.ts +68 -0
- package/src/providers/mac-osascript-pngf.ts +101 -0
- package/src/providers/mac-osascript-publicpng.ts +77 -0
- package/src/providers/mac-pngpaste.ts +69 -0
- package/src/providers/native-module.ts +84 -0
- package/src/providers/powershell-forms.ts +95 -0
- package/src/providers/registry.ts +88 -0
- package/src/providers/types.ts +24 -0
- package/src/providers/wl-paste.ts +81 -0
- package/src/providers/xclip.ts +87 -0
- package/src/recent-images.ts +1 -0
- package/src/shell-environment.ts +75 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [1.3.1] - 2026-06-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Default paste-image shortcuts now avoid Pi built-in shortcut conflicts on all platforms, including macOS. Previously macOS defaulted to overriding Pi's built-in `ctrl+v` image paste binding and printed a startup conflict warning. (reported by @gaodes in [#8](https://github.com/MasuRii/pi-image-tools/issues/8))
|
|
9
|
+
- Transcode clipboard images that arrive in MIME types model providers reject (most notably `image/bmp` from WSLg's Wayland clipboard bridge for Windows clipboard images, and `image/tiff` from some macOS sources) into PNG via ImageMagick before attaching them. Previously, pasting a Windows screenshot inside WSL produced an `Unknown image type: image/bmp` error from the model provider that blocked the message from being sent. (contributed by @codeviking428 in [#9](https://github.com/MasuRii/pi-image-tools/pull/9))
|
|
10
|
+
|
|
11
|
+
## [1.3.0] - 2026-06-01
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Added a provider registry for platform-specific clipboard readers.
|
|
15
|
+
- Added macOS `pngpaste` and `osascript` clipboard fallbacks.
|
|
16
|
+
- Added an inline preview queue and chat component for user image previews.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Deferred preview, clipboard, and recent-image module loading from the extension entrypoint.
|
|
20
|
+
- Made shortcut conflict defaults platform-aware.
|
|
21
|
+
- Updated Pi peer dependency ranges and the optional clipboard dependency for 0.78-compatible runtimes.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Improved shell and session handling for clipboard command providers.
|
|
25
|
+
|
|
3
26
|
## [1.2.1] - 2026-05-26
|
|
4
27
|
|
|
5
28
|
### Changed
|
package/README.md
CHANGED
|
@@ -25,11 +25,10 @@ Image attachment and preview extension for the **Pi coding agent**.
|
|
|
25
25
|
|----------|-----------------|---------------------|-------|
|
|
26
26
|
| Windows | Yes | Yes | Uses native clipboard module first, then PowerShell fallback |
|
|
27
27
|
| Linux | Yes | Yes | Requires a graphical session; uses `wl-paste` or `xclip`, then native module fallback |
|
|
28
|
-
|
|
|
28
|
+
| WSL2 (WSLg) | Yes | Yes | Linux providers see Windows clipboard images as `image/bmp`; install ImageMagick (`magick`/`convert`) so they can be transcoded to PNG before attaching |
|
|
29
|
+
| macOS | Yes | Yes | Uses `pngpaste` first, then `osascript` and native module fallbacks |
|
|
29
30
|
| Termux / headless Linux | No | Limited | Clipboard image paste is disabled without a graphical session |
|
|
30
31
|
|
|
31
|
-
\* macOS clipboard image support relies on the optional native clipboard module.
|
|
32
|
-
|
|
33
32
|
## Installation
|
|
34
33
|
|
|
35
34
|
### Extension folder
|
|
@@ -124,13 +123,15 @@ $env:PI_IMAGE_TOOLS_RECENT_DIRS = "C:\Users\me\Pictures\Screenshots;D:\Shares\Sc
|
|
|
124
123
|
|
|
125
124
|
### Runtime config
|
|
126
125
|
|
|
127
|
-
A config file can be placed at:
|
|
126
|
+
A config file can be placed at the `pi-image-tools` package/install root next to `index.ts`. For npm-installed extensions this is the package directory under Pi's npm install area, for example:
|
|
128
127
|
|
|
129
128
|
```text
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
~/.pi/agent/npm/node_modules/pi-image-tools/config.json
|
|
130
|
+
$PI_CODING_AGENT_DIR/npm/node_modules/pi-image-tools/config.json when PI_CODING_AGENT_DIR is set
|
|
132
131
|
```
|
|
133
132
|
|
|
133
|
+
For a local checkout or extension path, place `config.json` in that `pi-image-tools` directory.
|
|
134
|
+
|
|
134
135
|
Starter template:
|
|
135
136
|
|
|
136
137
|
```json
|
|
@@ -273,7 +274,9 @@ Preview behavior:
|
|
|
273
274
|
- `xclip` in X11 sessions
|
|
274
275
|
- `@mariozechner/clipboard` fallback
|
|
275
276
|
- **macOS**
|
|
276
|
-
-
|
|
277
|
+
- `pngpaste`
|
|
278
|
+
- `osascript` PNG clipboard readers
|
|
279
|
+
- `@mariozechner/clipboard` fallback
|
|
277
280
|
|
|
278
281
|
If a platform-specific reader exists but no image is currently on the clipboard, the command returns a normal “No image found in clipboard” message. If no usable reader exists at all, the extension surfaces a setup-oriented error.
|
|
279
282
|
|
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-image-tools",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Image attachment and rendering extension for Pi TUI",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./index.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./index.ts"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"index.ts",
|
|
12
|
-
"src",
|
|
13
|
-
"config/config.example.json",
|
|
14
|
-
"README.md",
|
|
15
|
-
"CHANGELOG.md",
|
|
16
|
-
"LICENSE"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"typecheck": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noEmit",
|
|
20
|
-
"build": "npm run typecheck",
|
|
21
|
-
"lint": "npm run typecheck",
|
|
22
|
-
"test": "node --test",
|
|
23
|
-
"check": "npm run build && npm run test"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"pi-package",
|
|
27
|
-
"pi",
|
|
28
|
-
"pi-extension",
|
|
29
|
-
"pi-coding-agent",
|
|
30
|
-
"pi-tui",
|
|
31
|
-
"coding-agent",
|
|
32
|
-
"image",
|
|
33
|
-
"image-attachment",
|
|
34
|
-
"image-preview",
|
|
35
|
-
"clipboard",
|
|
36
|
-
"preview",
|
|
37
|
-
"windows",
|
|
38
|
-
"linux",
|
|
39
|
-
"cross-platform"
|
|
40
|
-
],
|
|
41
|
-
"author": "MasuRii",
|
|
42
|
-
"license": "MIT",
|
|
43
|
-
"repository": {
|
|
44
|
-
"type": "git",
|
|
45
|
-
"url": "git+https://github.com/MasuRii/pi-image-tools.git"
|
|
46
|
-
},
|
|
47
|
-
"homepage": "https://github.com/MasuRii/pi-image-tools#readme",
|
|
48
|
-
"bugs": {
|
|
49
|
-
"url": "https://github.com/MasuRii/pi-image-tools/issues"
|
|
50
|
-
},
|
|
51
|
-
"engines": {
|
|
52
|
-
"node": ">=20"
|
|
53
|
-
},
|
|
54
|
-
"publishConfig": {
|
|
55
|
-
"access": "public"
|
|
56
|
-
},
|
|
57
|
-
"pi": {
|
|
58
|
-
"extensions": [
|
|
59
|
-
"./index.ts"
|
|
60
|
-
]
|
|
61
|
-
},
|
|
62
|
-
"peerDependencies": {
|
|
63
|
-
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0",
|
|
64
|
-
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0"
|
|
65
|
-
},
|
|
66
|
-
"optionalDependencies": {
|
|
67
|
-
"@mariozechner/clipboard": "^0.3.
|
|
68
|
-
}
|
|
69
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-image-tools",
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Image attachment and rendering extension for Pi TUI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"src",
|
|
13
|
+
"config/config.example.json",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noEmit",
|
|
20
|
+
"build": "npm run typecheck",
|
|
21
|
+
"lint": "npm run typecheck",
|
|
22
|
+
"test": "node --test test/*.test.js test/providers/*.test.js",
|
|
23
|
+
"check": "npm run build && npm run test"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"pi-package",
|
|
27
|
+
"pi",
|
|
28
|
+
"pi-extension",
|
|
29
|
+
"pi-coding-agent",
|
|
30
|
+
"pi-tui",
|
|
31
|
+
"coding-agent",
|
|
32
|
+
"image",
|
|
33
|
+
"image-attachment",
|
|
34
|
+
"image-preview",
|
|
35
|
+
"clipboard",
|
|
36
|
+
"preview",
|
|
37
|
+
"windows",
|
|
38
|
+
"linux",
|
|
39
|
+
"cross-platform"
|
|
40
|
+
],
|
|
41
|
+
"author": "MasuRii",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/MasuRii/pi-image-tools.git"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/MasuRii/pi-image-tools#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/MasuRii/pi-image-tools/issues"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"pi": {
|
|
58
|
+
"extensions": [
|
|
59
|
+
"./index.ts"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0",
|
|
64
|
+
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0"
|
|
65
|
+
},
|
|
66
|
+
"optionalDependencies": {
|
|
67
|
+
"@mariozechner/clipboard": "^0.3.9"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/clipboard.ts
CHANGED
|
@@ -1,263 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
interface CommandResult {
|
|
17
|
-
ok: boolean;
|
|
18
|
-
stdout: Buffer;
|
|
19
|
-
missingCommand: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface ClipboardReadResult {
|
|
23
|
-
available: boolean;
|
|
24
|
-
image: ClipboardImage | null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function hasGraphicalSession(platform: NodeJS.Platform, environment: NodeJS.ProcessEnv): boolean {
|
|
28
|
-
return platform !== "linux" || Boolean(environment.DISPLAY || environment.WAYLAND_DISPLAY);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isWaylandSession(environment: NodeJS.ProcessEnv): boolean {
|
|
32
|
-
return Boolean(environment.WAYLAND_DISPLAY) || environment.XDG_SESSION_TYPE === "wayland";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function loadClipboardModule(
|
|
36
|
-
platform: NodeJS.Platform = process.platform,
|
|
37
|
-
environment: NodeJS.ProcessEnv = process.env,
|
|
38
|
-
): ClipboardModule | null {
|
|
39
|
-
if (cachedClipboardModule !== undefined) {
|
|
40
|
-
return cachedClipboardModule;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (environment.TERMUX_VERSION || !hasGraphicalSession(platform, environment)) {
|
|
44
|
-
cachedClipboardModule = null;
|
|
45
|
-
return cachedClipboardModule;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
cachedClipboardModule = require("@mariozechner/clipboard") as ClipboardModule;
|
|
50
|
-
} catch {
|
|
51
|
-
cachedClipboardModule = null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return cachedClipboardModule;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function readClipboardImageViaNativeModule(
|
|
1
|
+
import { hasGraphicalSession, isWaylandSession } from "./shell-environment.js";
|
|
2
|
+
import { transcodeToSupportedFormat, type TranscodeOptions } from "./image-transcode.js";
|
|
3
|
+
import { NativeModuleProvider } from "./providers/native-module.js";
|
|
4
|
+
import { OsascriptPngfProvider } from "./providers/mac-osascript-pngf.js";
|
|
5
|
+
import { OsascriptPublicPngProvider } from "./providers/mac-osascript-publicpng.js";
|
|
6
|
+
import { PngpasteProvider } from "./providers/mac-pngpaste.js";
|
|
7
|
+
import { PowerShellFormsProvider } from "./providers/powershell-forms.js";
|
|
8
|
+
import { ClipboardProviderRegistry } from "./providers/registry.js";
|
|
9
|
+
import { WlPasteProvider } from "./providers/wl-paste.js";
|
|
10
|
+
import { XclipProvider } from "./providers/xclip.js";
|
|
11
|
+
import type { ClipboardImage } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export { hasGraphicalSession } from "./shell-environment.js";
|
|
14
|
+
|
|
15
|
+
export function buildDefaultClipboardProviderRegistry(
|
|
58
16
|
platform: NodeJS.Platform,
|
|
59
17
|
environment: NodeJS.ProcessEnv,
|
|
60
|
-
):
|
|
61
|
-
const
|
|
62
|
-
if (!clipboard) {
|
|
63
|
-
return { available: false, image: null };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!clipboard.hasImage()) {
|
|
67
|
-
return { available: true, image: null };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const imageData = await clipboard.getImageBinary();
|
|
71
|
-
if (!imageData || imageData.length === 0) {
|
|
72
|
-
return { available: true, image: null };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
|
|
76
|
-
return {
|
|
77
|
-
available: true,
|
|
78
|
-
image: {
|
|
79
|
-
bytes,
|
|
80
|
-
mimeType: "image/png",
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function runCommand(
|
|
86
|
-
command: string,
|
|
87
|
-
args: string[],
|
|
88
|
-
timeout: number,
|
|
89
|
-
): CommandResult {
|
|
90
|
-
const result = spawnSync(command, args, {
|
|
91
|
-
timeout,
|
|
92
|
-
maxBuffer: MAX_BUFFER_BYTES,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (result.error) {
|
|
96
|
-
return {
|
|
97
|
-
ok: false,
|
|
98
|
-
stdout: Buffer.alloc(0),
|
|
99
|
-
missingCommand: isErrnoException(result.error) && result.error.code === "ENOENT",
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const stdout = Buffer.isBuffer(result.stdout)
|
|
104
|
-
? result.stdout
|
|
105
|
-
: Buffer.from(result.stdout ?? "", typeof result.stdout === "string" ? "utf8" : undefined);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
ok: result.status === 0,
|
|
109
|
-
stdout,
|
|
110
|
-
missingCommand: false,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function readClipboardImageViaPowerShell(): ClipboardReadResult {
|
|
115
|
-
const script = `
|
|
116
|
-
$ErrorActionPreference = 'Stop'
|
|
117
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
118
|
-
Add-Type -AssemblyName System.Drawing
|
|
119
|
-
|
|
120
|
-
if (-not [System.Windows.Forms.Clipboard]::ContainsImage()) {
|
|
121
|
-
return
|
|
122
|
-
}
|
|
18
|
+
): ClipboardProviderRegistry {
|
|
19
|
+
const registry = new ClipboardProviderRegistry();
|
|
123
20
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
$stream = New-Object System.IO.MemoryStream
|
|
130
|
-
try {
|
|
131
|
-
$image.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
132
|
-
[System.Convert]::ToBase64String($stream.ToArray())
|
|
133
|
-
} finally {
|
|
134
|
-
$stream.Dispose()
|
|
135
|
-
$image.Dispose()
|
|
136
|
-
}
|
|
137
|
-
`;
|
|
138
|
-
|
|
139
|
-
const result = runPowerShellCommand(script, {
|
|
140
|
-
encoded: true,
|
|
141
|
-
sta: true,
|
|
142
|
-
timeout: READ_TIMEOUT_MS,
|
|
143
|
-
maxBuffer: MAX_BUFFER_BYTES,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (result.missingCommand) {
|
|
147
|
-
return { available: false, image: null };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!result.ok) {
|
|
151
|
-
return { available: true, image: null };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const base64 = result.stdout.trim();
|
|
155
|
-
if (!base64) {
|
|
156
|
-
return { available: true, image: null };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
const bytes = Buffer.from(base64, "base64");
|
|
161
|
-
if (bytes.length === 0) {
|
|
162
|
-
return { available: true, image: null };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
available: true,
|
|
167
|
-
image: {
|
|
168
|
-
bytes: new Uint8Array(bytes),
|
|
169
|
-
mimeType: "image/png",
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
} catch {
|
|
173
|
-
return { available: true, image: null };
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function readClipboardImageViaWlPaste(): ClipboardReadResult {
|
|
178
|
-
const listTypes = runCommand("wl-paste", ["--list-types"], LIST_TYPES_TIMEOUT_MS);
|
|
179
|
-
if (listTypes.missingCommand) {
|
|
180
|
-
return { available: false, image: null };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!listTypes.ok) {
|
|
184
|
-
return { available: true, image: null };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const mimeTypes = listTypes.stdout
|
|
188
|
-
.toString("utf8")
|
|
189
|
-
.split(/\r?\n/)
|
|
190
|
-
.map((mimeType) => mimeType.trim())
|
|
191
|
-
.filter((mimeType) => mimeType.length > 0);
|
|
192
|
-
|
|
193
|
-
const selectedMimeType = selectPreferredImageMimeType(mimeTypes);
|
|
194
|
-
if (!selectedMimeType) {
|
|
195
|
-
return { available: true, image: null };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const imageData = runCommand(
|
|
199
|
-
"wl-paste",
|
|
200
|
-
["--type", selectedMimeType, "--no-newline"],
|
|
201
|
-
READ_TIMEOUT_MS,
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
if (!imageData.ok || imageData.stdout.length === 0) {
|
|
205
|
-
return { available: true, image: null };
|
|
21
|
+
if (platform === "win32") {
|
|
22
|
+
registry
|
|
23
|
+
.register(new NativeModuleProvider({ priority: 10 }))
|
|
24
|
+
.register(new PowerShellFormsProvider({ priority: 20 }));
|
|
25
|
+
return registry;
|
|
206
26
|
}
|
|
207
27
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function readClipboardImageViaXclip(): ClipboardReadResult {
|
|
218
|
-
const targets = runCommand(
|
|
219
|
-
"xclip",
|
|
220
|
-
["-selection", "clipboard", "-t", "TARGETS", "-o"],
|
|
221
|
-
LIST_TYPES_TIMEOUT_MS,
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
if (targets.missingCommand) {
|
|
225
|
-
return { available: false, image: null };
|
|
28
|
+
if (platform === "linux") {
|
|
29
|
+
const waylandFirst = isWaylandSession(environment);
|
|
30
|
+
registry
|
|
31
|
+
.register(new WlPasteProvider({ priority: waylandFirst ? 10 : 20 }))
|
|
32
|
+
.register(new XclipProvider({ priority: waylandFirst ? 20 : 10 }))
|
|
33
|
+
.register(new NativeModuleProvider({ priority: 30 }));
|
|
34
|
+
return registry;
|
|
226
35
|
}
|
|
227
36
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const preferredMimeType =
|
|
237
|
-
advertisedMimeTypes.length > 0 ? selectPreferredImageMimeType(advertisedMimeTypes) : null;
|
|
238
|
-
const mimeTypesToTry = preferredMimeType
|
|
239
|
-
? [preferredMimeType, ...SUPPORTED_IMAGE_MIME_TYPES]
|
|
240
|
-
: [...SUPPORTED_IMAGE_MIME_TYPES];
|
|
241
|
-
|
|
242
|
-
for (const mimeType of mimeTypesToTry) {
|
|
243
|
-
const imageData = runCommand(
|
|
244
|
-
"xclip",
|
|
245
|
-
["-selection", "clipboard", "-t", mimeType, "-o"],
|
|
246
|
-
READ_TIMEOUT_MS,
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
if (imageData.ok && imageData.stdout.length > 0) {
|
|
250
|
-
return {
|
|
251
|
-
available: true,
|
|
252
|
-
image: {
|
|
253
|
-
bytes: new Uint8Array(imageData.stdout),
|
|
254
|
-
mimeType: normalizeMimeType(mimeType),
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
}
|
|
37
|
+
if (platform === "darwin") {
|
|
38
|
+
registry
|
|
39
|
+
.register(new PngpasteProvider({ priority: 10 }))
|
|
40
|
+
.register(new OsascriptPublicPngProvider({ priority: 20 }))
|
|
41
|
+
.register(new OsascriptPngfProvider({ priority: 30 }))
|
|
42
|
+
.register(new NativeModuleProvider({ priority: 40 }));
|
|
43
|
+
return registry;
|
|
258
44
|
}
|
|
259
45
|
|
|
260
|
-
|
|
46
|
+
registry.register(new NativeModuleProvider({ priority: 100 }));
|
|
47
|
+
return registry;
|
|
261
48
|
}
|
|
262
49
|
|
|
263
50
|
function getUnavailableReaderMessage(platform: NodeJS.Platform): string {
|
|
@@ -265,7 +52,7 @@ function getUnavailableReaderMessage(platform: NodeJS.Platform): string {
|
|
|
265
52
|
case "linux":
|
|
266
53
|
return "No Linux clipboard image reader is available. Install wl-clipboard or xclip, or ensure @mariozechner/clipboard is installed.";
|
|
267
54
|
case "darwin":
|
|
268
|
-
return "No macOS clipboard image reader is available.
|
|
55
|
+
return "No macOS clipboard image reader is available. Install pngpaste, ensure osascript is available, or ensure @mariozechner/clipboard is installed.";
|
|
269
56
|
case "win32":
|
|
270
57
|
return "No Windows clipboard image reader is available. Ensure PowerShell is available or @mariozechner/clipboard is installed.";
|
|
271
58
|
default:
|
|
@@ -276,6 +63,14 @@ function getUnavailableReaderMessage(platform: NodeJS.Platform): string {
|
|
|
276
63
|
export async function readClipboardImage(options?: {
|
|
277
64
|
environment?: NodeJS.ProcessEnv;
|
|
278
65
|
platform?: NodeJS.Platform;
|
|
66
|
+
registry?: ClipboardProviderRegistry;
|
|
67
|
+
/**
|
|
68
|
+
* Hooks for the post-read transcoding step that normalizes images into a
|
|
69
|
+
* MIME type accepted by model providers. Mainly for tests; the default
|
|
70
|
+
* implementation shells out to ImageMagick only when the source format isn't
|
|
71
|
+
* already supported.
|
|
72
|
+
*/
|
|
73
|
+
transcode?: TranscodeOptions;
|
|
279
74
|
}): Promise<ClipboardImage | null> {
|
|
280
75
|
const environment = options?.environment ?? process.env;
|
|
281
76
|
const platform = options?.platform ?? process.platform;
|
|
@@ -288,47 +83,15 @@ export async function readClipboardImage(options?: {
|
|
|
288
83
|
throw new Error("Clipboard image paste requires a graphical desktop session with DISPLAY or WAYLAND_DISPLAY.");
|
|
289
84
|
}
|
|
290
85
|
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (platform === "win32") {
|
|
299
|
-
const nativeImage = recordResult(await readClipboardImageViaNativeModule(platform, environment));
|
|
300
|
-
if (nativeImage) {
|
|
301
|
-
return nativeImage;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const powerShellImage = recordResult(readClipboardImageViaPowerShell());
|
|
305
|
-
if (powerShellImage) {
|
|
306
|
-
return powerShellImage;
|
|
307
|
-
}
|
|
308
|
-
} else if (platform === "linux") {
|
|
309
|
-
const sessionReaders = isWaylandSession(environment)
|
|
310
|
-
? [readClipboardImageViaWlPaste, readClipboardImageViaXclip]
|
|
311
|
-
: [readClipboardImageViaXclip, readClipboardImageViaWlPaste];
|
|
312
|
-
|
|
313
|
-
for (const reader of sessionReaders) {
|
|
314
|
-
const image = recordResult(reader());
|
|
315
|
-
if (image) {
|
|
316
|
-
return image;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const nativeImage = recordResult(await readClipboardImageViaNativeModule(platform, environment));
|
|
321
|
-
if (nativeImage) {
|
|
322
|
-
return nativeImage;
|
|
323
|
-
}
|
|
324
|
-
} else {
|
|
325
|
-
const nativeImage = recordResult(await readClipboardImageViaNativeModule(platform, environment));
|
|
326
|
-
if (nativeImage) {
|
|
327
|
-
return nativeImage;
|
|
328
|
-
}
|
|
86
|
+
const registry = options?.registry ?? buildDefaultClipboardProviderRegistry(platform, environment);
|
|
87
|
+
const result = await registry.read({ platform, environment });
|
|
88
|
+
if (result.image) {
|
|
89
|
+
// Providers (notably wl-paste under WSLg) may return formats like
|
|
90
|
+
// image/bmp that model providers reject. Normalize to PNG when needed.
|
|
91
|
+
return transcodeToSupportedFormat(result.image, { platform, ...options?.transcode });
|
|
329
92
|
}
|
|
330
93
|
|
|
331
|
-
if (
|
|
94
|
+
if (result.attempts.some((attempt) => attempt.available)) {
|
|
332
95
|
return null;
|
|
333
96
|
}
|
|
334
97
|
|