@nghyane/arcane-natives 0.1.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 ADDED
@@ -0,0 +1,61 @@
1
+ # @nghyane/arcane-natives
2
+
3
+ Native Rust functionality via N-API.
4
+
5
+ ## What's Inside
6
+
7
+ - **Grep**: Regex-based search powered by ripgrep's engine with native file walking and matching
8
+ - **Find**: Glob-based file/directory discovery with gitignore support (pure TypeScript via `globPaths`)
9
+ - **Image**: Image processing via photon-rs (resize, format conversion) exposed through N-API
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { grep, find, PhotonImage, SamplingFilter, ImageFormat } from "@nghyane/arcane-natives";
15
+
16
+ // Grep for a pattern
17
+ const results = await grep({
18
+ pattern: "TODO",
19
+ path: "/path/to/project",
20
+ glob: "*.ts",
21
+ context: 2,
22
+ });
23
+
24
+ // Find files
25
+ const files = await find({
26
+ pattern: "*.rs",
27
+ path: "/path/to/project",
28
+ fileType: "file",
29
+ });
30
+
31
+ // Image processing
32
+ const image = await PhotonImage.parse(bytes);
33
+ const resized = await image.resize(800, 600, SamplingFilter.Lanczos3);
34
+ const pngBytes = await resized.encode(ImageFormat.PNG, 100);
35
+ ```
36
+
37
+ ## Building
38
+
39
+ ```bash
40
+ # Build native addon from workspace root (requires Rust)
41
+ bun run build:native
42
+
43
+ # Type check
44
+ bun run check
45
+ ```
46
+
47
+ ## Architecture
48
+
49
+ ```
50
+ crates/arcane-natives/ # Rust source (workspace member)
51
+ src/lib.rs # N-API exports
52
+ src/image.rs # Image processing (photon-rs)
53
+ Cargo.toml # Rust dependencies
54
+ native/ # Native addon binaries
55
+ arcane_natives.<platform>-<arch>-modern.node # x64 modern ISA (AVX2)
56
+ arcane_natives.<platform>-<arch>-baseline.node # x64 baseline ISA
57
+ arcane_natives.<platform>-<arch>.node # non-x64 build artifact
58
+ src/ # TypeScript wrappers
59
+ native.ts # Native addon loader
60
+ index.ts # Public API
61
+ ```
package/package.json ADDED
@@ -0,0 +1,163 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@nghyane/arcane-natives",
4
+ "version": "0.1.0",
5
+ "description": "Native Rust bindings for grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
6
+ "homepage": "https://github.com/nghyane/arcane",
7
+ "author": "Can Bölük",
8
+ "license": "GPL-3.0-or-later",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/nghyane/arcane.git",
15
+ "directory": "packages/natives"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/nghyane/arcane/issues"
19
+ },
20
+ "keywords": [
21
+ "napi",
22
+ "rust",
23
+ "native",
24
+ "grep",
25
+ "text-processing",
26
+ "clipboard",
27
+ "image",
28
+ "pty",
29
+ "shell",
30
+ "syntax-highlighting"
31
+ ],
32
+ "main": "./src/index.ts",
33
+ "types": "./src/index.ts",
34
+ "scripts": {
35
+ "build:native": "bun scripts/build-native.ts",
36
+ "dev:native": "bun scripts/build-native.ts --dev",
37
+ "embed:native": "bun scripts/embed-native.ts",
38
+ "check": "biome check . && tsgo -p tsconfig.json",
39
+ "fix": "biome check --write --unsafe .",
40
+ "test": "bun run build:native && bun test",
41
+ "bench": "bun bench/grep.ts"
42
+ },
43
+ "dependencies": {
44
+ "@nghyane/arcane-utils": "workspace:*"
45
+ },
46
+ "devDependencies": {
47
+ "@types/bun": "^1.3.9"
48
+ },
49
+ "engines": {
50
+ "bun": ">=1.3.7"
51
+ },
52
+ "files": [
53
+ "src",
54
+ "native",
55
+ "README.md"
56
+ ],
57
+ "exports": {
58
+ ".": {
59
+ "types": "./src/index.ts",
60
+ "import": "./src/index.ts"
61
+ },
62
+ "./*": {
63
+ "types": "./src/*.ts",
64
+ "import": "./src/*.ts"
65
+ },
66
+ "./clipboard": {
67
+ "types": "./src/clipboard/index.ts",
68
+ "import": "./src/clipboard/index.ts"
69
+ },
70
+ "./clipboard/*": {
71
+ "types": "./src/clipboard/*.ts",
72
+ "import": "./src/clipboard/*.ts"
73
+ },
74
+ "./glob": {
75
+ "types": "./src/glob/index.ts",
76
+ "import": "./src/glob/index.ts"
77
+ },
78
+ "./glob/*": {
79
+ "types": "./src/glob/*.ts",
80
+ "import": "./src/glob/*.ts"
81
+ },
82
+ "./grep": {
83
+ "types": "./src/grep/index.ts",
84
+ "import": "./src/grep/index.ts"
85
+ },
86
+ "./grep/*": {
87
+ "types": "./src/grep/*.ts",
88
+ "import": "./src/grep/*.ts"
89
+ },
90
+ "./highlight": {
91
+ "types": "./src/highlight/index.ts",
92
+ "import": "./src/highlight/index.ts"
93
+ },
94
+ "./highlight/*": {
95
+ "types": "./src/highlight/*.ts",
96
+ "import": "./src/highlight/*.ts"
97
+ },
98
+ "./html": {
99
+ "types": "./src/html/index.ts",
100
+ "import": "./src/html/index.ts"
101
+ },
102
+ "./html/*": {
103
+ "types": "./src/html/*.ts",
104
+ "import": "./src/html/*.ts"
105
+ },
106
+ "./image": {
107
+ "types": "./src/image/index.ts",
108
+ "import": "./src/image/index.ts"
109
+ },
110
+ "./image/*": {
111
+ "types": "./src/image/*.ts",
112
+ "import": "./src/image/*.ts"
113
+ },
114
+ "./keys": {
115
+ "types": "./src/keys/index.ts",
116
+ "import": "./src/keys/index.ts"
117
+ },
118
+ "./keys/*": {
119
+ "types": "./src/keys/*.ts",
120
+ "import": "./src/keys/*.ts"
121
+ },
122
+ "./ps": {
123
+ "types": "./src/ps/index.ts",
124
+ "import": "./src/ps/index.ts"
125
+ },
126
+ "./ps/*": {
127
+ "types": "./src/ps/*.ts",
128
+ "import": "./src/ps/*.ts"
129
+ },
130
+ "./pty": {
131
+ "types": "./src/pty/index.ts",
132
+ "import": "./src/pty/index.ts"
133
+ },
134
+ "./pty/*": {
135
+ "types": "./src/pty/*.ts",
136
+ "import": "./src/pty/*.ts"
137
+ },
138
+ "./shell": {
139
+ "types": "./src/shell/index.ts",
140
+ "import": "./src/shell/index.ts"
141
+ },
142
+ "./shell/*": {
143
+ "types": "./src/shell/*.ts",
144
+ "import": "./src/shell/*.ts"
145
+ },
146
+ "./text": {
147
+ "types": "./src/text/index.ts",
148
+ "import": "./src/text/index.ts"
149
+ },
150
+ "./text/*": {
151
+ "types": "./src/text/*.ts",
152
+ "import": "./src/text/*.ts"
153
+ },
154
+ "./work": {
155
+ "types": "./src/work/index.ts",
156
+ "import": "./src/work/index.ts"
157
+ },
158
+ "./work/*": {
159
+ "types": "./src/work/*.ts",
160
+ "import": "./src/work/*.ts"
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Base types for native bindings.
3
+ * Modules extend this interface via declaration merging.
4
+ */
5
+
6
+ /** Callback type for threadsafe functions from N-API. */
7
+ export type TsFunc<T> = (error: Error | null, value: T) => void;
8
+
9
+ /** Options for cancellable operations. */
10
+ export interface Cancellable {
11
+ /** Timeout in milliseconds for the operation. */
12
+ timeoutMs?: number;
13
+ /** Abort signal for cancelling the operation. */
14
+ signal?: AbortSignal;
15
+ }
16
+
17
+ /**
18
+ * Native bindings interface.
19
+ * Extended by each module via declaration merging.
20
+ */
21
+ export interface NativeBindings {
22
+ cancelWork(id: number): void;
23
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Clipboard helpers backed by native arboard bindings.
3
+ *
4
+ * Adds OSC 52 fallback for SSH/mosh, Termux support, and headless guards
5
+ * on top of the native arboard layer.
6
+ */
7
+
8
+ import { execSync } from "node:child_process";
9
+
10
+ import { native } from "../native";
11
+
12
+ import type { ClipboardImage } from "./types";
13
+
14
+ export type { ClipboardImage } from "./types";
15
+
16
+ /** Whether a display server is available on Linux. */
17
+ const hasDisplay = process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
18
+
19
+ /**
20
+ * Copy text to the system clipboard.
21
+ *
22
+ * Emits OSC 52 first when running in a real terminal (works over SSH/mosh),
23
+ * then attempts native clipboard copy as best-effort for local sessions.
24
+ * On Termux, tries `termux-clipboard-set` before native.
25
+ *
26
+ * @param text - UTF-8 text to place on the clipboard.
27
+ */
28
+ export async function copyToClipboard(text: string): Promise<void> {
29
+ if (process.stdout.isTTY) {
30
+ const encoded = Buffer.from(text).toString("base64");
31
+ const osc52 = `\x1b]52;c;${encoded}\x07`;
32
+ const onError = (err: unknown) => {
33
+ process.stdout.off("error", onError);
34
+ // Prevent unhandled 'error' from crashing the process when stdout is a closed pipe.
35
+ if ((err as NodeJS.ErrnoException | null | undefined)?.code === "EPIPE") {
36
+ return;
37
+ }
38
+ };
39
+ try {
40
+ process.stdout.on("error", onError);
41
+ process.stdout.write(osc52, err => {
42
+ process.stdout.off("error", onError);
43
+ // If stdout is closed (e.g. piped to a process that exits early),
44
+ // ignore EPIPE and proceed with native clipboard best-effort.
45
+ if ((err as NodeJS.ErrnoException | null | undefined)?.code === "EPIPE") {
46
+ return;
47
+ }
48
+ });
49
+ } catch (err) {
50
+ process.stdout.off("error", onError);
51
+ if ((err as NodeJS.ErrnoException | null | undefined)?.code !== "EPIPE") {
52
+ // Ignore all write failures (OSC 52 is best-effort).
53
+ }
54
+ }
55
+ }
56
+
57
+ // Also try native tools (best effort for local sessions)
58
+ try {
59
+ if (process.env.TERMUX_VERSION) {
60
+ try {
61
+ execSync("termux-clipboard-set", { input: text, timeout: 5000 });
62
+ return;
63
+ } catch {
64
+ // Fall through to native
65
+ }
66
+ }
67
+
68
+ await native.copyToClipboard(text);
69
+ } catch {
70
+ // Ignore — clipboard copy is best-effort
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Read an image from the system clipboard.
76
+ *
77
+ * Returns null on Termux (no image clipboard support) or when no display
78
+ * server is available (headless/SSH without forwarding).
79
+ *
80
+ * @returns PNG payload or null when no image is available.
81
+ */
82
+ export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
83
+ if (process.env.TERMUX_VERSION) {
84
+ return null;
85
+ }
86
+
87
+ if (!hasDisplay) {
88
+ return null;
89
+ }
90
+
91
+ return native.readImageFromClipboard();
92
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Types for clipboard operations.
3
+ */
4
+
5
+ /** PNG-encoded clipboard image payload. */
6
+ export interface ClipboardImage {
7
+ /** PNG image bytes. */
8
+ data: Uint8Array;
9
+ /** MIME type for the PNG payload. */
10
+ mimeType: string;
11
+ }
12
+
13
+ declare module "../bindings" {
14
+ /** Native clipboard operations exposed by the bindings layer. */
15
+ interface NativeBindings {
16
+ /**
17
+ * Copy text to the system clipboard.
18
+ * @param text - UTF-8 text to place on the clipboard.
19
+ */
20
+ copyToClipboard(text: string): Promise<void>;
21
+ /**
22
+ * Read an image from the clipboard.
23
+ * @returns PNG payload or null when no image is available.
24
+ */
25
+ readImageFromClipboard(): Promise<ClipboardImage | null>;
26
+ }
27
+ }
@@ -0,0 +1,15 @@
1
+ export type EmbeddedAddonVariant = "modern" | "baseline" | "default";
2
+
3
+ export interface EmbeddedAddonFile {
4
+ variant: EmbeddedAddonVariant;
5
+ filename: string;
6
+ filePath: string;
7
+ }
8
+
9
+ export interface EmbeddedAddon {
10
+ platformTag: string;
11
+ version: string;
12
+ files: EmbeddedAddonFile[];
13
+ }
14
+
15
+ export const embeddedAddon: EmbeddedAddon | null = null;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * File discovery API powered by globset + ignore crate.
3
+ */
4
+
5
+ import * as path from "node:path";
6
+ import { native } from "../native";
7
+ import type { GlobMatch, GlobOptions, GlobResult } from "./types";
8
+
9
+ export type { GlobMatch, GlobOptions, GlobResult } from "./types";
10
+ export { FileType } from "./types";
11
+
12
+ /**
13
+ * Find files matching a glob pattern.
14
+ * Respects .gitignore by default.
15
+ */
16
+ export async function glob(options: GlobOptions, onMatch?: (match: GlobMatch) => void): Promise<GlobResult> {
17
+ const searchPath = path.resolve(options.path);
18
+ const pattern = options.pattern || "*";
19
+ // napi-rs ThreadsafeFunction passes (error, value) - skip callback on error
20
+ const cb = onMatch ? (err: Error | null, m: GlobMatch) => !err && onMatch(m) : undefined;
21
+
22
+ return native.glob(
23
+ {
24
+ ...options,
25
+ path: searchPath,
26
+ pattern,
27
+ hidden: options.hidden ?? false,
28
+ gitignore: options.gitignore ?? true,
29
+ recursive: options.recursive ?? true,
30
+ },
31
+ cb,
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Invalidate the filesystem scan cache.
37
+ *
38
+ * When called with a path, removes entries for roots containing that path.
39
+ * When called without a path, clears the entire cache.
40
+ */
41
+ export function invalidateFsScanCache(path?: string): void {
42
+ native.invalidateFsScanCache(path);
43
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Types for native find API.
3
+ */
4
+
5
+ import type { Cancellable, TsFunc } from "../bindings";
6
+
7
+ export const enum FileType {
8
+ /** A regular file. */
9
+ File = 1,
10
+ /** A directory. */
11
+ Dir = 2,
12
+ /** A symlink. */
13
+ Symlink = 3,
14
+ }
15
+
16
+ /** Options for discovering files and directories. */
17
+ export interface GlobOptions extends Cancellable {
18
+ /** Glob pattern to match (e.g., `*.ts`). */
19
+ pattern: string;
20
+ /** Directory to search. */
21
+ path: string;
22
+ /** Filter by file type: "file", "dir", or "symlink". Symlinks match file/dir filters when their target type matches. */
23
+ fileType?: FileType;
24
+ /** Match simple patterns recursively by default (example: *.ts -> recursive match). Set false to keep patterns relative to the search root only. */
25
+ recursive?: boolean;
26
+ /** Include hidden files (default: false). */
27
+ hidden?: boolean;
28
+ /** Maximum number of results to return. */
29
+ maxResults?: number;
30
+ /** Respect .gitignore files (default: true). */
31
+ gitignore?: boolean;
32
+ /** Enable shared filesystem scan cache (default: false). */
33
+ cache?: boolean;
34
+ /** Sort results by mtime (most recent first) before applying limit. */
35
+ sortByMtime?: boolean;
36
+ /** Include node_modules entries even when pattern does not mention node_modules. */
37
+ includeNodeModules?: boolean;
38
+ }
39
+
40
+ /** A single filesystem match. */
41
+ export interface GlobMatch {
42
+ /** Relative path from the search root. */
43
+ path: string;
44
+ /** Resolved filesystem type for the match (for fileType=file/dir filters, symlink targets are reported as file/dir). */
45
+ fileType: FileType;
46
+ /** Modification time in milliseconds since epoch, if available. */
47
+ mtime?: number;
48
+ }
49
+
50
+ /** Result of a find operation. */
51
+ export interface GlobResult {
52
+ /** Matched filesystem entries. */
53
+ matches: GlobMatch[];
54
+ /** Number of matches returned after limits are applied. */
55
+ totalMatches: number;
56
+ }
57
+
58
+ declare module "../bindings" {
59
+ interface NativeBindings {
60
+ /**
61
+ * Find filesystem entries matching a glob pattern.
62
+ * @param options Search options that control globbing and filters.
63
+ * @param onMatch Optional callback for streaming matches as they are found.
64
+ */
65
+ glob(options: GlobOptions, onMatch?: TsFunc<GlobMatch>): Promise<GlobResult>;
66
+ /** Invalidate the filesystem scan cache for the given path (or all caches if omitted). */
67
+ invalidateFsScanCache(path?: string): void;
68
+ }
69
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Native ripgrep wrapper using N-API.
3
+ */
4
+
5
+ import { native } from "../native";
6
+ import type {
7
+ ContextLine,
8
+ FuzzyFindMatch,
9
+ FuzzyFindOptions,
10
+ FuzzyFindResult,
11
+ GrepMatch,
12
+ GrepOptions,
13
+ GrepResult,
14
+ GrepSummary,
15
+ SearchOptions,
16
+ SearchResult,
17
+ } from "./types";
18
+
19
+ export type {
20
+ ContextLine,
21
+ FuzzyFindMatch,
22
+ FuzzyFindOptions,
23
+ FuzzyFindResult,
24
+ GrepMatch,
25
+ GrepOptions,
26
+ GrepResult,
27
+ GrepSummary,
28
+ SearchOptions,
29
+ SearchResult,
30
+ };
31
+
32
+ /**
33
+ * Search files for a regex pattern with optional streaming callback.
34
+ */
35
+ export async function grep(options: GrepOptions, onMatch?: (match: GrepMatch) => void): Promise<GrepResult> {
36
+ // napi-rs ThreadsafeFunction passes (error, value) - skip callback on error
37
+ const cb = onMatch ? (err: Error | null, m: GrepMatch) => !err && onMatch(m) : undefined;
38
+ return native.grep(options, cb);
39
+ }
40
+
41
+ /**
42
+ * Search a single file's content for a pattern.
43
+ * Lower-level API for when you already have file content.
44
+ *
45
+ * Accepts `Uint8Array`/`Buffer` for zero-copy when content is already UTF-8 encoded.
46
+ */
47
+ export function searchContent(content: string | Uint8Array, options: SearchOptions): SearchResult {
48
+ return native.search(content, options);
49
+ }
50
+
51
+ /**
52
+ * Quick check if content contains a pattern match.
53
+ *
54
+ * Accepts `Uint8Array`/`Buffer` for zero-copy when content/pattern are already UTF-8 encoded.
55
+ */
56
+ export function hasMatch(
57
+ content: string | Uint8Array,
58
+ pattern: string | Uint8Array,
59
+ options?: { ignoreCase?: boolean; multiline?: boolean },
60
+ ): boolean {
61
+ return native.hasMatch(content, pattern, options?.ignoreCase ?? false, options?.multiline ?? false);
62
+ }
63
+
64
+ /**
65
+ * Fuzzy file path search for autocomplete.
66
+ *
67
+ * Searches for files and directories whose paths contain the query substring
68
+ * (case-insensitive). Respects .gitignore by default.
69
+ */
70
+ export async function fuzzyFind(options: FuzzyFindOptions): Promise<FuzzyFindResult> {
71
+ return native.fuzzyFind(options);
72
+ }