@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/src/html/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Types for HTML to Markdown worker communication.
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 };
@@ -1,48 +1,55 @@
1
1
  /**
2
- * Async image processing via worker.
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 { WorkerPool } from "../pool";
9
- import { resolveWorkerSpecifier } from "../worker-resolver";
10
- import type { ImageRequest, ImageResponse } from "./types";
11
-
12
- // Re-export the enum for filter selection
13
- export { SamplingFilter } from "../../wasm/pi_natives";
14
-
15
- const pool = new WorkerPool<ImageRequest, ImageResponse>({
16
- createWorker: () =>
17
- new Worker(
18
- resolveWorkerSpecifier({
19
- compiled: "./packages/natives/src/image/worker.ts",
20
- dev: new URL("./worker.ts", import.meta.url),
21
- }),
22
- ),
23
- maxWorkers: 1,
24
- idleTimeoutMs: 0, // Keep alive - stateful (image handles)
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, width: number, height: 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, width: number, height: number): PhotonImage {
45
- return new PhotonImage(handle, width, height);
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
- * Load an image from encoded bytes (PNG, JPEG, WebP, GIF).
56
- * The bytes are transferred to the worker (zero-copy).
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.#width;
68
+ return this.#native().getWidth();
71
69
  }
72
70
 
73
71
  /** Get image height in pixels. */
74
72
  get_height(): number {
75
- return this.#height;
73
+ return this.#native().getHeight();
76
74
  }
77
75
 
78
76
  /** Export as PNG bytes. */
79
77
  async get_bytes(): Promise<Uint8Array> {
80
- if (this.#freed) throw new Error("Image already freed");
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
- if (this.#freed) throw new Error("Image already freed");
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 WASM memory. Must be called when done with the image. */
86
+ /** Release native resources. */
100
87
  free() {
101
88
  if (this.#freed) return;
102
89
  this.#freed = true;
103
- pool.request({ type: "free", handle: this.#handle }).catch(() => {});
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 handle = image._getHandle();
118
- const response = await pool.request<Extract<ImageResponse, { type: "resized" }>>({
119
- type: "resize",
120
- handle,
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 the image worker.
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 WASM.
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 { globPaths } from "@oh-my-pi/pi-utils";
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 interface FindOptions {
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
- const paths = await globPaths(globPattern, {
74
- cwd: searchPath,
75
- dot: options.hidden ?? false,
76
- onlyFiles: options.fileType === "file",
77
- gitignore: options.gitignore ?? true,
78
- });
79
-
80
- const matches: FindMatch[] = [];
81
- const maxResults = options.maxResults ?? Number.MAX_SAFE_INTEGER;
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 WASM.
2
+ * ANSI-aware text utilities powered by native bindings.
3
3
  */
4
4
 
5
- import * as wasm from "../../wasm/pi_natives";
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 WasmTextExports = typeof wasm & {
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: string): number {
36
- return wasmText.visible_width(text);
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: string, maxWidth: number, ellipsis = "…", pad = false): string {
43
- return wasmText.truncate_to_width(text, maxWidth, ellipsis, pad);
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(line: string, startCol: number, length: number, strict = false): SliceWithWidthResult {
50
- return wasmText.slice_with_width(line, startCol, length, strict);
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: string,
49
+ line: TextInput,
58
50
  beforeEnd: number,
59
51
  afterStart: number,
60
52
  afterLen: number,
61
53
  strictAfter = false,
62
54
  ): ExtractSegmentsResult {
63
- return wasmText.extract_segments(line, beforeEnd, afterStart, afterLen, strictAfter);
55
+ return native.extractSegments(line, beforeEnd, afterStart, afterLen, strictAfter);
64
56
  }