@linxiraos/pi-natives 1.1.25 → 1.1.27

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/README.md CHANGED
@@ -10,6 +10,7 @@ Native Rust functionality via N-API.
10
10
  - **Audio**: Cross-platform low-latency microphone capture and gapless speaker playback
11
11
  - **WebRTC**: Native Opus media, SDP offer/answer negotiation, and data-channel events for live sessions
12
12
  - **File locking**: Process-owned cross-process locks with in-memory kernel names on Linux/Windows and `flock(2)` sidecars on other Unix platforms
13
+ - **PDF**: In-memory PDF-to-Markdown extraction with OCR-page classification via `pdf-inspector`
13
14
 
14
15
  General-purpose image processing (decode/resize/encode for files and buffers)
15
16
  lives in [`Bun.Image`](https://bun.com/docs/runtime/image) on the JS side; this
@@ -19,7 +20,7 @@ that terminal protocol.
19
20
  ## Usage
20
21
 
21
22
  ```typescript
22
- import { grep, find, encodeSixel } from "@linxiraos/pi-natives";
23
+ import { encodeSixelAsync, grep, pdfToMarkdown } from "@linxiraos/pi-natives";
23
24
 
24
25
  // Grep for a pattern
25
26
  const results = await grep({
@@ -36,8 +37,13 @@ const files = await find({
36
37
  fileType: "file",
37
38
  });
38
39
 
39
- // SIXEL encode for a terminal cell box (px)
40
- const sequence = encodeSixel(pngBytes, widthPx, heightPx);
40
+ // SIXEL encode for a terminal cell box (px), off the JS thread
41
+ // (`encodeSixel` is the synchronous form)
42
+ const sequence = await encodeSixelAsync(pngBytes, widthPx, heightPx);
43
+
44
+ // Extract PDF text and identify pages that still need OCR
45
+ const pdf = await pdfToMarkdown(pdfBytes);
46
+ console.log(pdf.markdown, pdf.pagesNeedingOcr);
41
47
  ```
42
48
 
43
49
  ### Lazy Windows path helpers
@@ -1,28 +1,4 @@
1
- interface AdaptedDesktopCapabilities {
2
- readonly [key: string]: unknown;
3
- readonly ax: boolean;
4
- readonly backgroundWindowInput: boolean;
5
- readonly takeover: boolean;
6
- readonly axPermission: string;
7
- }
1
+ import type { DesktopSession } from "./index.js";
8
2
 
9
- interface AdaptedDesktopSession {
10
- readonly capabilities: AdaptedDesktopCapabilities;
11
- listWindows(): Promise<Array<Record<string, unknown>>>;
12
- capture(target: string, caps?: unknown): Promise<Record<string, unknown>>;
13
- click(
14
- target: string,
15
- x: number,
16
- y: number,
17
- options?: { button?: string; count?: number; modifiers?: string[]; takeover?: boolean },
18
- ): Promise<void>;
19
- typeText(target: string, text: string, options?: { takeover?: boolean }): Promise<void>;
20
- keyChord(target: string, keys: string[], options?: { takeover?: boolean }): Promise<void>;
21
- close(): Promise<void>;
22
- }
23
-
24
- interface AdaptedDesktopSessionConstructor {
25
- new (options: Record<string, unknown>): AdaptedDesktopSession;
26
- }
27
-
28
- export function adaptDesktopSession(NativeDesktopSession: unknown): AdaptedDesktopSessionConstructor;
3
+ /** Return the current native ABI, or a constructor that rejects an outdated addon on first desktop use. */
4
+ export function adaptDesktopSession(NativeDesktopSession: unknown): typeof DesktopSession;
@@ -1,404 +1,36 @@
1
- const LEGACY_ERROR_CODES = {
2
- DESKTOP_INVALID_OPTIONS: "InvalidTarget",
3
- DESKTOP_INVALID_ACTION: "InvalidTarget",
4
- DESKTOP_BACKEND_UNAVAILABLE: null,
5
- DESKTOP_PERMISSION_DENIED: "PermissionDenied",
6
- DESKTOP_CAPTURE_FAILED: "CaptureFailed",
7
- DESKTOP_INPUT_FAILED: "InputFailed",
8
- DESKTOP_DEADLINE_EXCEEDED: "Timeout",
9
- DESKTOP_LAYOUT_CHANGED: "InvalidCoordinateFrame",
10
- DESKTOP_COORDINATE_OUT_OF_BOUNDS: "InvalidCoordinateFrame",
11
- DESKTOP_SESSION_CLOSED: "Closed",
12
- DESKTOP_WORKER_FAILED: "Internal",
13
- };
14
-
15
- const ADAPTED_SESSION_CLASSES = new WeakMap();
16
-
17
- function desktopError(code, message) {
18
- return new Error(`${code}: ${message}`);
19
- }
20
-
21
- function normalizeError(error, fallbackCode) {
22
- if (!(error instanceof Error)) return desktopError(fallbackCode, String(error));
23
- if (/^[A-Z][A-Za-z]+: /.test(error.message)) return error;
24
-
25
- const match = /^(DESKTOP_[A-Z_]+):\s*(.*)$/.exec(error.message);
26
- if (match === null) return desktopError(fallbackCode, error.message);
27
- const code = LEGACY_ERROR_CODES[match[1]] ?? fallbackCode;
28
- return desktopError(code, match[2]);
29
- }
30
-
31
- function normalizeCapabilities(capabilities) {
32
- return {
33
- ...capabilities,
34
- ax: false,
35
- backgroundWindowInput: false,
36
- takeover: true,
37
- axPermission: "unavailable",
38
- };
39
- }
40
-
41
- function legacyPoint(point) {
42
- return { x: Math.round(point.x), y: Math.round(point.y) };
43
- }
44
-
45
- function captureCapsKey(caps) {
46
- return `${caps?.maxWidth ?? ""}:${caps?.maxHeight ?? ""}`;
47
- }
48
-
49
- function legacyButton(button) {
50
- return button === "middle" ? "wheel" : button;
51
- }
52
- function sourceDimensions(capture) {
53
- if (capture.sourceWidth !== undefined && capture.sourceHeight !== undefined) {
54
- return { sourceWidth: capture.sourceWidth, sourceHeight: capture.sourceHeight };
55
- }
56
- const displays = Array.isArray(capture.displays) ? capture.displays : [];
57
- if (displays.length === 0) {
58
- return { sourceWidth: capture.width, sourceHeight: capture.height };
59
- }
60
- const minX = Math.min(...displays.map(display => display.x));
61
- const minY = Math.min(...displays.map(display => display.y));
62
- const maxX = Math.max(...displays.map(display => display.x + display.width));
63
- const maxY = Math.max(...displays.map(display => display.y + display.height));
64
- const nativeScale = Math.max(1, ...displays.map(display => display.scale ?? 1));
65
- return {
66
- sourceWidth: Math.max(1, Math.round((maxX - minX) * nativeScale)),
67
- sourceHeight: Math.max(1, Math.round((maxY - minY) * nativeScale)),
68
- };
69
- }
70
-
71
- function displaysEqual(a, b) {
72
- if (a === b) return true;
73
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
74
- for (let i = 0; i < a.length; i++) {
75
- const x = a[i];
76
- const y = b[i];
77
- if (
78
- x === y ||
79
- (x?.id === y?.id &&
80
- x?.x === y?.x &&
81
- x?.y === y?.y &&
82
- x?.width === y?.width &&
83
- x?.height === y?.height &&
84
- x?.scale === y?.scale &&
85
- x?.pixelX === y?.pixelX &&
86
- x?.pixelY === y?.pixelY &&
87
- x?.pixelWidth === y?.pixelWidth &&
88
- x?.pixelHeight === y?.pixelHeight)
89
- ) {
90
- continue;
91
- }
92
- return false;
93
- }
94
- return true;
95
- }
96
-
97
- // Field-by-field frame comparison: the JSON.stringify signature allocated a
98
- // full string over the display list on every capture and every input action.
99
- // A rolling hash would risk collisions where string equality had none, so
100
- // compare the same fields directly instead.
101
- function sameFrame(a, b) {
102
- if (a === b) return true;
103
- if (!a || !b) return false;
104
- return a.target === b.target && a.width === b.width && a.height === b.height && displaysEqual(a.displays, b.displays);
105
- }
106
-
107
- // Lightweight projection of the fields sameFrame reads: the stored capture
108
- // must NOT retain the full screenshot (`data: Uint8Array`, multi-MB per
109
- // target) after callers finish with it.
110
- function frameIdentity(capture) {
111
- const displays = Array.isArray(capture.displays) ? capture.displays.map(displayOf) : capture.displays;
112
- return { target: capture.target, width: capture.width, height: capture.height, displays };
113
- }
114
-
115
- function displayOf(display) {
116
- return {
117
- id: display?.id,
118
- x: display?.x,
119
- y: display?.y,
120
- width: display?.width,
121
- height: display?.height,
122
- scale: display?.scale,
123
- pixelX: display?.pixelX,
124
- pixelY: display?.pixelY,
125
- pixelWidth: display?.pixelWidth,
126
- pixelHeight: display?.pixelHeight,
127
- };
128
- }
129
-
130
- function frameSignature(capture) {
131
- return JSON.stringify({
132
- target: capture.target,
133
- width: capture.width,
134
- height: capture.height,
135
- displays: capture.displays?.map(display => ({
136
- id: display.id,
137
- x: display.x,
138
- y: display.y,
139
- width: display.width,
140
- height: display.height,
141
- scale: display.scale,
142
- pixelX: display.pixelX,
143
- pixelY: display.pixelY,
144
- pixelWidth: display.pixelWidth,
145
- pixelHeight: display.pixelHeight,
146
- })),
147
- });
148
- }
149
-
150
- /**
151
- * Adapt the pre-parity desktop addon ABI used by pull-request CI artifacts to
152
- * the current session contract. Released addons exposed capture/execute/close;
153
- * current addons already expose the complete API and pass through unchanged.
154
- */
1
+ const REQUIRED_METHODS = [
2
+ "click",
3
+ "capture",
4
+ "captureRegion",
5
+ "cancel",
6
+ "retire",
7
+ "observe",
8
+ "listApplications",
9
+ "openApplication",
10
+ "menuItems",
11
+ "menuSelect",
12
+ "holdKeys",
13
+ "holdMouse",
14
+ "acquireControl",
15
+ "releaseControl",
16
+ "controlState",
17
+ "bringToCurrentSpace",
18
+ ];
19
+
20
+ /** Require the current desktop ABI; partial legacy emulation cannot preserve input ownership. */
155
21
  export function adaptDesktopSession(NativeDesktopSession) {
156
- if (typeof NativeDesktopSession?.prototype?.click === "function") return NativeDesktopSession;
157
- const cached = ADAPTED_SESSION_CLASSES.get(NativeDesktopSession);
158
- if (cached) return cached;
159
-
160
- class DesktopSession {
161
- #native;
162
- #nativeDesktopSession;
163
- #options;
164
- #sessions;
165
- #closed = false;
166
- #capturedTargets = new Map();
167
-
168
- constructor(options) {
169
- try {
170
- this.#nativeDesktopSession = NativeDesktopSession;
171
- this.#options = options;
172
- this.#native = new NativeDesktopSession(options);
173
- this.#sessions = new Map([[captureCapsKey(), this.#native]]);
174
- } catch (error) {
175
- throw normalizeError(error, "Internal");
176
- }
177
- }
178
-
179
- get capabilities() {
180
- return normalizeCapabilities(this.#native.capabilities);
181
- }
182
-
183
- #ensureOpen() {
184
- if (this.#closed) throw desktopError("Closed", "desktop session is closed");
185
- }
186
-
187
- #nativeForCapturedTarget(target) {
188
- const capture = this.#capturedTargets.get(target);
189
- if (capture) return capture.native;
190
- throw desktopError(
191
- "InvalidCoordinateFrame",
192
- `no capture of '${target}' yet — take a screenshot of this target first`,
193
- );
194
- }
195
-
196
- #sessionForCapture(caps) {
197
- const key = captureCapsKey(caps);
198
- const existing = this.#sessions.get(key);
199
- if (existing) return existing;
200
- try {
201
- const native = new this.#nativeDesktopSession({
202
- ...this.#options,
203
- maxWidth: caps?.maxWidth,
204
- maxHeight: caps?.maxHeight,
205
- });
206
- this.#sessions.set(key, native);
207
- return native;
208
- } catch (error) {
209
- throw normalizeError(error, "CaptureFailed");
210
- }
211
- }
212
-
213
- #ensureForeground(target, options) {
214
- if (target !== "desktop" && options?.takeover !== true) {
215
- throw desktopError(
216
- "BackgroundUnavailable",
217
- "the installed native addon supports foreground input only",
218
- );
219
- }
220
- }
221
-
222
- async #execute(actions, target, native = this.#native, fallbackCode = "InputFailed") {
223
- try {
224
- const capture = await native.execute(Array.isArray(actions) ? actions : [actions], target);
225
- const previous = this.#capturedTargets.get(target);
226
- if (capture && previous?.native === native && !sameFrame(capture, previous.capture)) {
227
- this.#capturedTargets.delete(target);
228
- }
229
- } catch (error) {
230
- throw normalizeError(error, fallbackCode);
231
- }
232
- }
233
-
234
- async listDisplays() {
235
- this.#ensureOpen();
236
- throw desktopError("CaptureFailed", "the installed native addon does not expose display enumeration");
237
- }
238
-
239
- async listWindows() {
240
- this.#ensureOpen();
241
- if (typeof this.#native.listWindows !== "function") {
242
- throw desktopError("CaptureFailed", "the installed native addon does not expose window enumeration");
243
- }
244
- try {
245
- return await this.#native.listWindows();
246
- } catch (error) {
247
- throw normalizeError(error, "CaptureFailed");
248
- }
249
- }
250
-
251
- async capture(target, caps) {
252
- this.#ensureOpen();
253
- if (target !== "desktop" && !/^\d+$/.test(target)) {
254
- throw desktopError("InvalidTarget", `invalid window target '${target}'`);
255
- }
256
- if (target !== "desktop" && typeof this.#native.listWindows !== "function") {
257
- throw desktopError("CaptureFailed", "the installed native addon does not support window capture");
258
- }
259
- try {
260
- const native = this.#sessionForCapture(caps);
261
- const capture = await native.capture(target);
262
- const adapted = { ...capture, ...sourceDimensions(capture), target };
263
- // Store only the comparison fields, not the screenshot bytes:
264
- // `adapted` carries the full PNG `data` (multi-MB per target).
265
- // Keep the string signature for any external reader of the
266
- // stored shape.
267
- this.#capturedTargets.set(target, { native, capture: frameIdentity(adapted), signature: frameSignature(adapted) });
268
- return adapted;
269
- } catch (error) {
270
- throw normalizeError(error, "CaptureFailed");
271
- }
272
- }
273
-
274
- async click(target, x, y, options) {
275
- this.#ensureOpen();
276
- this.#ensureForeground(target, options);
277
- const native = this.#nativeForCapturedTarget(target);
278
- const count = Math.max(1, options?.count ?? 1);
279
- const button = legacyButton(options?.button ?? "left");
280
- const point = { x: Math.round(x), y: Math.round(y), keys: options?.modifiers ?? [] };
281
- const actions =
282
- count === 2 && button === "left"
283
- ? [{ type: "double_click", ...point }]
284
- : Array.from({ length: count }, () => ({ type: "click", ...point, button }));
285
- await this.#execute(actions, target, native);
286
- }
287
-
288
- async moveMouse(target, x, y, options) {
289
- this.#ensureOpen();
290
- this.#ensureForeground(target, options);
291
- await this.#execute(
292
- { type: "move", x: Math.round(x), y: Math.round(y), keys: options?.modifiers ?? [] },
293
- target,
294
- this.#nativeForCapturedTarget(target),
295
- );
296
- }
297
-
298
- async drag(target, path, options) {
299
- this.#ensureOpen();
300
- this.#ensureForeground(target, options);
301
- await this.#execute(
302
- { type: "drag", path: path.map(legacyPoint), keys: options?.modifiers ?? [] },
303
- target,
304
- this.#nativeForCapturedTarget(target),
305
- );
306
- }
307
-
308
- async scroll(target, x, y, dx, dy, options) {
309
- this.#ensureOpen();
310
- this.#ensureForeground(target, options);
311
- await this.#execute(
312
- {
313
- type: "scroll",
314
- x: Math.round(x),
315
- y: Math.round(y),
316
- scroll_x: Math.round(dx),
317
- scroll_y: Math.round(dy),
318
- keys: options?.modifiers ?? [],
319
- },
320
- target,
321
- this.#nativeForCapturedTarget(target),
22
+ const prototype = NativeDesktopSession?.prototype;
23
+ const missing = REQUIRED_METHODS.filter(method => typeof prototype?.[method] !== "function");
24
+ if (missing.length === 0) {
25
+ return NativeDesktopSession;
26
+ }
27
+ // The shared native entrypoint is also imported by non-desktop tools. Keep
28
+ // their imports usable, but never emulate the new ABI with unsafe fallbacks.
29
+ return class UnsupportedDesktopSession {
30
+ constructor() {
31
+ throw new Error(
32
+ `Unsupported: desktop native addon is outdated (missing ${missing.join(", ")}). Update the native addon or run bun run build:native from the repository root.`,
322
33
  );
323
34
  }
324
-
325
- async typeText(target, text, options) {
326
- this.#ensureOpen();
327
- this.#ensureForeground(target, options);
328
- await this.#execute({ type: "type", text }, target, this.#capturedTargets.get(target)?.native ?? this.#native);
329
- }
330
-
331
- async keyChord(target, keys, options) {
332
- this.#ensureOpen();
333
- this.#ensureForeground(target, options);
334
- await this.#execute({ type: "keypress", keys }, target, this.#capturedTargets.get(target)?.native ?? this.#native);
335
- }
336
-
337
- async raiseWindow() {
338
- this.#ensureOpen();
339
- throw desktopError("BackgroundUnavailable", "the installed native addon does not support window control");
340
- }
341
-
342
- async axSnapshot() {
343
- this.#ensureOpen();
344
- throw desktopError("AxUnsupported", "accessibility is unavailable in the installed native addon");
345
- }
346
-
347
- async axQuery() {
348
- return this.axSnapshot();
349
- }
350
-
351
- async axElementAt() {
352
- return this.axSnapshot();
353
- }
354
-
355
- async axFocused() {
356
- return this.axSnapshot();
357
- }
358
-
359
- async axNode() {
360
- return this.axSnapshot();
361
- }
362
-
363
- async axAttributes() {
364
- return this.axSnapshot();
365
- }
366
-
367
- async axChildren() {
368
- return this.axSnapshot();
369
- }
370
-
371
- async axParent() {
372
- return this.axSnapshot();
373
- }
374
-
375
- async axPerform() {
376
- return this.axSnapshot();
377
- }
378
-
379
- async axSetValue() {
380
- return this.axSnapshot();
381
- }
382
-
383
- async axFocus() {
384
- return this.axSnapshot();
385
- }
386
-
387
- async axClick() {
388
- return this.axSnapshot();
389
- }
390
-
391
- async close() {
392
- if (this.#closed) return;
393
- this.#closed = true;
394
- try {
395
- await Promise.all([...this.#sessions.values()].map(native => native.close()));
396
- } catch (error) {
397
- throw normalizeError(error, "Internal");
398
- }
399
- }
400
- }
401
-
402
- ADAPTED_SESSION_CLASSES.set(NativeDesktopSession, DesktopSession);
403
- return DesktopSession;
35
+ };
404
36
  }
package/native/index.d.ts CHANGED
@@ -31,10 +31,42 @@ export declare class AudioPlayback {
31
31
  /** Persistent, serialized native desktop capture/input/accessibility session. */
32
32
  export declare class DesktopSession {
33
33
  constructor(options?: DesktopSessionOptions | undefined | null)
34
+ /**
35
+ * Asks the worker when it is idle, so permissions are read live. While
36
+ * another operation holds the worker, answers from the snapshot of the
37
+ * latest capabilities read or capture instead of blocking the JS thread
38
+ * behind it.
39
+ */
34
40
  get capabilities(): DesktopCapabilities
35
41
  listDisplays(): Promise<Array<DesktopDisplay>>
36
42
  listWindows(): Promise<Array<DesktopWindow>>
43
+ listApplications(options?: ApplicationQuery | undefined | null): Promise<Array<Application>>
44
+ openApplication(id: string, options?: ApplicationOpenOptions | undefined | null): Promise<Application>
45
+ /**
46
+ * Capture and accessibility share one serialized request. Neither a failed
47
+ * snapshot nor an abandoned reply replaces the last delivered input frame.
48
+ */
49
+ observe(target: string, caps?: CaptureCaps | undefined | null, axOptions?: AxSnapshotOptions | undefined | null): Promise<DesktopObservation>
50
+ menuItems(target: string, path?: Array<string> | undefined | null): Promise<Array<DesktopMenuItem>>
51
+ menuSelect(target: string, path: Array<string>): Promise<undefined>
52
+ bringToCurrentSpace(windowId: string): Promise<undefined>
53
+ holdKeys(target: string, keys: Array<string>, options: HoldOptions): Promise<undefined>
54
+ holdMouse(target: string, x: number, y: number, options: HoldOptions): Promise<undefined>
55
+ /**
56
+ * Native ownership only. Human approval is required by the host before
57
+ * calling this method.
58
+ */
59
+ acquireControl(): Promise<DesktopControlState>
60
+ releaseControl(): void
61
+ controlState(): DesktopControlState
62
+ /** Retire queued work from a completed helper without revoking task control. */
63
+ retire(): void
37
64
  capture(target: string, caps?: CaptureCaps | undefined | null): Promise<DesktopCapture>
65
+ /**
66
+ * Capture a fresh native-detail region without replacing the full input
67
+ * coordinate frame.
68
+ */
69
+ captureRegion(target: string, region: CaptureRegion, caps?: CaptureCaps | undefined | null): Promise<DesktopCapture>
38
70
  click(target: string, x: number, y: number, opts?: PointerOptions | undefined | null): Promise<undefined>
39
71
  moveMouse(target: string, x: number, y: number, opts?: PointerOptions | undefined | null): Promise<undefined>
40
72
  drag(target: string, path: Array<DesktopPoint>, opts?: PointerOptions | undefined | null): Promise<undefined>
@@ -58,6 +90,11 @@ export declare class DesktopSession {
58
90
  axSetValue(reference: string, value: string): Promise<undefined>
59
91
  axFocus(reference: string): Promise<undefined>
60
92
  axClick(reference: string, opts?: PointerOptions | undefined | null): Promise<undefined>
93
+ /**
94
+ * Immediately cancel operations submitted before this call. Later
95
+ * operations may proceed.
96
+ */
97
+ cancel(): void
61
98
  close(): Promise<undefined>
62
99
  }
63
100
 
@@ -722,6 +759,29 @@ export declare function appleFmCancel(handle: number): void
722
759
  */
723
760
  export declare function appleFmGenerate(request: string, onEvent: (err: null | Error, event: string) => void): number
724
761
 
762
+ /**
763
+ * Installed application identity with currently observable process
764
+ * information.
765
+ */
766
+ export interface Application {
767
+ id: string
768
+ name: string
769
+ path: string
770
+ running: boolean
771
+ pid?: number
772
+ }
773
+
774
+ /** Controls whether launching an application deliberately activates it. */
775
+ export interface ApplicationOpenOptions {
776
+ activate?: boolean
777
+ }
778
+
779
+ /** Filters the native application inventory without requiring screen access. */
780
+ export interface ApplicationQuery {
781
+ query?: string
782
+ runningOnly?: boolean
783
+ }
784
+
725
785
  /**
726
786
  * Apply ast-grep rewrite rules to matching files; honors `dryRun` and returns
727
787
  * a promise.
@@ -1013,6 +1073,15 @@ export interface AxSnapshotOptions {
1013
1073
  all?: boolean
1014
1074
  }
1015
1075
 
1076
+ export interface BlockParseOptions {
1077
+ /** Source code to parse. */
1078
+ code: string
1079
+ /** Language alias (e.g. "rust", "typescript") used before path inference. */
1080
+ lang?: string
1081
+ /** File path used to infer language by extension when `lang` is omitted. */
1082
+ path?: string
1083
+ }
1084
+
1016
1085
  export interface BlockRange {
1017
1086
  /** 1-indexed inclusive first line of the resolved block. */
1018
1087
  startLine: number
@@ -1045,6 +1114,14 @@ export interface CaptureCaps {
1045
1114
  maxHeight?: number
1046
1115
  }
1047
1116
 
1117
+ /** Rectangle in pixels of the most recent full screenshot of the same target. */
1118
+ export interface CaptureRegion {
1119
+ x: number
1120
+ y: number
1121
+ width: number
1122
+ height: number
1123
+ }
1124
+
1048
1125
  /** Clipboard image payload encoded as PNG bytes. */
1049
1126
  export interface ClipboardImage {
1050
1127
  /** PNG-encoded image bytes. */
@@ -1100,7 +1177,7 @@ export declare function cosineSimilarityPairs(vectors: Float64Array, count: numb
1100
1177
  export declare function countTokens(input: string | string[], encoding?: Encoding | undefined | null): number
1101
1178
 
1102
1179
  /**
1103
- * Decode one complete SIXEL control string into a PNG.
1180
+ * Decode one complete SIXEL control string into a PNG on the calling thread.
1104
1181
  *
1105
1182
  * The decoder is deliberately bounded before handing the stream to
1106
1183
  * `icy_sixel`: raster declarations, repeats, and row advances are scanned
@@ -1109,6 +1186,12 @@ export declare function countTokens(input: string | string[], encoding?: Encodin
1109
1186
  */
1110
1187
  export declare function decodeSixelToPng(bytes: Uint8Array): Uint8Array
1111
1188
 
1189
+ /**
1190
+ * Same result as [`decode_sixel_to_png`], but the decode runs on the native
1191
+ * blocking pool instead of the JS thread.
1192
+ */
1193
+ export declare function decodeSixelToPngAsync(bytes: Uint8Array): Promise<Uint8Array>
1194
+
1112
1195
  export interface DesktopCapabilities {
1113
1196
  backend: string
1114
1197
  displayServer?: string
@@ -1121,6 +1204,15 @@ export interface DesktopCapabilities {
1121
1204
  * target and post real input).
1122
1205
  */
1123
1206
  takeover: boolean
1207
+ applications: boolean
1208
+ menus: boolean
1209
+ heldInput: boolean
1210
+ spaces: boolean
1211
+ /**
1212
+ * Native global Escape cancellation while input/control ownership is held.
1213
+ * Wayland requires the host interrupt action instead.
1214
+ */
1215
+ globalEscape: boolean
1124
1216
  capturePermission: string
1125
1217
  inputPermission: string
1126
1218
  axPermission: string
@@ -1138,12 +1230,24 @@ export interface DesktopCapture {
1138
1230
  * unscaled.
1139
1231
  */
1140
1232
  sourceHeight: number
1233
+ /** Dimensions of the full screenshot coordinate frame used by pointer input. */
1234
+ coordinateWidth: number
1235
+ coordinateHeight: number
1236
+ /**
1237
+ * Region in the full screenshot's coordinates; zoom pixels are not input
1238
+ * coordinates.
1239
+ */
1240
+ region?: CaptureRegion
1141
1241
  target: string
1142
1242
  displays: Array<DesktopDisplay>
1143
1243
  backend: string
1144
1244
  displayServer?: string
1145
1245
  }
1146
1246
 
1247
+ export interface DesktopControlState {
1248
+ active: boolean
1249
+ }
1250
+
1147
1251
  /**
1148
1252
  * Monitor geometry in both global logical desktop coordinates and composite
1149
1253
  * screenshot pixels.
@@ -1163,6 +1267,24 @@ export interface DesktopDisplay {
1163
1267
  isPrimary: boolean
1164
1268
  }
1165
1269
 
1270
+ /**
1271
+ * One immediate child of a native application menu. Paths contain the actual
1272
+ * native labels, including ellipses; selection accepts normalized labels.
1273
+ */
1274
+ export interface DesktopMenuItem {
1275
+ title: string
1276
+ path: Array<string>
1277
+ enabled: boolean
1278
+ checked: boolean
1279
+ hasSubmenu: boolean
1280
+ shortcut?: string
1281
+ }
1282
+
1283
+ export interface DesktopObservation {
1284
+ capture: DesktopCapture
1285
+ accessibility: AxSnapshot
1286
+ }
1287
+
1166
1288
  export interface DesktopPoint {
1167
1289
  x: number
1168
1290
  y: number
@@ -1520,16 +1642,27 @@ export interface EnclosingBoundaryOptions {
1520
1642
  }
1521
1643
 
1522
1644
  /**
1523
- * Encode image bytes into a SIXEL escape sequence for terminal rendering.
1645
+ * Encode image bytes into a SIXEL escape sequence for terminal rendering, on
1646
+ * the calling thread.
1524
1647
  *
1525
1648
  * The input image is decoded and resized to the requested pixel dimensions
1526
- * before encoding.
1649
+ * before encoding. Prefer [`encode_sixel_async`] unless the caller cannot
1650
+ * wait: the decode, resize and dither block the JS thread.
1527
1651
  *
1528
1652
  * # Errors
1529
1653
  * Returns an error if decoding, resizing, or SIXEL encoding fails.
1530
1654
  */
1531
1655
  export declare function encodeSixel(bytes: Uint8Array, targetWidthPx: number, targetHeightPx: number): string
1532
1656
 
1657
+ /**
1658
+ * Same result as [`encode_sixel`], but the decode, resize and dither run on
1659
+ * the native blocking pool, so they never stall the JavaScript event loop.
1660
+ *
1661
+ * # Errors
1662
+ * Rejects if decoding, resizing, or SIXEL encoding fails.
1663
+ */
1664
+ export declare function encodeSixelAsync(bytes: Uint8Array, targetWidthPx: number, targetHeightPx: number): Promise<string>
1665
+
1533
1666
  /** Tokenizer encoding to use. */
1534
1667
  export declare enum Encoding {
1535
1668
  /** GPT-4o / o1 / GPT-5 (default). */
@@ -1957,6 +2090,14 @@ export interface HighlightColors {
1957
2090
  deleted?: string
1958
2091
  }
1959
2092
 
2093
+ export interface HoldOptions {
2094
+ /** Duration in seconds, from zero through 100. */
2095
+ duration: number
2096
+ button?: string
2097
+ keys?: Array<string>
2098
+ takeover?: boolean
2099
+ }
2100
+
1960
2101
  /**
1961
2102
  * Convert HTML source to Markdown with optional preprocessing.
1962
2103
  *
@@ -2066,10 +2207,13 @@ export interface IsoProbeResult {
2066
2207
  }
2067
2208
 
2068
2209
  /**
2069
- * Pick the best backend available right now. `preferred` is treated as
2070
- * a hint — see [`pi_iso::resolve`] for the exact priority rules.
2210
+ * Pick the best backend available right now.
2211
+ *
2212
+ * `preferred` is treated as a hint — see [`pi_iso::resolve`] for the exact
2213
+ * priority rules. Backend probes may spawn CLIs, so they run on the native
2214
+ * blocking pool.
2071
2215
  */
2072
- export declare function isoResolve(preferred?: IsoBackendKind | undefined | null): IsoResolveResult
2216
+ export declare function isoResolve(preferred?: IsoBackendKind | undefined | null): Promise<IsoResolveResult>
2073
2217
 
2074
2218
  /** Outcome of [`iso_resolve`]. */
2075
2219
  export interface IsoResolveResult {
@@ -2457,6 +2601,8 @@ export interface PointerOptions {
2457
2601
  button?: string
2458
2602
  count?: number
2459
2603
  modifiers?: Array<string>
2604
+ /** Arbitrary keys held for the duration of a drag. */
2605
+ keys?: Array<string>
2460
2606
  /**
2461
2607
  * Briefly activate the target window and post real input instead of the
2462
2608
  * default background delivery.
@@ -2627,10 +2773,11 @@ export declare function renderMermaidAscii(text: string, options?: MermaidRender
2627
2773
  * Render one snapcompact frame on a libuv worker: print pre-normalized text
2628
2774
  * onto a `size`-wide bitmap and encode it as PNG.
2629
2775
  *
2630
- * The bitmap height hugs the rows the text actually occupies
2631
- * (`usedRows * lineRepeat * cellHeight`), so a partially filled frame never
2632
- * pays for blank padding rows. The glyph grid holds `floor(size/cellWidth) *
2633
- * floor(size/cellHeight/lineRepeat)` characters; input beyond that is ignored.
2776
+ * The bitmap height hugs the rows the text occupies
2777
+ * (`usedRows * lineRepeat * cellHeight`), with a 64px floor for vision
2778
+ * processors that reject smaller dimensions. The glyph grid holds
2779
+ * `floor(size/cellWidth) * floor(size/cellHeight/lineRepeat)` characters;
2780
+ * input beyond that is ignored.
2634
2781
  * Native-cell bitmap-font shapes encode as indexed PNG; stretched bitmap-font
2635
2782
  * shapes (target cell != font cell) encode as RGB. TrueType shapes encode RGB
2636
2783
  * directly from grayscale coverage.
@@ -3108,8 +3255,8 @@ export declare function sliceWithWidth(line: string, startCol: number, length: n
3108
3255
  export interface SnapcompactRenderOptions {
3109
3256
  /**
3110
3257
  * Frame width in pixels; also bounds the grid rows
3111
- * (`floor(size/cellHeight/lineRepeat)`). Output height hugs the rows the
3112
- * text actually uses instead of padding to a square.
3258
+ * (`floor(size/cellHeight/lineRepeat)`). Output height hugs the used rows
3259
+ * with a 64px floor, rather than padding every frame to a square.
3113
3260
  */
3114
3261
  size: number
3115
3262
  /**
@@ -3496,6 +3643,18 @@ export interface VectorTopK {
3496
3643
  */
3497
3644
  export declare function visibleWidth(text: string, tabWidth: number): number
3498
3645
 
3646
+ /**
3647
+ * Parse `options.code` into the shared tree cache on the native blocking
3648
+ * pool.
3649
+ *
3650
+ * [`enclosing_block_boundaries`], [`block_range_at`] and [`node_chain_at`]
3651
+ * are synchronous and parse on the JS thread when their source is not
3652
+ * cached; awaiting this first makes that parse a cache hit. Resolves without
3653
+ * parsing when the language is unrecognized or the source is too large for
3654
+ * the cache to keep.
3655
+ */
3656
+ export declare function warmBlockParse(options: BlockParseOptions): Promise<undefined>
3657
+
3499
3658
  /**
3500
3659
  * Warm syntax grammars, scope matchers, and the regexes of commonly
3501
3660
  * highlighted languages on the native worker pool.
package/native/index.js CHANGED
@@ -58,6 +58,7 @@ export const copyToClipboard = nativeBindings.copyToClipboard ?? missingNativeEx
58
58
  export const cosineSimilarityPairs = nativeBindings.cosineSimilarityPairs ?? missingNativeExport("cosineSimilarityPairs");
59
59
  export const countTokens = nativeBindings.countTokens ?? missingNativeExport("countTokens");
60
60
  export const decodeSixelToPng = nativeBindings.decodeSixelToPng ?? missingNativeExport("decodeSixelToPng");
61
+ export const decodeSixelToPngAsync = nativeBindings.decodeSixelToPngAsync ?? missingNativeExport("decodeSixelToPngAsync");
61
62
  export const detectMacOSAppearance = nativeBindings.detectMacOSAppearance ?? missingNativeExport("detectMacOSAppearance");
62
63
  export const deviceCheckGenerateToken = nativeBindings.deviceCheckGenerateToken ?? missingNativeExport("deviceCheckGenerateToken");
63
64
  export const diffLineRuns = nativeBindings.diffLineRuns ?? missingNativeExport("diffLineRuns");
@@ -70,6 +71,7 @@ export const editGrammar = nativeBindings.editGrammar ?? missingNativeExport("ed
70
71
  export const editInspect = nativeBindings.editInspect ?? missingNativeExport("editInspect");
71
72
  export const enclosingBlockBoundaries = nativeBindings.enclosingBlockBoundaries ?? missingNativeExport("enclosingBlockBoundaries");
72
73
  export const encodeSixel = nativeBindings.encodeSixel ?? missingNativeExport("encodeSixel");
74
+ export const encodeSixelAsync = nativeBindings.encodeSixelAsync ?? missingNativeExport("encodeSixelAsync");
73
75
  export const execReplace = nativeBindings.execReplace ?? missingNativeExport("execReplace");
74
76
  export const executeShell = nativeBindings.executeShell ?? missingNativeExport("executeShell");
75
77
  export const expandWindowsLongPath = nativeBindings.expandWindowsLongPath ?? missingNativeExport("expandWindowsLongPath");
@@ -138,6 +140,7 @@ export const vcsJoinPatches = nativeBindings.vcsJoinPatches ?? missingNativeExpo
138
140
  export const vcsValidateHunkSelections = nativeBindings.vcsValidateHunkSelections ?? missingNativeExport("vcsValidateHunkSelections");
139
141
  export const vectorIndexTopK = nativeBindings.vectorIndexTopK ?? missingNativeExport("vectorIndexTopK");
140
142
  export const visibleWidth = nativeBindings.visibleWidth ?? missingNativeExport("visibleWidth");
143
+ export const warmBlockParse = nativeBindings.warmBlockParse ?? missingNativeExport("warmBlockParse");
141
144
  export const warmHighlighter = nativeBindings.warmHighlighter ?? missingNativeExport("warmHighlighter");
142
145
  export const wrapTextWithAnsi = nativeBindings.wrapTextWithAnsi ?? missingNativeExport("wrapTextWithAnsi");
143
146
 
package/native/vcs.d.ts CHANGED
@@ -81,6 +81,12 @@ export declare function detachGitDir(
81
81
  signal?: AbortSignal,
82
82
  ): Promise<"no-git" | "independent" | "detached">;
83
83
 
84
+ /** Run Git's checkout hook in a new linked worktree and return its output and exit status. */
85
+ export declare function runPostCheckoutHook(
86
+ worktreeRoot: string,
87
+ head: string,
88
+ ): Promise<{ exitCode: number; stdout: Uint8Array; stderr: Uint8Array }>;
89
+
84
90
  /** Join patch fragments, preserving each part's trailing newline. */
85
91
  export declare function joinPatches(parts: string[]): string;
86
92
 
package/native/vcs.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { $ } from "bun";
1
2
  import * as fs from "node:fs";
2
3
  import { loadNative } from "./loader-state.js";
3
4
 
@@ -95,6 +96,19 @@ export function detachGitDir(worktreeRoot, sourceCommonDir, signal) {
95
96
  return api().vcsDetachGitDir(worktreeRoot, sourceCommonDir, signal);
96
97
  }
97
98
 
99
+ /**
100
+ * Run Git's checkout hook in a newly created linked worktree, using the same
101
+ * null old HEAD and branch-switch flag as `git worktree add`.
102
+ */
103
+ export async function runPostCheckoutHook(worktreeRoot, head) {
104
+ const nullOid = "0".repeat(head.length);
105
+ const result = await $`git hook run --ignore-missing post-checkout -- ${nullOid} ${head} 1`
106
+ .cwd(worktreeRoot)
107
+ .quiet()
108
+ .nothrow();
109
+ return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
110
+ }
111
+
98
112
  /** Join patch fragments, preserving each part's trailing newline. */
99
113
  export function joinPatches(parts) {
100
114
  return api().vcsJoinPatches(parts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-natives",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
4
4
  "description": "Native Rust bindings for PDF conversion, audio, WebRTC, grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
5
5
  "type": "module",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
@@ -111,11 +111,11 @@
111
111
  }
112
112
  },
113
113
  "optionalDependencies": {
114
- "@linxiraos/pi-natives-linux-x64": "1.1.25",
115
- "@linxiraos/pi-natives-linux-arm64": "1.1.25",
116
- "@linxiraos/pi-natives-darwin-x64": "1.1.25",
117
- "@linxiraos/pi-natives-darwin-arm64": "1.1.25",
118
- "@linxiraos/pi-natives-win32-x64": "1.1.25",
119
- "@linxiraos/pi-natives-win32-arm64": "1.1.25"
114
+ "@linxiraos/pi-natives-linux-x64": "1.1.27",
115
+ "@linxiraos/pi-natives-linux-arm64": "1.1.27",
116
+ "@linxiraos/pi-natives-darwin-x64": "1.1.27",
117
+ "@linxiraos/pi-natives-darwin-arm64": "1.1.27",
118
+ "@linxiraos/pi-natives-win32-x64": "1.1.27",
119
+ "@linxiraos/pi-natives-win32-arm64": "1.1.27"
120
120
  }
121
121
  }