@oh-my-pi/pi-natives 9.3.1 → 9.6.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.
- package/README.md +10 -13
- package/native/pi_natives.darwin-arm64.node +0 -0
- package/native/pi_natives.darwin-x64.node +0 -0
- package/native/pi_natives.linux-arm64.node +0 -0
- package/native/pi_natives.linux-x64.node +0 -0
- package/native/pi_natives.win32-x64.node +0 -0
- package/package.json +6 -6
- package/src/find/types.ts +31 -0
- package/src/grep/index.ts +37 -295
- package/src/grep/types.ts +59 -19
- package/src/highlight/index.ts +5 -13
- package/src/html/index.ts +5 -35
- package/src/html/types.ts +1 -16
- package/src/image/index.ts +52 -73
- package/src/index.ts +20 -93
- package/src/native.ts +159 -0
- package/src/request-options.ts +94 -0
- package/src/text/index.ts +16 -24
- package/src/grep/file-reader.ts +0 -51
- package/src/grep/filters.ts +0 -77
- package/src/grep/worker.ts +0 -212
- package/src/html/worker.ts +0 -40
- package/src/image/types.ts +0 -52
- package/src/image/worker.ts +0 -152
- package/src/pool.ts +0 -362
- package/src/wasix.ts +0 -1745
- package/src/worker-resolver.ts +0 -9
- package/wasm/pi_natives.d.ts +0 -148
- package/wasm/pi_natives.js +0 -891
- package/wasm/pi_natives_bg.wasm +0 -0
- package/wasm/pi_natives_bg.wasm.d.ts +0 -32
package/src/html/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Types for HTML to Markdown
|
|
2
|
+
* Types for HTML to Markdown conversion.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export interface HtmlToMarkdownOptions {
|
|
@@ -8,18 +8,3 @@ export interface HtmlToMarkdownOptions {
|
|
|
8
8
|
/** Skip images during conversion */
|
|
9
9
|
skipImages?: boolean;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
export type HtmlRequest =
|
|
13
|
-
| { type: "init"; id: number }
|
|
14
|
-
| { type: "destroy" }
|
|
15
|
-
| {
|
|
16
|
-
type: "convert";
|
|
17
|
-
id: number;
|
|
18
|
-
html: string;
|
|
19
|
-
options?: HtmlToMarkdownOptions;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type HtmlResponse =
|
|
23
|
-
| { type: "ready"; id: number }
|
|
24
|
-
| { type: "error"; id: number; error: string }
|
|
25
|
-
| { type: "converted"; id: number; markdown: string };
|
package/src/image/index.ts
CHANGED
|
@@ -1,48 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* All heavy lifting happens in a worker thread to avoid blocking the main thread.
|
|
5
|
-
* Uses transferable ArrayBuffers to avoid copying image data.
|
|
2
|
+
* Image processing via native bindings.
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
5
|
+
import { type NativePhotonImage, native } from "../native";
|
|
6
|
+
|
|
7
|
+
const images = new Map<number, NativePhotonImage>();
|
|
8
|
+
let nextHandle = 1;
|
|
9
|
+
|
|
10
|
+
function registerImage(image: NativePhotonImage): number {
|
|
11
|
+
const handle = nextHandle++;
|
|
12
|
+
images.set(handle, image);
|
|
13
|
+
return handle;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getImage(handle: number): NativePhotonImage {
|
|
17
|
+
const image = images.get(handle);
|
|
18
|
+
if (!image) {
|
|
19
|
+
throw new Error("Image already freed");
|
|
20
|
+
}
|
|
21
|
+
return image;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const SamplingFilter = native.SamplingFilter;
|
|
25
|
+
export type SamplingFilter = (typeof SamplingFilter)[keyof typeof SamplingFilter];
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Image handle for async operations.
|
|
29
|
-
* Must call free() when done to release WASM memory.
|
|
30
29
|
*/
|
|
31
30
|
export class PhotonImage {
|
|
32
31
|
#handle: number;
|
|
33
|
-
#width: number;
|
|
34
|
-
#height: number;
|
|
35
32
|
#freed = false;
|
|
36
33
|
|
|
37
|
-
private constructor(handle: number
|
|
34
|
+
private constructor(handle: number) {
|
|
38
35
|
this.#handle = handle;
|
|
39
|
-
this.#width = width;
|
|
40
|
-
this.#height = height;
|
|
41
36
|
}
|
|
42
37
|
|
|
43
38
|
/** @internal */
|
|
44
|
-
static _create(handle: number
|
|
45
|
-
|
|
39
|
+
static _create(handle: number): PhotonImage {
|
|
40
|
+
if (!images.has(handle)) {
|
|
41
|
+
throw new Error("Invalid image handle");
|
|
42
|
+
}
|
|
43
|
+
return new PhotonImage(handle);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load an image from encoded bytes (PNG, JPEG, WebP, GIF).
|
|
48
|
+
*/
|
|
49
|
+
static async new_from_byteslice(bytes: Uint8Array): Promise<PhotonImage> {
|
|
50
|
+
const image = await native.PhotonImage.newFromByteslice(bytes);
|
|
51
|
+
const handle = registerImage(image);
|
|
52
|
+
return new PhotonImage(handle);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
/** @internal */
|
|
@@ -51,56 +58,36 @@ export class PhotonImage {
|
|
|
51
58
|
return this.#handle;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*/
|
|
58
|
-
static async new_from_byteslice(bytes: Uint8Array): Promise<PhotonImage> {
|
|
59
|
-
const response = await pool.request<Extract<ImageResponse, { type: "loaded" }>>(
|
|
60
|
-
{ type: "load", bytes },
|
|
61
|
-
{
|
|
62
|
-
transfer: [bytes.buffer],
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
return new PhotonImage(response.handle, response.width, response.height);
|
|
61
|
+
#native(): NativePhotonImage {
|
|
62
|
+
if (this.#freed) throw new Error("Image already freed");
|
|
63
|
+
return getImage(this.#handle);
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
/** Get image width in pixels. */
|
|
69
67
|
get_width(): number {
|
|
70
|
-
return this.#
|
|
68
|
+
return this.#native().getWidth();
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
/** Get image height in pixels. */
|
|
74
72
|
get_height(): number {
|
|
75
|
-
return this.#
|
|
73
|
+
return this.#native().getHeight();
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
/** Export as PNG bytes. */
|
|
79
77
|
async get_bytes(): Promise<Uint8Array> {
|
|
80
|
-
|
|
81
|
-
const response = await pool.request<Extract<ImageResponse, { type: "bytes" }>>({
|
|
82
|
-
type: "get_png",
|
|
83
|
-
handle: this.#handle,
|
|
84
|
-
});
|
|
85
|
-
return response.bytes;
|
|
78
|
+
return this.#native().getBytes();
|
|
86
79
|
}
|
|
87
80
|
|
|
88
81
|
/** Export as JPEG bytes with specified quality (0-100). */
|
|
89
82
|
async get_bytes_jpeg(quality: number): Promise<Uint8Array> {
|
|
90
|
-
|
|
91
|
-
const response = await pool.request<Extract<ImageResponse, { type: "bytes" }>>({
|
|
92
|
-
type: "get_jpeg",
|
|
93
|
-
handle: this.#handle,
|
|
94
|
-
quality,
|
|
95
|
-
});
|
|
96
|
-
return response.bytes;
|
|
83
|
+
return this.#native().getBytesJpeg(quality);
|
|
97
84
|
}
|
|
98
85
|
|
|
99
|
-
/** Release
|
|
86
|
+
/** Release native resources. */
|
|
100
87
|
free() {
|
|
101
88
|
if (this.#freed) return;
|
|
102
89
|
this.#freed = true;
|
|
103
|
-
|
|
90
|
+
images.delete(this.#handle);
|
|
104
91
|
}
|
|
105
92
|
|
|
106
93
|
/** Alias for free() to support using-declarations. */
|
|
@@ -114,21 +101,13 @@ export class PhotonImage {
|
|
|
114
101
|
* Returns a new PhotonImage (original is not modified).
|
|
115
102
|
*/
|
|
116
103
|
export async function resize(image: PhotonImage, width: number, height: number, filter: number): Promise<PhotonImage> {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
width,
|
|
122
|
-
height,
|
|
123
|
-
filter,
|
|
124
|
-
});
|
|
125
|
-
return PhotonImage._create(response.handle, response.width, response.height);
|
|
104
|
+
const nativeImage = getImage(image._getHandle());
|
|
105
|
+
const resized = await nativeImage.resize(width, height, filter);
|
|
106
|
+
const handle = registerImage(resized);
|
|
107
|
+
return PhotonImage._create(handle);
|
|
126
108
|
}
|
|
127
109
|
|
|
128
110
|
/**
|
|
129
|
-
* Terminate
|
|
130
|
-
* Call this when shutting down to clean up resources.
|
|
111
|
+
* Terminate image resources (no-op for native bindings).
|
|
131
112
|
*/
|
|
132
|
-
export function terminate(): void {
|
|
133
|
-
pool.terminate();
|
|
134
|
-
}
|
|
113
|
+
export function terminate(): void {}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Native utilities powered by
|
|
2
|
+
* Native utilities powered by N-API.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import * as fs from "node:fs/promises";
|
|
6
5
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
6
|
+
import type { FindMatch, FindOptions, FindResult } from "./find/types";
|
|
7
|
+
import { native } from "./native";
|
|
8
8
|
|
|
9
9
|
// =============================================================================
|
|
10
10
|
// Grep (ripgrep-based regex search)
|
|
@@ -12,52 +12,24 @@ import { globPaths } from "@oh-my-pi/pi-utils";
|
|
|
12
12
|
|
|
13
13
|
export {
|
|
14
14
|
type ContextLine,
|
|
15
|
+
type FuzzyFindMatch,
|
|
16
|
+
type FuzzyFindOptions,
|
|
17
|
+
type FuzzyFindResult,
|
|
18
|
+
fuzzyFind,
|
|
15
19
|
type GrepMatch,
|
|
16
20
|
type GrepOptions,
|
|
17
21
|
type GrepResult,
|
|
18
22
|
type GrepSummary,
|
|
19
23
|
grep,
|
|
20
|
-
grepDirect,
|
|
21
|
-
grepPool,
|
|
22
24
|
hasMatch,
|
|
23
25
|
searchContent,
|
|
24
|
-
terminate,
|
|
25
26
|
} from "./grep/index";
|
|
26
27
|
|
|
27
|
-
// =============================================================================
|
|
28
|
-
// WASI implementation
|
|
29
|
-
// =============================================================================
|
|
30
|
-
|
|
31
|
-
export { WASI1, WASIError, WASIExitError, type WASIOptions } from "./wasix";
|
|
32
|
-
|
|
33
28
|
// =============================================================================
|
|
34
29
|
// Find (file discovery)
|
|
35
30
|
// =============================================================================
|
|
36
31
|
|
|
37
|
-
export
|
|
38
|
-
/** Glob pattern to match (e.g., `*.ts`) */
|
|
39
|
-
pattern: string;
|
|
40
|
-
/** Directory to search */
|
|
41
|
-
path: string;
|
|
42
|
-
/** Filter by file type: "file", "dir", or "symlink" */
|
|
43
|
-
fileType?: "file" | "dir" | "symlink";
|
|
44
|
-
/** Include hidden files (default: false) */
|
|
45
|
-
hidden?: boolean;
|
|
46
|
-
/** Maximum number of results */
|
|
47
|
-
maxResults?: number;
|
|
48
|
-
/** Respect .gitignore files (default: true) */
|
|
49
|
-
gitignore?: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface FindMatch {
|
|
53
|
-
path: string;
|
|
54
|
-
fileType: "file" | "dir" | "symlink";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface FindResult {
|
|
58
|
-
matches: FindMatch[];
|
|
59
|
-
totalMatches: number;
|
|
60
|
-
}
|
|
32
|
+
export type { FindMatch, FindOptions, FindResult } from "./find/types";
|
|
61
33
|
|
|
62
34
|
/**
|
|
63
35
|
* Find files matching a glob pattern.
|
|
@@ -70,59 +42,19 @@ export async function find(options: FindOptions, onMatch?: (match: FindMatch) =>
|
|
|
70
42
|
// Convert simple patterns to recursive globs if needed
|
|
71
43
|
const globPattern = pattern.includes("/") || pattern.startsWith("**") ? pattern : `**/${pattern}`;
|
|
72
44
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for (const p of paths) {
|
|
84
|
-
if (matches.length >= maxResults) {
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const normalizedPath = p.replace(/\\/g, "/");
|
|
89
|
-
if (!normalizedPath) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let stats: Awaited<ReturnType<typeof fs.lstat>>;
|
|
94
|
-
try {
|
|
95
|
-
stats = await fs.lstat(path.join(searchPath, normalizedPath));
|
|
96
|
-
} catch {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const fileType: "file" | "dir" | "symlink" = stats.isSymbolicLink()
|
|
101
|
-
? "symlink"
|
|
102
|
-
: stats.isDirectory()
|
|
103
|
-
? "dir"
|
|
104
|
-
: "file";
|
|
105
|
-
|
|
106
|
-
if (options.fileType && options.fileType !== fileType) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const match: FindMatch = {
|
|
111
|
-
path: normalizedPath,
|
|
112
|
-
fileType,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
matches.push(match);
|
|
116
|
-
onMatch?.(match);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
matches,
|
|
121
|
-
totalMatches: matches.length,
|
|
122
|
-
};
|
|
45
|
+
return native.find(
|
|
46
|
+
{
|
|
47
|
+
...options,
|
|
48
|
+
path: searchPath,
|
|
49
|
+
pattern: globPattern,
|
|
50
|
+
hidden: options.hidden ?? false,
|
|
51
|
+
gitignore: options.gitignore ?? true,
|
|
52
|
+
},
|
|
53
|
+
onMatch,
|
|
54
|
+
);
|
|
123
55
|
}
|
|
124
56
|
|
|
125
|
-
//
|
|
57
|
+
// ===================================================== ========================
|
|
126
58
|
// Image processing (photon-compatible API)
|
|
127
59
|
// =============================================================================
|
|
128
60
|
|
|
@@ -164,11 +96,6 @@ export {
|
|
|
164
96
|
export {
|
|
165
97
|
type HtmlToMarkdownOptions,
|
|
166
98
|
htmlToMarkdown,
|
|
167
|
-
terminate as terminateHtmlWorker,
|
|
168
99
|
} from "./html/index";
|
|
169
100
|
|
|
170
|
-
|
|
171
|
-
// Worker Pool (shared infrastructure)
|
|
172
|
-
// =============================================================================
|
|
173
|
-
|
|
174
|
-
export { type BaseRequest, type BaseResponse, type RequestOptions, WorkerPool, type WorkerPoolOptions } from "./pool";
|
|
101
|
+
export type { RequestOptions } from "./request-options";
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { FindMatch, FindOptions, FindResult } from "./find/types";
|
|
4
|
+
import type {
|
|
5
|
+
FuzzyFindOptions,
|
|
6
|
+
FuzzyFindResult,
|
|
7
|
+
GrepOptions,
|
|
8
|
+
GrepResult,
|
|
9
|
+
SearchOptions,
|
|
10
|
+
SearchResult,
|
|
11
|
+
} from "./grep/types";
|
|
12
|
+
import type { HighlightColors } from "./highlight/index";
|
|
13
|
+
import type { HtmlToMarkdownOptions } from "./html/types";
|
|
14
|
+
import type { ExtractSegmentsResult, SliceWithWidthResult, TextInput } from "./text/index";
|
|
15
|
+
|
|
16
|
+
export interface NativePhotonImage {
|
|
17
|
+
getWidth(): number;
|
|
18
|
+
getHeight(): number;
|
|
19
|
+
getBytes(): Promise<Uint8Array>;
|
|
20
|
+
getBytesJpeg(quality: number): Promise<Uint8Array>;
|
|
21
|
+
getBytesWebp(): Promise<Uint8Array>;
|
|
22
|
+
getBytesGif(): Promise<Uint8Array>;
|
|
23
|
+
resize(width: number, height: number, filter: number): Promise<NativePhotonImage>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface NativePhotonImageConstructor {
|
|
27
|
+
newFromByteslice(bytes: Uint8Array): Promise<NativePhotonImage>;
|
|
28
|
+
prototype: NativePhotonImage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NativeSamplingFilter {
|
|
32
|
+
Nearest: 1;
|
|
33
|
+
Triangle: 2;
|
|
34
|
+
CatmullRom: 3;
|
|
35
|
+
Gaussian: 4;
|
|
36
|
+
Lanczos3: 5;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
import type { GrepMatch } from "./grep/types";
|
|
40
|
+
|
|
41
|
+
export interface NativeBindings {
|
|
42
|
+
find(options: FindOptions, onMatch?: (match: FindMatch) => void): Promise<FindResult>;
|
|
43
|
+
fuzzyFind(options: FuzzyFindOptions): Promise<FuzzyFindResult>;
|
|
44
|
+
grep(options: GrepOptions, onMatch?: (match: GrepMatch) => void): Promise<GrepResult>;
|
|
45
|
+
search(content: string | Uint8Array, options: SearchOptions): SearchResult;
|
|
46
|
+
hasMatch(
|
|
47
|
+
content: string | Uint8Array,
|
|
48
|
+
pattern: string | Uint8Array,
|
|
49
|
+
ignoreCase: boolean,
|
|
50
|
+
multiline: boolean,
|
|
51
|
+
): boolean;
|
|
52
|
+
htmlToMarkdown(html: string, options?: HtmlToMarkdownOptions | null): Promise<string>;
|
|
53
|
+
highlightCode(code: string, lang: string | null | undefined, colors: HighlightColors): string;
|
|
54
|
+
supportsLanguage(lang: string): boolean;
|
|
55
|
+
getSupportedLanguages(): string[];
|
|
56
|
+
SamplingFilter: NativeSamplingFilter;
|
|
57
|
+
PhotonImage: NativePhotonImageConstructor;
|
|
58
|
+
visibleWidth(text: TextInput): number;
|
|
59
|
+
truncateToWidth(text: TextInput, maxWidth: number, ellipsis: TextInput, pad: boolean): string;
|
|
60
|
+
sliceWithWidth(line: TextInput, startCol: number, length: number, strict: boolean): SliceWithWidthResult;
|
|
61
|
+
extractSegments(
|
|
62
|
+
line: TextInput,
|
|
63
|
+
beforeEnd: number,
|
|
64
|
+
afterStart: number,
|
|
65
|
+
afterLen: number,
|
|
66
|
+
strictAfter: boolean,
|
|
67
|
+
): ExtractSegmentsResult;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const require = createRequire(import.meta.url);
|
|
71
|
+
const platformTag = `${process.platform}-${process.arch}`;
|
|
72
|
+
const nativeDir = path.join(import.meta.dir, "..", "native");
|
|
73
|
+
const repoRoot = path.join(import.meta.dir, "..", "..", "..");
|
|
74
|
+
const execDir = path.dirname(process.execPath);
|
|
75
|
+
|
|
76
|
+
const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
|
|
77
|
+
|
|
78
|
+
const candidates = [
|
|
79
|
+
path.join(nativeDir, `pi_natives.${platformTag}.node`),
|
|
80
|
+
path.join(nativeDir, "pi_natives.node"),
|
|
81
|
+
path.join(execDir, `pi_natives.${platformTag}.node`),
|
|
82
|
+
path.join(execDir, "pi_natives.node"),
|
|
83
|
+
path.join(repoRoot, "target", "release", "pi_natives.node"),
|
|
84
|
+
path.join(repoRoot, "crates", "pi-natives", "target", "release", "pi_natives.node"),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function loadNative(): NativeBindings {
|
|
88
|
+
const errors: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (const candidate of candidates) {
|
|
91
|
+
try {
|
|
92
|
+
const bindings = require(candidate) as NativeBindings;
|
|
93
|
+
validateNative(bindings, candidate);
|
|
94
|
+
return bindings;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
errors.push(`${candidate}: ${message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if this is an unsupported platform
|
|
102
|
+
if (!SUPPORTED_PLATFORMS.includes(platformTag)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Unsupported platform: ${platformTag}\n` +
|
|
105
|
+
`Supported platforms: ${SUPPORTED_PLATFORMS.join(", ")}\n` +
|
|
106
|
+
"If you need support for this platform, please open an issue.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const details = errors.map(error => `- ${error}`).join("\n");
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Failed to load pi_natives native addon for ${platformTag}.\n\n` +
|
|
113
|
+
`Tried:\n${details}\n\n` +
|
|
114
|
+
"If installed via npm/bun, try reinstalling: bun install @oh-my-pi/pi-natives\n" +
|
|
115
|
+
"If developing locally, build with: bun --cwd=packages/natives run build:native",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateNative(bindings: NativeBindings, source: string): void {
|
|
120
|
+
const missing: string[] = [];
|
|
121
|
+
const checkFn = (name: keyof NativeBindings) => {
|
|
122
|
+
if (typeof bindings[name] !== "function") {
|
|
123
|
+
missing.push(name);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
checkFn("find");
|
|
128
|
+
checkFn("fuzzyFind");
|
|
129
|
+
checkFn("grep");
|
|
130
|
+
checkFn("search");
|
|
131
|
+
checkFn("hasMatch");
|
|
132
|
+
checkFn("htmlToMarkdown");
|
|
133
|
+
checkFn("highlightCode");
|
|
134
|
+
checkFn("supportsLanguage");
|
|
135
|
+
checkFn("getSupportedLanguages");
|
|
136
|
+
checkFn("visibleWidth");
|
|
137
|
+
checkFn("truncateToWidth");
|
|
138
|
+
checkFn("sliceWithWidth");
|
|
139
|
+
checkFn("extractSegments");
|
|
140
|
+
|
|
141
|
+
if (!bindings.PhotonImage?.newFromByteslice) {
|
|
142
|
+
missing.push("PhotonImage.newFromByteslice");
|
|
143
|
+
}
|
|
144
|
+
if (!bindings.PhotonImage?.prototype?.resize) {
|
|
145
|
+
missing.push("PhotonImage.resize");
|
|
146
|
+
}
|
|
147
|
+
if (!bindings.SamplingFilter || typeof bindings.SamplingFilter.Lanczos3 !== "number") {
|
|
148
|
+
missing.push("SamplingFilter");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (missing.length) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Native addon missing exports (${source}). Missing: ${missing.join(", ")}. ` +
|
|
154
|
+
"Rebuild with `bun --cwd=packages/natives run build:native`.",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const native = loadNative();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export interface RequestOptions {
|
|
2
|
+
timeoutMs?: number;
|
|
3
|
+
signal?: AbortSignal;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function abortReason(signal?: AbortSignal): Error {
|
|
7
|
+
if (!signal || !signal.reason) return new Error("Request aborted");
|
|
8
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
9
|
+
return new Error("Request aborted", { cause: signal.reason });
|
|
10
|
+
}
|
|
11
|
+
export async function wrapRequestOptions<T>(fn: () => Promise<T>, options?: RequestOptions): Promise<T> {
|
|
12
|
+
const timeoutMs = options?.timeoutMs ?? 0;
|
|
13
|
+
const signal = options?.signal;
|
|
14
|
+
|
|
15
|
+
// Fast path: no timeout + no signal
|
|
16
|
+
if (!signal && timeoutMs <= 0) return fn();
|
|
17
|
+
|
|
18
|
+
// If already aborted, fail immediately
|
|
19
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
20
|
+
|
|
21
|
+
// If we only have an abort signal and no timeout, keep it simple.
|
|
22
|
+
if (signal && timeoutMs <= 0) {
|
|
23
|
+
return withAbortSignal(fn, signal);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return withTimeoutAndOptionalAbort(fn, timeoutMs, signal);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function withAbortSignal<T>(fn: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
|
30
|
+
return new Promise<T>((resolve, reject) => {
|
|
31
|
+
const onAbort = () => {
|
|
32
|
+
signal.removeEventListener("abort", onAbort);
|
|
33
|
+
reject(abortReason(signal));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
37
|
+
|
|
38
|
+
// If it races and aborts right after addEventListener, `once` handles it,
|
|
39
|
+
// but we still want to short-circuit.
|
|
40
|
+
if (signal.aborted) return onAbort();
|
|
41
|
+
|
|
42
|
+
fn().then(
|
|
43
|
+
v => {
|
|
44
|
+
signal.removeEventListener("abort", onAbort);
|
|
45
|
+
resolve(v);
|
|
46
|
+
},
|
|
47
|
+
err => {
|
|
48
|
+
signal.removeEventListener("abort", onAbort);
|
|
49
|
+
reject(err);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function withTimeoutAndOptionalAbort<T>(fn: () => Promise<T>, timeoutMs: number, signal?: AbortSignal): Promise<T> {
|
|
56
|
+
return new Promise<T>((resolve, reject) => {
|
|
57
|
+
let settled = false;
|
|
58
|
+
|
|
59
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
60
|
+
const cleanup = () => {
|
|
61
|
+
if (timeoutId) {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
timeoutId = undefined;
|
|
64
|
+
}
|
|
65
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const settle = (ok: boolean, value: any) => {
|
|
69
|
+
if (settled) return;
|
|
70
|
+
settled = true;
|
|
71
|
+
cleanup();
|
|
72
|
+
ok ? resolve(value as T) : reject(value);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const onAbort = () => settle(false, abortReason(signal!));
|
|
76
|
+
|
|
77
|
+
// timeout
|
|
78
|
+
timeoutId = setTimeout(() => {
|
|
79
|
+
settle(false, new Error(`Request timed out after ${timeoutMs}ms`));
|
|
80
|
+
}, timeoutMs);
|
|
81
|
+
|
|
82
|
+
// abort (optional)
|
|
83
|
+
if (signal) {
|
|
84
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
85
|
+
if (signal.aborted) return onAbort();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// run
|
|
89
|
+
fn().then(
|
|
90
|
+
v => settle(true, v),
|
|
91
|
+
err => settle(false, err),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
}
|
package/src/text/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ANSI-aware text utilities powered by
|
|
2
|
+
* ANSI-aware text utilities powered by native bindings.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { native } from "../native";
|
|
6
6
|
|
|
7
7
|
export interface SliceWithWidthResult {
|
|
8
8
|
text: string;
|
|
@@ -16,49 +16,41 @@ export interface ExtractSegmentsResult {
|
|
|
16
16
|
afterWidth: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
type
|
|
20
|
-
visible_width: (text: string) => number;
|
|
21
|
-
truncate_to_width: (text: string, maxWidth: number, ellipsis: string, pad: boolean) => string;
|
|
22
|
-
slice_with_width: (line: string, startCol: number, length: number, strict: boolean) => SliceWithWidthResult;
|
|
23
|
-
extract_segments: (
|
|
24
|
-
line: string,
|
|
25
|
-
beforeEnd: number,
|
|
26
|
-
afterStart: number,
|
|
27
|
-
afterLen: number,
|
|
28
|
-
strictAfter: boolean,
|
|
29
|
-
) => ExtractSegmentsResult;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const wasmText = wasm as WasmTextExports;
|
|
19
|
+
export type TextInput = string | Uint8Array;
|
|
33
20
|
|
|
34
21
|
/** Compute the visible width of a string, ignoring ANSI codes. */
|
|
35
|
-
export function visibleWidth(text:
|
|
36
|
-
return
|
|
22
|
+
export function visibleWidth(text: TextInput): number {
|
|
23
|
+
return native.visibleWidth(text);
|
|
37
24
|
}
|
|
38
25
|
|
|
39
26
|
/**
|
|
40
27
|
* Truncate a string to a visible width, preserving ANSI codes.
|
|
41
28
|
*/
|
|
42
|
-
export function truncateToWidth(text:
|
|
43
|
-
return
|
|
29
|
+
export function truncateToWidth(text: TextInput, maxWidth: number, ellipsis: TextInput = "…", pad = false): string {
|
|
30
|
+
return native.truncateToWidth(text, maxWidth, ellipsis, pad);
|
|
44
31
|
}
|
|
45
32
|
|
|
46
33
|
/**
|
|
47
34
|
* Slice a range of visible columns from a line.
|
|
48
35
|
*/
|
|
49
|
-
export function sliceWithWidth(
|
|
50
|
-
|
|
36
|
+
export function sliceWithWidth(
|
|
37
|
+
line: TextInput,
|
|
38
|
+
startCol: number,
|
|
39
|
+
length: number,
|
|
40
|
+
strict = false,
|
|
41
|
+
): SliceWithWidthResult {
|
|
42
|
+
return native.sliceWithWidth(line, startCol, length, strict);
|
|
51
43
|
}
|
|
52
44
|
|
|
53
45
|
/**
|
|
54
46
|
* Extract before/after segments around an overlay region.
|
|
55
47
|
*/
|
|
56
48
|
export function extractSegments(
|
|
57
|
-
line:
|
|
49
|
+
line: TextInput,
|
|
58
50
|
beforeEnd: number,
|
|
59
51
|
afterStart: number,
|
|
60
52
|
afterLen: number,
|
|
61
53
|
strictAfter = false,
|
|
62
54
|
): ExtractSegmentsResult {
|
|
63
|
-
return
|
|
55
|
+
return native.extractSegments(line, beforeEnd, afterStart, afterLen, strictAfter);
|
|
64
56
|
}
|