refactor: migrated native tools to WASM-based implementation
- Consolidated grep, find, and image processing into a new `@oh-my-pi/pi-natives` WASM package. - Eliminated subprocess overhead and simplified JS tool implementations. - Updated CI to include Rust workspace, linting, formatting, and WASM build steps. - Documented `Promise.withResolvers()` and `using` declarations for wasm-bindgen.
This commit is contained in:
@@ -18,6 +18,43 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
rust:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Clippy
|
||||
run: cargo clippy --workspace -- -D warnings
|
||||
- name: Check
|
||||
run: cargo check --workspace
|
||||
|
||||
wasm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install wasm-pack
|
||||
run: cargo install wasm-pack
|
||||
- name: Install binaryen (wasm-opt)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y binaryen
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3"
|
||||
- name: Build WASM
|
||||
run: bun --cwd=packages/natives run build:wasm
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -49,7 +86,7 @@ jobs:
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: test
|
||||
needs: [rust, wasm, test]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -44,3 +44,5 @@ __pycache__/
|
||||
*.b64.js
|
||||
|
||||
*.cpuprofile
|
||||
|
||||
target/
|
||||
|
||||
@@ -26,6 +26,34 @@ This repo contains multiple packages, but **`packages/coding-agent/`** is the pr
|
||||
- Always ask before removing functionality or code that appears to be intentional
|
||||
- **NEVER build prompts in code** — no inline strings, no template literals, no string concatenation. Prompts live in static `.md` files; use Handlebars for any dynamic content.
|
||||
- **Import static text files via Bun** — use `import content from "./prompt.md" with { type: "text" }` instead of `readFileSync`
|
||||
- **Use `Promise.withResolvers()`** instead of `new Promise((resolve, reject) => ...)` — cleaner, avoids callback nesting, and the resolver functions are properly typed:
|
||||
```typescript
|
||||
// BAD: Verbose, callback nesting
|
||||
const promise = new Promise<string>((resolve, reject) => { ... });
|
||||
|
||||
// GOOD: Clean destructuring, typed resolvers
|
||||
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
||||
```
|
||||
|
||||
## WASM Bindgen Resources
|
||||
|
||||
**Use `using` declarations** for wasm-bindgen objects — they implement `Symbol.dispose`:
|
||||
|
||||
```typescript
|
||||
// BAD: Manual try/finally with .free()
|
||||
const image = PhotonImage.new_from_byteslice(bytes);
|
||||
try {
|
||||
return image.get_bytes();
|
||||
} finally {
|
||||
image.free();
|
||||
}
|
||||
|
||||
// GOOD: Automatic cleanup via Symbol.dispose
|
||||
using image = PhotonImage.new_from_byteslice(bytes);
|
||||
return image.get_bytes();
|
||||
```
|
||||
|
||||
This applies to all wasm-bindgen generated classes (`CompiledPattern`, `PhotonImage`, etc.).
|
||||
|
||||
## Bun Over Node
|
||||
|
||||
|
||||
Generated
+579
@@ -0,0 +1,579 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs_io"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep-matcher"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36d7b71093325ab22d780b40d7df3066ae4aebb518ba719d38c697a8228a8023"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep-regex"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce0c256c3ad82bcc07b812c15a45ec1d398122e8e15124f96695234db7112ef"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"grep-matcher",
|
||||
"log",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep-searcher"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac63295322dc48ebb20a25348147905d816318888e64f531bfc2a2bc0577dc34"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"encoding_rs",
|
||||
"encoding_rs_io",
|
||||
"grep-matcher",
|
||||
"log",
|
||||
"memchr",
|
||||
"memmap2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "pi-natives"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"grep-matcher",
|
||||
"grep-regex",
|
||||
"grep-searcher",
|
||||
"image",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-wasm-bindgen"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi-grep"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"grep-matcher",
|
||||
"grep-regex",
|
||||
"grep-searcher",
|
||||
"ignore",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2959ca473aae96a14ecedf501d20b3608d2825ba280d5adb57d651721885b0c2"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
authors = ["Can Boluk"]
|
||||
repository = "https://github.com/can1357/oh-my-pi"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
||||
[profile.dev]
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# Base Lint Levels
|
||||
all = { level = "warn", priority = -1 }
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
perf = { level = "warn", priority = -1 }
|
||||
style = { level = "warn", priority = -1 }
|
||||
suspicious = { level = "deny", priority = -1 }
|
||||
|
||||
# Meta: Lint Attributes
|
||||
allow_attributes_without_reason = "warn"
|
||||
|
||||
# Safety & Unsafe Code
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
|
||||
# Numeric Casts & Conversions
|
||||
cast_lossless = "allow"
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
|
||||
# Functions & Closures
|
||||
inline_always = "allow"
|
||||
must_use_candidate = "allow"
|
||||
needless_pass_by_value = "allow"
|
||||
redundant_closure_for_method_calls = "allow"
|
||||
return_self_not_must_use = "allow"
|
||||
|
||||
# Structs & Types
|
||||
missing_fields_in_debug = "allow"
|
||||
struct_excessive_bools = "allow"
|
||||
|
||||
# Pattern Matching
|
||||
match_same_arms = "allow"
|
||||
match_wildcard_for_single_variants = "allow"
|
||||
option_if_let_else = "allow"
|
||||
|
||||
# Imports & Module Organization
|
||||
enum_glob_use = "allow"
|
||||
items_after_statements = "allow"
|
||||
wildcard_imports = "allow"
|
||||
|
||||
# Variables & Type Inference
|
||||
similar_names = "allow"
|
||||
|
||||
# Literals & Formatting
|
||||
unreadable_literal = "allow"
|
||||
|
||||
# Code Style
|
||||
default_trait_access = "allow"
|
||||
significant_drop_tightening = "allow"
|
||||
|
||||
# Documentation
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
|
||||
# Complexity
|
||||
too_many_lines = "allow"
|
||||
+2
-1
@@ -12,7 +12,8 @@
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off",
|
||||
"noControlCharactersInRegex": "off",
|
||||
"noEmptyInterface": "off"
|
||||
"noEmptyInterface": "off",
|
||||
"noConstEnum": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"@oh-my-pi/omp-stats": "workspace:*",
|
||||
"@oh-my-pi/pi-agent-core": "workspace:*",
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
"@oh-my-pi/pi-natives": "workspace:*",
|
||||
"@oh-my-pi/pi-tui": "workspace:*",
|
||||
"@oh-my-pi/pi-utils": "workspace:*",
|
||||
"@openai/agents": "^0.4.4",
|
||||
@@ -99,6 +100,16 @@
|
||||
"ms": "^2.1.3",
|
||||
},
|
||||
},
|
||||
"packages/natives": {
|
||||
"name": "@oh-my-pi/pi-natives",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@oh-my-pi/pi-utils": "workspace:*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.10",
|
||||
},
|
||||
},
|
||||
"packages/pi-utils": {
|
||||
"name": "@oh-my-pi/pi-utils",
|
||||
"version": "8.12.3",
|
||||
@@ -293,6 +304,8 @@
|
||||
|
||||
"@oh-my-pi/pi-coding-agent": ["@oh-my-pi/pi-coding-agent@workspace:packages/coding-agent"],
|
||||
|
||||
"@oh-my-pi/pi-natives": ["@oh-my-pi/pi-natives@workspace:packages/natives"],
|
||||
|
||||
"@oh-my-pi/pi-tui": ["@oh-my-pi/pi-tui@workspace:packages/tui"],
|
||||
|
||||
"@oh-my-pi/pi-utils": ["@oh-my-pi/pi-utils@workspace:packages/pi-utils"],
|
||||
@@ -475,21 +488,21 @@
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260128.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260128.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260128.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260128.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260128.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260128.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260128.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260128.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-P7JZ3NpFdH1FqA8TbAKTsufOvykywFAW36H+3ZzUo9PRz/wmTPGkzn/CQ1QM2/YqxWwqLkU96CLeh8onIpj4nw=="],
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260129.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260129.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260129.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260129.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260129.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260129.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260129.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260129.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-vYk84B/+e1zfDRRsETQy+xA4Cka4bY+nNKjJgSFr5lCu35EwqOn9WonJeT49pinnpJZApDih+8U9DvSbgpCT9Q=="],
|
||||
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260128.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rPyH+d+M8NnYA9FOOB9xPtllB2jtDiB7vnLDGYVjE7y8j3XNMEHFYlwoZ5HLS2yLCgwmmcmEUgurJX94V9Iqeg=="],
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260129.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sP/8x3P/U3RqDJhnNrQECsD/U0k5/2f3H6Sztyf13Yq7FkBDi2U3ypI8ZpQ6UbzoFIQjD0EFe5+8fckPWJltOA=="],
|
||||
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260128.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-8zVZNktu8n6+kvWp9x6vb4J2uD51q58mB497ki0Fxf1q2WPTsPFjBUanFZc8K5hltmaoijF1214lN/KNmhVSqg=="],
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260129.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-51KL04rbl1s0Xe5gn6/xn0LJtURea0XCuDAlRZ0MSAEqwGVisg2CsaGsI09ICgo5FRHfd1RDFJkfPmFwxxpfDg=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260128.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ZOKLNjuT5QFkbuJvS3cBTruV22gAllWRqhgFaMYIorT7WcTzEci2S5RfEz1NzYPYZeY4vJssMmr/O8wdPoMpMA=="],
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260129.1", "", { "os": "linux", "cpu": "arm" }, "sha512-QplErMgtDiHzbGZG1jm+158yfE4scyvBZlKI3izzgUbTnuaw89RTeoQsd6YIWTQ9m3zGbUzD6F6jU7QsGD6msw=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260128.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-xcf3OW/bh6gA+urfG3ofHnfIBhKSqHtFolk2Akd/ZGLiUJik4mimhwlrqx9qus8tGFI3BepdCgfSNhW61fTrhw=="],
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260129.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-XXbjXv8KoqSF5kP1sQ84iKmoDa79qZWUUDrzDT4lu+hbkQ2Gu0nShstun0Xut2g/1kJtViISc+FFU1C7l9UOtA=="],
|
||||
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260128.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qr9gTFYXCS83Sro+rBnw3fZIPRMBR9j/9FS5KtPRBOqn4jpiAR64CAE1NDHtTIBYwgPbpd6kvt8Ay5c+S4zxPQ=="],
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260129.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eh95rGSBtwBF6PZvDX+tCmChcL1v+gV1J+pAWDVOoTSMERZ5hcQC3XADVTFEMJyG6XVxARUfFcd9N9UnVy1vVA=="],
|
||||
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260128.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-8Ke5A+MNaiUqaX6nEDpXXmQqfQBBq4CtN6mGEQs5a5QCnyqcnjy13QlJuCxzRd/nvob7lWIxR6kz43Fdn8l5ig=="],
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260129.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-DF0FZ3XlsChbLuh3wmVz2UbHPGeyXMmQ8uhaCNtiQig5L79e+12d4Qvft7ffSYsnvbbIYY6u4d4SNxog68JZOQ=="],
|
||||
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260128.1", "", { "os": "win32", "cpu": "x64" }, "sha512-+r0hFY/fnQUY5z3lcVNWz5j2G2RBxkQVzRAvbQyl2esGt0scy/6dPeP/PrvHFADWoVJkV9DTSBhbmsJfL1inKw=="],
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260129.1", "", { "os": "win32", "cpu": "x64" }, "sha512-3Utyo3HIS/FBJw1F6JNIjEYCvnEgPJ61yIBAkQ4YpQobATsNLjs38KR0TkJU4gsHfTOaJ5ZuZIxHc1dskBMSkA=="],
|
||||
|
||||
"@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="],
|
||||
|
||||
@@ -879,7 +892,7 @@
|
||||
|
||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="],
|
||||
"openai": ["openai@6.17.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA=="],
|
||||
|
||||
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
|
||||
|
||||
@@ -1127,7 +1140,7 @@
|
||||
|
||||
"cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
||||
|
||||
"cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
|
||||
"cli-truncate/string-width": ["string-width@8.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw=="],
|
||||
|
||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "pi-natives"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.6"
|
||||
grep-regex = "0.1.14"
|
||||
grep-searcher = "0.1.16"
|
||||
grep-matcher = "0.1.8"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
||||
|
||||
# Better panic messages in debug builds
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Minimal image processing API for resizing and format conversion.
|
||||
//!
|
||||
//! Provides only the subset of functionality needed:
|
||||
//! - Load image from bytes (PNG, JPEG, WebP, GIF)
|
||||
//! - Get dimensions
|
||||
//! - Resize with Lanczos3 filter
|
||||
//! - Export as PNG or JPEG
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use image::{DynamicImage, ImageFormat, ImageReader, imageops::FilterType};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Sampling filter for resize operations.
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum SamplingFilter {
|
||||
Nearest = 1,
|
||||
Triangle = 2,
|
||||
CatmullRom = 3,
|
||||
Gaussian = 4,
|
||||
Lanczos3 = 5,
|
||||
}
|
||||
|
||||
impl From<SamplingFilter> for FilterType {
|
||||
fn from(filter: SamplingFilter) -> Self {
|
||||
match filter {
|
||||
SamplingFilter::Nearest => FilterType::Nearest,
|
||||
SamplingFilter::Triangle => FilterType::Triangle,
|
||||
SamplingFilter::CatmullRom => FilterType::CatmullRom,
|
||||
SamplingFilter::Gaussian => FilterType::Gaussian,
|
||||
SamplingFilter::Lanczos3 => FilterType::Lanczos3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Image container for WASM interop.
|
||||
#[wasm_bindgen]
|
||||
pub struct PhotonImage {
|
||||
img: DynamicImage,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl PhotonImage {
|
||||
/// Create a new PhotonImage from encoded image bytes (PNG, JPEG, WebP, GIF).
|
||||
#[wasm_bindgen(js_name = new_from_byteslice)]
|
||||
pub fn new_from_byteslice(bytes: &[u8]) -> Result<PhotonImage, JsValue> {
|
||||
let reader = ImageReader::new(Cursor::new(bytes))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to detect image format: {e}")))?;
|
||||
|
||||
let img = reader
|
||||
.decode()
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to decode image: {e}")))?;
|
||||
|
||||
Ok(PhotonImage { img })
|
||||
}
|
||||
|
||||
/// Get the width of the image.
|
||||
#[wasm_bindgen(js_name = get_width)]
|
||||
pub fn get_width(&self) -> u32 {
|
||||
self.img.width()
|
||||
}
|
||||
|
||||
/// Get the height of the image.
|
||||
#[wasm_bindgen(js_name = get_height)]
|
||||
pub fn get_height(&self) -> u32 {
|
||||
self.img.height()
|
||||
}
|
||||
|
||||
/// Export image as PNG bytes.
|
||||
#[wasm_bindgen(js_name = get_bytes)]
|
||||
pub fn get_bytes(&self) -> Result<Vec<u8>, JsValue> {
|
||||
let mut buffer = Vec::new();
|
||||
self.img
|
||||
.write_to(&mut Cursor::new(&mut buffer), ImageFormat::Png)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to encode PNG: {e}")))?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// Export image as JPEG bytes with specified quality (0-100).
|
||||
#[wasm_bindgen(js_name = get_bytes_jpeg)]
|
||||
pub fn get_bytes_jpeg(&self, quality: u8) -> Result<Vec<u8>, JsValue> {
|
||||
let mut buffer = Vec::new();
|
||||
let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buffer, quality);
|
||||
self.img
|
||||
.write_with_encoder(encoder)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to encode JPEG: {e}")))?;
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize an image to the specified dimensions.
|
||||
#[wasm_bindgen]
|
||||
pub fn resize(
|
||||
image: &PhotonImage,
|
||||
width: u32,
|
||||
height: u32,
|
||||
filter: SamplingFilter,
|
||||
) -> PhotonImage {
|
||||
let resized = image.img.resize_exact(width, height, filter.into());
|
||||
PhotonImage { img: resized }
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
//! WASM module for regex matching using ripgrep's engine.
|
||||
//!
|
||||
//! This module provides pure regex matching - no filesystem access.
|
||||
//! The JS side handles directory walking and file reading, then passes
|
||||
//! content here for matching.
|
||||
|
||||
pub mod image;
|
||||
|
||||
use std::io::{self, Cursor};
|
||||
|
||||
use grep_matcher::Matcher;
|
||||
use grep_regex::RegexMatcherBuilder;
|
||||
use grep_searcher::{
|
||||
BinaryDetection, Searcher, SearcherBuilder, Sink, SinkContext, SinkContextKind, SinkMatch,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
fn set_panic_hook() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
/// Options for searching file content.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SearchOptions {
|
||||
/// Regex pattern to search for.
|
||||
pub pattern: String,
|
||||
/// Case-insensitive search.
|
||||
#[serde(default)]
|
||||
pub ignore_case: bool,
|
||||
/// Enable multiline matching.
|
||||
#[serde(default)]
|
||||
pub multiline: bool,
|
||||
/// Maximum number of matches to return.
|
||||
#[serde(default)]
|
||||
pub max_count: Option<u64>,
|
||||
/// Skip first N matches.
|
||||
#[serde(default)]
|
||||
pub offset: Option<u64>,
|
||||
/// Lines of context before/after matches.
|
||||
#[serde(default)]
|
||||
pub context: Option<usize>,
|
||||
/// Truncate lines longer than this (characters).
|
||||
#[serde(default)]
|
||||
pub max_columns: Option<usize>,
|
||||
/// Output mode (content or count).
|
||||
#[serde(default)]
|
||||
pub mode: SearchMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SearchMode {
|
||||
#[default]
|
||||
Content,
|
||||
Count,
|
||||
}
|
||||
|
||||
/// A context line (before or after a match).
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContextLine {
|
||||
pub line_number: u64,
|
||||
pub line: String,
|
||||
}
|
||||
|
||||
/// A single match in the content.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Match {
|
||||
/// 1-indexed line number.
|
||||
pub line_number: u64,
|
||||
/// The matched line content.
|
||||
pub line: String,
|
||||
/// Context lines before the match.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub context_before: Vec<ContextLine>,
|
||||
/// Context lines after the match.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub context_after: Vec<ContextLine>,
|
||||
/// Whether the line was truncated.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
/// Result of searching content.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SearchResult {
|
||||
/// All matches found.
|
||||
pub matches: Vec<Match>,
|
||||
/// Total number of matches (may exceed `matches.len()` due to offset/limit).
|
||||
pub match_count: u64,
|
||||
/// Whether the limit was reached.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub limit_reached: bool,
|
||||
}
|
||||
|
||||
/// Error result.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ErrorResult {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
struct MatchCollector {
|
||||
matches: Vec<CollectedMatch>,
|
||||
match_count: u64,
|
||||
collected_count: u64,
|
||||
max_count: Option<u64>,
|
||||
offset: u64,
|
||||
skipped: u64,
|
||||
limit_reached: bool,
|
||||
context_before: Vec<ContextLine>,
|
||||
max_columns: Option<usize>,
|
||||
collect_matches: bool,
|
||||
}
|
||||
|
||||
struct CollectedMatch {
|
||||
line_number: u64,
|
||||
line: String,
|
||||
context_before: Vec<ContextLine>,
|
||||
context_after: Vec<ContextLine>,
|
||||
truncated: bool,
|
||||
}
|
||||
|
||||
impl MatchCollector {
|
||||
const fn new(
|
||||
max_count: Option<u64>,
|
||||
offset: u64,
|
||||
max_columns: Option<usize>,
|
||||
collect_matches: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
matches: Vec::new(),
|
||||
match_count: 0,
|
||||
collected_count: 0,
|
||||
max_count,
|
||||
offset,
|
||||
skipped: 0,
|
||||
limit_reached: false,
|
||||
context_before: Vec::new(),
|
||||
max_columns,
|
||||
collect_matches,
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_line(&self, line: &str) -> (String, bool) {
|
||||
match self.max_columns {
|
||||
Some(max) if line.len() > max => {
|
||||
let truncated = format!("{}...", &line[..max.saturating_sub(3)]);
|
||||
(truncated, true)
|
||||
},
|
||||
_ => (line.to_string(), false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink for MatchCollector {
|
||||
type Error = io::Error;
|
||||
|
||||
fn matched(&mut self, _searcher: &Searcher, mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {
|
||||
self.match_count += 1;
|
||||
|
||||
// If we already hit the limit, stop now (after-context for previous match was collected)
|
||||
if self.limit_reached {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if self.skipped < self.offset {
|
||||
self.skipped += 1;
|
||||
self.context_before.clear();
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if self.collect_matches {
|
||||
let raw_line = String::from_utf8_lossy(mat.bytes()).trim_end().to_string();
|
||||
let (line, truncated) = self.truncate_line(&raw_line);
|
||||
let line_number = mat.line_number().unwrap_or(0);
|
||||
|
||||
self.matches.push(CollectedMatch {
|
||||
line_number,
|
||||
line,
|
||||
context_before: std::mem::take(&mut self.context_before),
|
||||
context_after: Vec::new(),
|
||||
truncated,
|
||||
});
|
||||
} else {
|
||||
self.context_before.clear();
|
||||
}
|
||||
|
||||
self.collected_count += 1;
|
||||
|
||||
// Mark limit reached but don't stop yet - allow after-context to be collected
|
||||
if let Some(max) = self.max_count
|
||||
&& self.collected_count >= max
|
||||
{
|
||||
self.limit_reached = true;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn context(&mut self, _searcher: &Searcher, ctx: &SinkContext<'_>) -> Result<bool, Self::Error> {
|
||||
if !self.collect_matches {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let raw_line = String::from_utf8_lossy(ctx.bytes()).trim_end().to_string();
|
||||
let (line, _) = self.truncate_line(&raw_line);
|
||||
let line_number = ctx.line_number().unwrap_or(0);
|
||||
|
||||
match ctx.kind() {
|
||||
SinkContextKind::Before => {
|
||||
self.context_before.push(ContextLine { line_number, line });
|
||||
},
|
||||
SinkContextKind::After => {
|
||||
if let Some(last_match) = self.matches.last_mut() {
|
||||
last_match
|
||||
.context_after
|
||||
.push(ContextLine { line_number, line });
|
||||
}
|
||||
},
|
||||
SinkContextKind::Other => {},
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// A compiled regex matcher that can be reused across multiple searches.
|
||||
#[wasm_bindgen]
|
||||
pub struct CompiledPattern {
|
||||
matcher: grep_regex::RegexMatcher,
|
||||
context: usize,
|
||||
max_columns: Option<usize>,
|
||||
mode: SearchMode,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl CompiledPattern {
|
||||
/// Compile a regex pattern for reuse.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(options: JsValue) -> Result<Self, JsValue> {
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
set_panic_hook();
|
||||
|
||||
let opts: SearchOptions = serde_wasm_bindgen::from_value(options)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid options: {e}")))?;
|
||||
|
||||
let matcher = RegexMatcherBuilder::new()
|
||||
.case_insensitive(opts.ignore_case)
|
||||
.multi_line(opts.multiline)
|
||||
.build(&opts.pattern)
|
||||
.map_err(|e| JsValue::from_str(&format!("Regex error: {e}")))?;
|
||||
|
||||
Ok(Self {
|
||||
matcher,
|
||||
context: opts.context.unwrap_or(0),
|
||||
max_columns: opts.max_columns,
|
||||
mode: opts.mode,
|
||||
})
|
||||
}
|
||||
|
||||
/// Search content using this compiled pattern.
|
||||
/// Returns matches as a JS object.
|
||||
pub fn search(&self, content: &str, max_count: Option<u32>, offset: Option<u32>) -> JsValue {
|
||||
let context_lines = self.context;
|
||||
let mut searcher = SearcherBuilder::new()
|
||||
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||
.line_number(true)
|
||||
.before_context(context_lines)
|
||||
.after_context(context_lines)
|
||||
.build();
|
||||
|
||||
let mut collector = MatchCollector::new(
|
||||
max_count.map(|n| n as u64),
|
||||
offset.unwrap_or(0) as u64,
|
||||
self.max_columns,
|
||||
self.mode == SearchMode::Content,
|
||||
);
|
||||
|
||||
let cursor = Cursor::new(content.as_bytes());
|
||||
if let Err(e) = searcher.search_reader(&self.matcher, cursor, &mut collector) {
|
||||
return serde_wasm_bindgen::to_value(&ErrorResult { error: e.to_string() })
|
||||
.unwrap_or(JsValue::NULL);
|
||||
}
|
||||
|
||||
let result = SearchResult {
|
||||
matches: collector
|
||||
.matches
|
||||
.into_iter()
|
||||
.map(|m| Match {
|
||||
line_number: m.line_number,
|
||||
line: m.line,
|
||||
context_before: m.context_before,
|
||||
context_after: m.context_after,
|
||||
truncated: m.truncated,
|
||||
})
|
||||
.collect(),
|
||||
match_count: collector.match_count,
|
||||
limit_reached: collector.limit_reached,
|
||||
};
|
||||
|
||||
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Check if content has any matches (faster than full search).
|
||||
pub fn has_match(&self, content: &str) -> bool {
|
||||
self.matcher.is_match(content.as_bytes()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Search bytes directly (avoids UTF-16 to UTF-8 conversion).
|
||||
/// Use with `Bun.mmap()` for best performance.
|
||||
pub fn search_bytes(
|
||||
&self,
|
||||
content: &[u8],
|
||||
max_count: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
) -> JsValue {
|
||||
let context_lines = self.context;
|
||||
let mut searcher = SearcherBuilder::new()
|
||||
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||
.line_number(true)
|
||||
.before_context(context_lines)
|
||||
.after_context(context_lines)
|
||||
.build();
|
||||
|
||||
let mut collector = MatchCollector::new(
|
||||
max_count.map(|n| n as u64),
|
||||
offset.unwrap_or(0) as u64,
|
||||
self.max_columns,
|
||||
self.mode == SearchMode::Content,
|
||||
);
|
||||
|
||||
let cursor = Cursor::new(content);
|
||||
if let Err(e) = searcher.search_reader(&self.matcher, cursor, &mut collector) {
|
||||
return serde_wasm_bindgen::to_value(&ErrorResult { error: e.to_string() })
|
||||
.unwrap_or(JsValue::NULL);
|
||||
}
|
||||
|
||||
let result = SearchResult {
|
||||
matches: collector
|
||||
.matches
|
||||
.into_iter()
|
||||
.map(|m| Match {
|
||||
line_number: m.line_number,
|
||||
line: m.line,
|
||||
context_before: m.context_before,
|
||||
context_after: m.context_after,
|
||||
truncated: m.truncated,
|
||||
})
|
||||
.collect(),
|
||||
match_count: collector.match_count,
|
||||
limit_reached: collector.limit_reached,
|
||||
};
|
||||
|
||||
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Check if bytes have any matches (faster than full search).
|
||||
pub fn has_match_bytes(&self, content: &[u8]) -> bool {
|
||||
self.matcher.is_match(content).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Search content for a pattern (one-shot, compiles pattern each time).
|
||||
/// For repeated searches with the same pattern, use [`CompiledPattern`].
|
||||
#[wasm_bindgen]
|
||||
pub fn search(content: &str, options: JsValue) -> JsValue {
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
set_panic_hook();
|
||||
|
||||
let opts: SearchOptions = match serde_wasm_bindgen::from_value(options) {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
return serde_wasm_bindgen::to_value(&ErrorResult {
|
||||
error: format!("Invalid options: {e}"),
|
||||
})
|
||||
.unwrap_or(JsValue::NULL);
|
||||
},
|
||||
};
|
||||
|
||||
let matcher = match RegexMatcherBuilder::new()
|
||||
.case_insensitive(opts.ignore_case)
|
||||
.multi_line(opts.multiline)
|
||||
.build(&opts.pattern)
|
||||
{
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
return serde_wasm_bindgen::to_value(&ErrorResult { error: format!("Regex error: {e}") })
|
||||
.unwrap_or(JsValue::NULL);
|
||||
},
|
||||
};
|
||||
|
||||
let context_lines = opts.context.unwrap_or(0);
|
||||
let mut searcher = SearcherBuilder::new()
|
||||
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||
.line_number(true)
|
||||
.before_context(context_lines)
|
||||
.after_context(context_lines)
|
||||
.build();
|
||||
|
||||
let mut collector = MatchCollector::new(
|
||||
opts.max_count,
|
||||
opts.offset.unwrap_or(0),
|
||||
opts.max_columns,
|
||||
opts.mode == SearchMode::Content,
|
||||
);
|
||||
|
||||
let cursor = Cursor::new(content.as_bytes());
|
||||
if let Err(e) = searcher.search_reader(&matcher, cursor, &mut collector) {
|
||||
return serde_wasm_bindgen::to_value(&ErrorResult { error: e.to_string() })
|
||||
.unwrap_or(JsValue::NULL);
|
||||
}
|
||||
|
||||
let result = SearchResult {
|
||||
matches: collector
|
||||
.matches
|
||||
.into_iter()
|
||||
.map(|m| Match {
|
||||
line_number: m.line_number,
|
||||
line: m.line,
|
||||
context_before: m.context_before,
|
||||
context_after: m.context_after,
|
||||
truncated: m.truncated,
|
||||
})
|
||||
.collect(),
|
||||
match_count: collector.match_count,
|
||||
limit_reached: collector.limit_reached,
|
||||
};
|
||||
|
||||
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Quick check if content matches a pattern.
|
||||
#[wasm_bindgen]
|
||||
pub fn has_match(
|
||||
content: &str,
|
||||
pattern: &str,
|
||||
ignore_case: bool,
|
||||
multiline: bool,
|
||||
) -> Result<bool, JsValue> {
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
set_panic_hook();
|
||||
|
||||
let matcher = RegexMatcherBuilder::new()
|
||||
.case_insensitive(ignore_case)
|
||||
.multi_line(multiline)
|
||||
.build(pattern)
|
||||
.map_err(|e| JsValue::from_str(&format!("Regex error: {e}")))?;
|
||||
|
||||
Ok(matcher.is_match(content.as_bytes()).unwrap_or(false))
|
||||
}
|
||||
+6
-1
@@ -10,8 +10,13 @@
|
||||
"dev": "bun packages/coding-agent/src/cli.ts",
|
||||
"stats": "bun --cwd=packages/coding-agent src/cli.ts stats",
|
||||
"check": "biome check --write . && tsgo -p tsconfig.json && tsgo -p packages/stats/tsconfig.client.json",
|
||||
"test": "bun --cwd=packages/ai run test && bun --cwd=packages/agent run test && bun --cwd=packages/tui run test && bun --cwd=packages/coding-agent run test",
|
||||
"test": "bun --cwd=packages/ai run test && bun --cwd=packages/agent run test && bun --cwd=packages/tui run test && bun --cwd=packages/natives run test && bun --cwd=packages/coding-agent run test",
|
||||
"fix": "biome check --write --unsafe . && bun --cwd=packages/coding-agent run format-prompts",
|
||||
"rust:check": "cargo check --workspace",
|
||||
"rust:clippy": "cargo clippy --workspace -- -D warnings",
|
||||
"rust:fmt": "cargo fmt --all -- --check",
|
||||
"rust:fmt:fix": "cargo fmt --all",
|
||||
"rust:lint": "cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings",
|
||||
"bench:gen-fixtures": "bun run packages/react-edit-benchmark/generate.ts --react-dir /tmp/react-source --count-per-type 3",
|
||||
"bench:edit": "bun --cwd=packages/react-edit-benchmark run start",
|
||||
"version:patch": "npm version patch -ws --no-git-tag-version && bun scripts/sync-versions.ts && rm -rf node_modules packages/*/node_modules bun.lock && bun install",
|
||||
|
||||
@@ -58,10 +58,6 @@
|
||||
"types": "./src/internal-urls/*.ts",
|
||||
"import": "./src/internal-urls/*.ts"
|
||||
},
|
||||
"./vendor/photon/*": {
|
||||
"types": "./src/vendor/photon/*",
|
||||
"import": "./src/vendor/photon/*"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./src/*.ts",
|
||||
"import": "./src/*.ts"
|
||||
@@ -86,6 +82,7 @@
|
||||
"@oh-my-pi/omp-stats": "workspace:*",
|
||||
"@oh-my-pi/pi-agent-core": "workspace:*",
|
||||
"@oh-my-pi/pi-ai": "workspace:*",
|
||||
"@oh-my-pi/pi-natives": "workspace:*",
|
||||
"@oh-my-pi/pi-tui": "workspace:*",
|
||||
"@oh-my-pi/pi-utils": "workspace:*",
|
||||
"@openai/agents": "^0.4.4",
|
||||
|
||||
@@ -93,11 +93,6 @@ export function getToolsDir(): string {
|
||||
return path.join(getAgentDir(), "tools");
|
||||
}
|
||||
|
||||
/** Get path to managed binaries directory (fd, rg) */
|
||||
export function getBinDir(): string {
|
||||
return path.join(getAgentDir(), "bin");
|
||||
}
|
||||
|
||||
/** Get path to slash commands directory */
|
||||
export function getCommandsDir(): string {
|
||||
return path.join(getAgentDir(), "commands");
|
||||
|
||||
@@ -6,8 +6,6 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
|
||||
|
||||
// Check external tools
|
||||
const tools = [
|
||||
{ name: "fd", description: "File finder" },
|
||||
{ name: "rg", description: "Ripgrep" },
|
||||
{ name: "sd", description: "Find-replace" },
|
||||
{ name: "sg", description: "AST-grep" },
|
||||
{ name: "git", description: "Version control" },
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||
import chalk from "chalk";
|
||||
import { getAgentDbPath, getAgentDir, getBinDir } from "./config";
|
||||
import { getAgentDbPath, getAgentDir } from "./config";
|
||||
import { AgentStorage } from "./session/agent-storage";
|
||||
import type { AuthCredential } from "./session/auth-storage";
|
||||
|
||||
@@ -144,50 +144,6 @@ export async function migrateSessionsFromAgentRoot(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move fd/rg binaries from tools/ to bin/ if they exist.
|
||||
*/
|
||||
async function migrateToolsToBin(): Promise<void> {
|
||||
const agentDir = getAgentDir();
|
||||
const toolsDir = path.join(agentDir, "tools");
|
||||
const binDir = getBinDir();
|
||||
|
||||
if (!fs.existsSync(toolsDir)) return;
|
||||
|
||||
const binaries = ["fd", "rg", "fd.exe", "rg.exe"];
|
||||
let movedAny = false;
|
||||
|
||||
for (const bin of binaries) {
|
||||
const oldPath = path.join(toolsDir, bin);
|
||||
const newPath = path.join(binDir, bin);
|
||||
if (!fs.existsSync(oldPath)) continue;
|
||||
|
||||
if (!fs.existsSync(binDir)) {
|
||||
await fs.promises.mkdir(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(newPath)) {
|
||||
try {
|
||||
await fs.promises.rename(oldPath, newPath);
|
||||
movedAny = true;
|
||||
} catch (error) {
|
||||
logger.warn("Failed to migrate binary", { from: oldPath, to: newPath, error: String(error) });
|
||||
}
|
||||
} else {
|
||||
// Target exists, just delete the old one
|
||||
try {
|
||||
await fs.promises.rm(oldPath, { force: true });
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (movedAny) {
|
||||
console.log(chalk.green(`Migrated managed binaries tools/ → bin/`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all migrations. Called once on startup.
|
||||
*
|
||||
@@ -201,7 +157,6 @@ export async function runMigrations(_cwd: string): Promise<{
|
||||
// Then: run data migrations
|
||||
const migratedAuthProviders = await migrateAuthToAgentDb();
|
||||
await migrateSessionsFromAgentRoot();
|
||||
await migrateToolsToBin();
|
||||
|
||||
return { migratedAuthProviders, deprecationWarnings: [] };
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { globPaths } from "@oh-my-pi/pi-utils";
|
||||
import { find as wasmFind } from "@oh-my-pi/pi-natives";
|
||||
import { untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import chalk from "chalk";
|
||||
import { contextFileCapability } from "./capability/context-file";
|
||||
@@ -201,17 +202,21 @@ type ProjectTreeScan = {
|
||||
const GLOB_TIMEOUT_MS = 5000;
|
||||
|
||||
/**
|
||||
* Scan project tree using fs.promises.glob with exclusion filters.
|
||||
* Returns null if glob fails.
|
||||
* Scan project tree using ripgrep-wasm find with exclusion filters.
|
||||
* Returns null if scan fails.
|
||||
*/
|
||||
async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan | null> {
|
||||
let entries: string[];
|
||||
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
||||
try {
|
||||
entries = await globPaths("**/*", {
|
||||
cwd: root,
|
||||
gitignore: true,
|
||||
timeoutMs: GLOB_TIMEOUT_MS,
|
||||
});
|
||||
const result = await untilAborted(timeoutSignal, () =>
|
||||
wasmFind({
|
||||
pattern: "**/*",
|
||||
path: root,
|
||||
fileType: "file",
|
||||
}),
|
||||
);
|
||||
entries = result.matches.map(match => match.path).filter(entry => entry.length > 0);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -222,7 +227,7 @@ async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan |
|
||||
dirContents.set(root, new Map());
|
||||
|
||||
for (const entry of entries) {
|
||||
const filePath = entry.trim();
|
||||
const filePath = entry;
|
||||
if (!filePath) continue;
|
||||
const absolutePath = path.join(root, filePath);
|
||||
// Check static ignores on path components
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import { find as wasmFind } from "@oh-my-pi/pi-natives";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import { globPaths, isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import type { Static } from "@sinclair/typebox";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { renderPromptTemplate } from "../config/prompt-templates";
|
||||
@@ -169,22 +170,26 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
||||
}
|
||||
|
||||
let lines: string[];
|
||||
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
||||
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
||||
try {
|
||||
lines = await globPaths(globPattern, {
|
||||
cwd: searchPath,
|
||||
gitignore: true,
|
||||
dot: includeHidden,
|
||||
signal,
|
||||
timeoutMs: GLOB_TIMEOUT_MS,
|
||||
});
|
||||
const result = await untilAborted(combinedSignal, () =>
|
||||
wasmFind({
|
||||
pattern: globPattern,
|
||||
path: searchPath,
|
||||
fileType: "file",
|
||||
hidden: includeHidden,
|
||||
}),
|
||||
);
|
||||
lines = result.matches.map(match => match.path);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
if (timeoutSignal.aborted && !signal?.aborted) {
|
||||
const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
|
||||
throw new ToolError(`find timed out after ${timeoutSeconds}s`);
|
||||
}
|
||||
throw new ToolAbortError();
|
||||
}
|
||||
if (error instanceof Error && error.name === "TimeoutError") {
|
||||
const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
|
||||
throw new ToolError(`glob timed out after ${timeoutSeconds}s`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -197,13 +202,13 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
||||
|
||||
for (const rawLine of lines) {
|
||||
throwIfAborted(signal);
|
||||
const line = rawLine.replace(/\r$/, "").trim();
|
||||
const line = rawLine;
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
||||
let relativePath = line.replace(/\\/g, "/");
|
||||
let relativePath = line;
|
||||
|
||||
let mtimeMs = 0;
|
||||
let isDirectory = false;
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import * as nodePath from "node:path";
|
||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import { StringEnum } from "@oh-my-pi/pi-ai";
|
||||
import { type GrepMatch as WasmGrepMatch, grep as wasmGrep } from "@oh-my-pi/pi-natives";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import { ptree } from "@oh-my-pi/pi-utils";
|
||||
import { untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { renderPromptTemplate } from "../config/prompt-templates";
|
||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||
import type { Theme } from "../modes/theme/theme";
|
||||
import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
|
||||
import { renderFileList, renderStatusLine, renderTreeList } from "../tui";
|
||||
import { ensureTool } from "../utils/tools-manager";
|
||||
import { untilAborted } from "../utils/utils";
|
||||
import type { ToolSession } from ".";
|
||||
import { applyListLimit } from "./list-limit";
|
||||
import type { OutputMeta } from "./output-meta";
|
||||
import { resolveToCwd } from "./path-utils";
|
||||
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
||||
import { ToolAbortError, ToolError } from "./tool-errors";
|
||||
import { ToolError } from "./tool-errors";
|
||||
import { toolResult } from "./tool-result";
|
||||
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine } from "./truncate";
|
||||
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "./truncate";
|
||||
|
||||
const grepSchema = Type.Object({
|
||||
pattern: Type.String({ description: "Regex pattern to search for" }),
|
||||
@@ -27,7 +25,7 @@ const grepSchema = Type.Object({
|
||||
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern (e.g., '*.js')" })),
|
||||
type: Type.Optional(Type.String({ description: "Filter by file type (e.g., js, py, rust)" })),
|
||||
output_mode: Type.Optional(
|
||||
StringEnum(["files_with_matches", "content", "count"], {
|
||||
StringEnum(["filesWithMatches", "content", "count"], {
|
||||
description: "Output format (default: files_with_matches)",
|
||||
}),
|
||||
),
|
||||
@@ -47,76 +45,22 @@ export interface GrepToolDetails {
|
||||
resultLimitReached?: number;
|
||||
linesTruncated?: boolean;
|
||||
meta?: OutputMeta;
|
||||
// Fields for TUI rendering
|
||||
scopePath?: string;
|
||||
matchCount?: number;
|
||||
fileCount?: number;
|
||||
files?: string[];
|
||||
fileMatches?: Array<{ path: string; count: number }>;
|
||||
mode?: "content" | "files_with_matches" | "count";
|
||||
mode?: "content" | "filesWithMatches" | "count";
|
||||
truncated?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface RgResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run rg command and capture output.
|
||||
*
|
||||
* @throws ToolAbortError if signal is aborted
|
||||
*/
|
||||
export async function runRg(
|
||||
rgPath: string,
|
||||
args: string[],
|
||||
options?: { signal?: AbortSignal; timeoutMs?: number },
|
||||
): Promise<RgResult> {
|
||||
const timeoutSeconds = options?.timeoutMs ? Math.max(1, Math.round(options.timeoutMs / 1000)) : undefined;
|
||||
const timeoutMessage = timeoutSeconds ? `rg timed out after ${timeoutSeconds}s` : "rg timed out";
|
||||
|
||||
const result = await ptree.exec([rgPath, ...args], {
|
||||
signal: options?.signal,
|
||||
timeout: options?.timeoutMs,
|
||||
allowNonZero: true,
|
||||
allowAbort: true,
|
||||
stderr: "buffer",
|
||||
});
|
||||
|
||||
if (result.exitError instanceof ptree.TimeoutError) {
|
||||
throw new ToolError(timeoutMessage);
|
||||
}
|
||||
if (result.exitError?.aborted) {
|
||||
throw new ToolAbortError();
|
||||
}
|
||||
|
||||
return {
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
exitCode: result.exitCode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pluggable operations for the grep tool.
|
||||
* Override these to delegate search to remote systems (e.g., SSH).
|
||||
*/
|
||||
export interface GrepOperations {
|
||||
/** Check if path is a directory. Throws if path doesn't exist. */
|
||||
isDirectory: (absolutePath: string) => Promise<boolean> | boolean;
|
||||
/** Read file contents for context lines */
|
||||
readFile: (absolutePath: string) => Promise<string> | string;
|
||||
}
|
||||
|
||||
const defaultGrepOperations: GrepOperations = {
|
||||
isDirectory: async p => (await Bun.file(p).stat()).isDirectory(),
|
||||
readFile: p => Bun.file(p).text(),
|
||||
};
|
||||
|
||||
export interface GrepToolOptions {
|
||||
/** Custom operations for grep. Default: local filesystem + ripgrep */
|
||||
operations?: GrepOperations;
|
||||
}
|
||||
|
||||
@@ -129,46 +73,18 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
public readonly parameters = grepSchema;
|
||||
|
||||
private readonly session: ToolSession;
|
||||
private readonly ops: GrepOperations;
|
||||
|
||||
constructor(session: ToolSession, options?: GrepToolOptions) {
|
||||
constructor(session: ToolSession, _options?: GrepToolOptions) {
|
||||
this.session = session;
|
||||
this.ops = options?.operations ?? defaultGrepOperations;
|
||||
this.description = renderPromptTemplate(grepDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a pattern against ripgrep's regex engine.
|
||||
* Uses a quick dry-run against the null device to check for parse errors.
|
||||
*/
|
||||
private async validateRegexPattern(pattern: string, rgPath?: string): Promise<{ valid: boolean; error?: string }> {
|
||||
if (!rgPath) {
|
||||
return { valid: true }; // Can't validate, assume valid
|
||||
}
|
||||
|
||||
// Run ripgrep against the null device with the pattern - this validates regex syntax
|
||||
// without searching any files
|
||||
const nullDevice = process.platform === "win32" ? "NUL" : "/dev/null";
|
||||
const result = await ptree.exec([rgPath, "--no-config", "--quiet", "--", pattern, nullDevice], {
|
||||
allowNonZero: true,
|
||||
allowAbort: true,
|
||||
});
|
||||
|
||||
// Exit code 1 = no matches (pattern is valid), 0 = matches found
|
||||
// Exit code 2 = error (often regex parse error)
|
||||
if (result.exitCode === 2 && result.stderr.includes("regex parse error")) {
|
||||
return { valid: false, error: result.stderr.trim() };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
public async execute(
|
||||
_toolCallId: string,
|
||||
params: GrepParams,
|
||||
signal?: AbortSignal,
|
||||
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
||||
toolContext?: AgentToolContext,
|
||||
_toolContext?: AgentToolContext,
|
||||
): Promise<AgentToolResult<GrepToolDetails>> {
|
||||
const { pattern, path: searchDir, glob, type, output_mode, i, n, context, multiline, limit, offset } = params;
|
||||
|
||||
@@ -189,38 +105,11 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
}
|
||||
const normalizedLimit = rawLimit !== undefined && rawLimit > 0 ? rawLimit : undefined;
|
||||
|
||||
const normalizeContext = (value: number | undefined, label: string): number => {
|
||||
if (value === undefined) return 0;
|
||||
const normalized = Number.isFinite(value) ? Math.floor(value) : Number.NaN;
|
||||
if (!Number.isFinite(normalized) || normalized < 0) {
|
||||
throw new ToolError(`${label} must be a non-negative number`);
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const normalizedContext = normalizeContext(context ?? 5, "Context");
|
||||
const normalizedContext = context ?? 5;
|
||||
const showLineNumbers = n ?? true;
|
||||
const ignoreCase = i ?? false;
|
||||
const normalizedGlob = glob?.trim() ?? "";
|
||||
const normalizedType = type?.trim() ?? "";
|
||||
const hasContentHints = limit !== undefined || context !== undefined;
|
||||
|
||||
// Validate regex patterns early to surface parse errors before running rg
|
||||
const rgPath = await ensureTool("rg", {
|
||||
silent: true,
|
||||
notify: message => toolContext?.ui?.notify(message, "info"),
|
||||
});
|
||||
|
||||
if (!rgPath) {
|
||||
throw new ToolError("rg is not available and could not be downloaded");
|
||||
}
|
||||
|
||||
const validation = await this.validateRegexPattern(normalizedPattern, rgPath);
|
||||
if (!validation.valid) {
|
||||
throw new ToolError(validation.error ?? "Invalid regex pattern");
|
||||
}
|
||||
|
||||
// rgPath resolved earlier
|
||||
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
|
||||
const scopePath = (() => {
|
||||
const relative = nodePath.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
||||
@@ -229,98 +118,50 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
|
||||
let isDirectory: boolean;
|
||||
try {
|
||||
isDirectory = await this.ops.isDirectory(searchPath);
|
||||
const stat = await Bun.file(searchPath).stat();
|
||||
isDirectory = stat.isDirectory();
|
||||
} catch {
|
||||
throw new ToolError(`Path not found: ${searchPath}`);
|
||||
}
|
||||
const effectiveOutputMode =
|
||||
output_mode ?? (!isDirectory || hasContentHints ? "content" : "files_with_matches");
|
||||
const effectiveOffset = normalizedOffset > 0 ? normalizedOffset : 0;
|
||||
|
||||
const effectiveOutputMode = output_mode ?? (!isDirectory || hasContentHints ? "content" : "filesWithMatches");
|
||||
const effectiveLimit =
|
||||
effectiveOutputMode === "content" ? (normalizedLimit ?? DEFAULT_MATCH_LIMIT) : normalizedLimit;
|
||||
|
||||
// Run WASM grep
|
||||
let result: Awaited<ReturnType<typeof wasmGrep>>;
|
||||
try {
|
||||
result = await wasmGrep({
|
||||
pattern: normalizedPattern,
|
||||
path: searchPath,
|
||||
glob: glob?.trim() || undefined,
|
||||
type: type?.trim() || undefined,
|
||||
ignoreCase,
|
||||
multiline: multiline ?? false,
|
||||
hidden: true,
|
||||
maxCount: effectiveLimit,
|
||||
offset: normalizedOffset > 0 ? normalizedOffset : undefined,
|
||||
context: effectiveOutputMode === "content" ? normalizedContext : undefined,
|
||||
maxColumns: DEFAULT_MAX_COLUMN,
|
||||
mode: effectiveOutputMode,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.startsWith("regex parse error")) {
|
||||
throw new ToolError(err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const formatPath = (filePath: string): string => {
|
||||
// WASM returns paths starting with / (the virtual root)
|
||||
const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
||||
if (isDirectory) {
|
||||
const relative = nodePath.relative(searchPath, filePath);
|
||||
if (relative && !relative.startsWith("..")) {
|
||||
return relative.replace(/\\/g, "/");
|
||||
}
|
||||
return cleanPath.replace(/\\/g, "/");
|
||||
}
|
||||
return nodePath.basename(filePath);
|
||||
return nodePath.basename(cleanPath);
|
||||
};
|
||||
|
||||
const fileCache = new Map<string, Promise<string[]>>();
|
||||
const getFileLines = async (filePath: string): Promise<string[]> => {
|
||||
let linesPromise = fileCache.get(filePath);
|
||||
if (!linesPromise) {
|
||||
linesPromise = (async () => {
|
||||
try {
|
||||
const content = await this.ops.readFile(filePath);
|
||||
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
fileCache.set(filePath, linesPromise);
|
||||
}
|
||||
return linesPromise;
|
||||
};
|
||||
|
||||
const args: string[] = [];
|
||||
|
||||
// Ignore user config files for consistent behavior
|
||||
args.push("--no-config");
|
||||
|
||||
// Base arguments depend on output mode
|
||||
if (effectiveOutputMode === "files_with_matches") {
|
||||
args.push("--files-with-matches", "--color=never");
|
||||
} else if (effectiveOutputMode === "count") {
|
||||
args.push("--count", "--color=never");
|
||||
} else {
|
||||
args.push("--json", "--color=never");
|
||||
if (showLineNumbers) {
|
||||
args.push("--line-number");
|
||||
}
|
||||
}
|
||||
|
||||
args.push("--hidden");
|
||||
|
||||
if (ignoreCase) {
|
||||
args.push("--ignore-case");
|
||||
} else {
|
||||
args.push("--case-sensitive");
|
||||
}
|
||||
|
||||
if (multiline) {
|
||||
args.push("--multiline");
|
||||
}
|
||||
|
||||
if (normalizedGlob) {
|
||||
args.push("--glob", normalizedGlob);
|
||||
}
|
||||
|
||||
args.push("--glob", "!**/.git/**");
|
||||
args.push("--glob", "!**/node_modules/**");
|
||||
|
||||
if (normalizedType) {
|
||||
args.push("--type", normalizedType);
|
||||
}
|
||||
|
||||
if (effectiveOutputMode === "content") {
|
||||
if (normalizedContext > 0) {
|
||||
args.push("-C", String(normalizedContext));
|
||||
}
|
||||
}
|
||||
|
||||
args.push("--", normalizedPattern, searchPath);
|
||||
|
||||
using child = ptree.spawn([rgPath, ...args], { signal });
|
||||
|
||||
let matchCount = 0;
|
||||
let matchLimitReached = false;
|
||||
let linesTruncated = false;
|
||||
let killedDueToLimit = false;
|
||||
const outputLines: string[] = [];
|
||||
// Build output
|
||||
const files = new Set<string>();
|
||||
const fileList: string[] = [];
|
||||
const fileMatchCounts = new Map<string, number>();
|
||||
@@ -333,289 +174,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
}
|
||||
};
|
||||
|
||||
const recordFileMatch = (filePath: string) => {
|
||||
const relative = formatPath(filePath);
|
||||
fileMatchCounts.set(relative, (fileMatchCounts.get(relative) ?? 0) + 1);
|
||||
};
|
||||
|
||||
// For simple output modes (files_with_matches, count), process text directly
|
||||
if (effectiveOutputMode === "files_with_matches" || effectiveOutputMode === "count") {
|
||||
const stdout = await child.text().catch(x => {
|
||||
if (x instanceof ptree.Exception && x.exitCode === 1) {
|
||||
return "";
|
||||
}
|
||||
return Promise.reject(x);
|
||||
});
|
||||
|
||||
const exitCode = child.exitCode ?? 0;
|
||||
if (exitCode !== 0 && exitCode !== 1) {
|
||||
const errorMsg = child.peekStderr().trim() || `ripgrep exited with code ${exitCode}`;
|
||||
throw new ToolError(errorMsg);
|
||||
}
|
||||
|
||||
const lines = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(line => line.length > 0);
|
||||
|
||||
if (lines.length === 0) {
|
||||
const details: GrepToolDetails = {
|
||||
scopePath,
|
||||
matchCount: 0,
|
||||
fileCount: 0,
|
||||
files: [],
|
||||
mode: effectiveOutputMode,
|
||||
truncated: false,
|
||||
};
|
||||
return toolResult(details).text("No matches found").done();
|
||||
}
|
||||
|
||||
const offsetLines = effectiveOffset > 0 ? lines.slice(effectiveOffset) : lines;
|
||||
const listLimit = applyListLimit(offsetLines, {
|
||||
limit: normalizedLimit,
|
||||
limitType: "result",
|
||||
});
|
||||
const processedLines = listLimit.items;
|
||||
const limitMeta = listLimit.meta;
|
||||
|
||||
let simpleMatchCount = 0;
|
||||
let fileCount = 0;
|
||||
const simpleFiles = new Set<string>();
|
||||
const simpleFileList: string[] = [];
|
||||
const simpleFileMatchCounts = new Map<string, number>();
|
||||
|
||||
const recordSimpleFile = (filePath: string) => {
|
||||
const relative = formatPath(filePath);
|
||||
if (!simpleFiles.has(relative)) {
|
||||
simpleFiles.add(relative);
|
||||
simpleFileList.push(relative);
|
||||
}
|
||||
};
|
||||
|
||||
// Count mode: ripgrep provides total count per file, so we set directly (not increment)
|
||||
const setFileMatchCount = (filePath: string, count: number) => {
|
||||
const relative = formatPath(filePath);
|
||||
simpleFileMatchCounts.set(relative, count);
|
||||
};
|
||||
|
||||
if (effectiveOutputMode === "files_with_matches") {
|
||||
for (const line of processedLines) {
|
||||
recordSimpleFile(line);
|
||||
}
|
||||
fileCount = simpleFiles.size;
|
||||
simpleMatchCount = fileCount;
|
||||
} else {
|
||||
for (const line of processedLines) {
|
||||
const separatorIndex = line.lastIndexOf(":");
|
||||
const filePart = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
||||
const countPart = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
||||
const count = Number.parseInt(countPart, 10);
|
||||
recordSimpleFile(filePart);
|
||||
if (!Number.isNaN(count)) {
|
||||
simpleMatchCount += count;
|
||||
setFileMatchCount(filePart, count);
|
||||
}
|
||||
}
|
||||
fileCount = simpleFiles.size;
|
||||
}
|
||||
|
||||
const truncatedByLimit = Boolean(limitMeta.resultLimit);
|
||||
|
||||
// For count mode, format as "path:count"
|
||||
if (effectiveOutputMode === "count") {
|
||||
const formatted = processedLines.map(line => {
|
||||
const separatorIndex = line.lastIndexOf(":");
|
||||
const relative = formatPath(separatorIndex === -1 ? line : line.slice(0, separatorIndex));
|
||||
const count = separatorIndex === -1 ? "0" : line.slice(separatorIndex + 1);
|
||||
return `${relative}:${count}`;
|
||||
});
|
||||
const output = formatted.join("\n");
|
||||
const details: GrepToolDetails = {
|
||||
scopePath,
|
||||
matchCount: simpleMatchCount,
|
||||
fileCount,
|
||||
files: simpleFileList,
|
||||
fileMatches: simpleFileList.map(path => ({
|
||||
path,
|
||||
count: simpleFileMatchCounts.get(path) ?? 0,
|
||||
})),
|
||||
mode: effectiveOutputMode,
|
||||
truncated: truncatedByLimit,
|
||||
resultLimitReached: limitMeta.resultLimit?.reached,
|
||||
};
|
||||
return toolResult(details)
|
||||
.text(output)
|
||||
.limits({
|
||||
resultLimit: limitMeta.resultLimit?.reached,
|
||||
})
|
||||
.done();
|
||||
}
|
||||
|
||||
// For files_with_matches, format paths
|
||||
const formatted = processedLines.map(line => formatPath(line));
|
||||
const output = formatted.join("\n");
|
||||
const details: GrepToolDetails = {
|
||||
scopePath,
|
||||
matchCount: simpleMatchCount,
|
||||
fileCount,
|
||||
files: simpleFileList,
|
||||
mode: effectiveOutputMode,
|
||||
truncated: truncatedByLimit,
|
||||
resultLimitReached: limitMeta.resultLimit?.reached,
|
||||
};
|
||||
return toolResult(details)
|
||||
.text(output)
|
||||
.limits({
|
||||
resultLimit: limitMeta.resultLimit?.reached,
|
||||
})
|
||||
.done();
|
||||
}
|
||||
|
||||
// Content mode - existing JSON processing
|
||||
const formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {
|
||||
const relativePath = formatPath(filePath);
|
||||
const lines = await getFileLines(filePath);
|
||||
if (!lines.length) {
|
||||
return showLineNumbers
|
||||
? [`${relativePath}:${lineNumber}: (unable to read file)`]
|
||||
: [`${relativePath}: (unable to read file)`];
|
||||
}
|
||||
|
||||
const block: string[] = [];
|
||||
const start = normalizedContext > 0 ? Math.max(1, lineNumber - normalizedContext) : lineNumber;
|
||||
const end = normalizedContext > 0 ? Math.min(lines.length, lineNumber + normalizedContext) : lineNumber;
|
||||
|
||||
for (let current = start; current <= end; current++) {
|
||||
const lineText = lines[current - 1] ?? "";
|
||||
const sanitized = lineText.replace(/\r/g, "");
|
||||
const isMatchLine = current === lineNumber;
|
||||
|
||||
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
||||
if (wasTruncated) {
|
||||
linesTruncated = true;
|
||||
}
|
||||
|
||||
if (isMatchLine) {
|
||||
block.push(
|
||||
showLineNumbers
|
||||
? `${relativePath}:${current}: ${truncatedText}`
|
||||
: `${relativePath}: ${truncatedText}`,
|
||||
);
|
||||
} else {
|
||||
block.push(
|
||||
showLineNumbers
|
||||
? `${relativePath}-${current}- ${truncatedText}`
|
||||
: `${relativePath}- ${truncatedText}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return block;
|
||||
};
|
||||
|
||||
const maxMatches = effectiveLimit !== undefined ? effectiveLimit + effectiveOffset : undefined;
|
||||
const processEvent = async (event: unknown): Promise<void> => {
|
||||
if (!event || typeof event !== "object") {
|
||||
return;
|
||||
}
|
||||
const parsed = event as { type?: string; data?: { path?: { text?: string }; line_number?: number } };
|
||||
if (parsed.type !== "match") {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = matchCount + 1;
|
||||
if (maxMatches !== undefined && nextIndex > maxMatches) {
|
||||
matchLimitReached = true;
|
||||
killedDueToLimit = true;
|
||||
return;
|
||||
}
|
||||
|
||||
matchCount = nextIndex;
|
||||
const filePath = parsed.data?.path?.text;
|
||||
const lineNumber = parsed.data?.line_number;
|
||||
|
||||
if (filePath && typeof lineNumber === "number") {
|
||||
if (matchCount <= effectiveOffset) {
|
||||
return;
|
||||
}
|
||||
recordFile(filePath);
|
||||
recordFileMatch(filePath);
|
||||
const block = await formatBlock(filePath, lineNumber);
|
||||
outputLines.push(...block);
|
||||
}
|
||||
};
|
||||
|
||||
let buffer = "";
|
||||
const parseBuffer = async () => {
|
||||
while (buffer.length > 0) {
|
||||
const result = Bun.JSONL.parseChunk(buffer);
|
||||
for (const value of result.values) {
|
||||
await processEvent(value);
|
||||
}
|
||||
|
||||
if (result.read > 0) {
|
||||
buffer = buffer.slice(result.read);
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
const nextNewline = buffer.indexOf("\n");
|
||||
if (nextNewline === -1) {
|
||||
buffer = "";
|
||||
break;
|
||||
}
|
||||
buffer = buffer.slice(nextNewline + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.read === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Process stdout stream with JSONL chunk parsing
|
||||
const decoder = new TextDecoder();
|
||||
try {
|
||||
for await (const chunk of child.stdout) {
|
||||
if (killedDueToLimit) {
|
||||
break;
|
||||
}
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
await parseBuffer();
|
||||
}
|
||||
if (!killedDueToLimit) {
|
||||
buffer += decoder.decode();
|
||||
await parseBuffer();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ptree.Exception && err.aborted) {
|
||||
throw new ToolAbortError();
|
||||
}
|
||||
// Stream may close early if we killed due to limit - that's ok
|
||||
if (!killedDueToLimit) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for process to exit
|
||||
try {
|
||||
await child.exitedCleanly;
|
||||
} catch (err) {
|
||||
if (err instanceof ptree.Exception) {
|
||||
if (err.aborted) {
|
||||
throw new ToolAbortError();
|
||||
}
|
||||
// Non-zero exit is ok if we killed due to limit or exit code 1 (no matches)
|
||||
if (!killedDueToLimit && err.exitCode !== 1) {
|
||||
const errorMsg = child.peekStderr().trim() || `ripgrep exited with code ${err.exitCode}`;
|
||||
throw new ToolError(errorMsg);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchCount === 0) {
|
||||
if (result.totalMatches === 0) {
|
||||
const details: GrepToolDetails = {
|
||||
scopePath,
|
||||
matchCount: 0,
|
||||
@@ -627,21 +186,76 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
return toolResult(details).text("No matches found").done();
|
||||
}
|
||||
|
||||
const limitMeta =
|
||||
matchLimitReached && effectiveLimit !== undefined
|
||||
? { matchLimit: { reached: effectiveLimit, suggestion: effectiveLimit * 2 } }
|
||||
: {};
|
||||
let outputLines: string[] = [];
|
||||
let linesTruncated = false;
|
||||
|
||||
for (const match of result.matches) {
|
||||
recordFile(match.path);
|
||||
const relativePath = formatPath(match.path);
|
||||
|
||||
if (effectiveOutputMode === "content") {
|
||||
// Add context before
|
||||
if (match.contextBefore) {
|
||||
for (const ctx of match.contextBefore) {
|
||||
outputLines.push(
|
||||
showLineNumbers
|
||||
? `${relativePath}-${ctx.lineNumber}- ${ctx.line}`
|
||||
: `${relativePath}- ${ctx.line}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add match line
|
||||
outputLines.push(
|
||||
showLineNumbers
|
||||
? `${relativePath}:${match.lineNumber}: ${match.line}`
|
||||
: `${relativePath}: ${match.line}`,
|
||||
);
|
||||
|
||||
if (match.truncated) {
|
||||
linesTruncated = true;
|
||||
}
|
||||
|
||||
// Add context after
|
||||
if (match.contextAfter) {
|
||||
for (const ctx of match.contextAfter) {
|
||||
outputLines.push(
|
||||
showLineNumbers
|
||||
? `${relativePath}-${ctx.lineNumber}- ${ctx.line}`
|
||||
: `${relativePath}- ${ctx.line}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Track per-file counts
|
||||
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
||||
} else if (effectiveOutputMode === "filesWithMatches") {
|
||||
// One line per file
|
||||
const matchWithCount = match as WasmGrepMatch & { matchCount?: number };
|
||||
fileMatchCounts.set(relativePath, matchWithCount.matchCount ?? 1);
|
||||
} else {
|
||||
// count mode
|
||||
const matchWithCount = match as WasmGrepMatch & { matchCount?: number };
|
||||
fileMatchCounts.set(relativePath, matchWithCount.matchCount ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Format output based on mode
|
||||
if (effectiveOutputMode === "filesWithMatches") {
|
||||
outputLines = fileList;
|
||||
} else if (effectiveOutputMode === "count") {
|
||||
outputLines = fileList.map(f => `${f}:${fileMatchCounts.get(f) ?? 0}`);
|
||||
}
|
||||
|
||||
// Apply byte truncation (no line limit since we already have match limit)
|
||||
const rawOutput = outputLines.join("\n");
|
||||
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
||||
|
||||
const output = truncation.content;
|
||||
const truncated = Boolean(matchLimitReached || truncation.truncated || limitMeta.matchLimit || linesTruncated);
|
||||
|
||||
const truncated = Boolean(result.limitReached || truncation.truncated || linesTruncated);
|
||||
const details: GrepToolDetails = {
|
||||
scopePath,
|
||||
matchCount: effectiveOffset > 0 ? Math.max(0, matchCount - effectiveOffset) : matchCount,
|
||||
fileCount: files.size,
|
||||
matchCount: result.totalMatches,
|
||||
fileCount: result.filesWithMatches,
|
||||
files: fileList,
|
||||
fileMatches: fileList.map(path => ({
|
||||
path,
|
||||
@@ -649,22 +263,19 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
||||
})),
|
||||
mode: effectiveOutputMode,
|
||||
truncated,
|
||||
matchLimitReached: limitMeta.matchLimit?.reached,
|
||||
matchLimitReached: result.limitReached ? effectiveLimit : undefined,
|
||||
};
|
||||
|
||||
// Keep TUI compatibility fields
|
||||
if (matchLimitReached && effectiveLimit !== undefined) {
|
||||
details.matchLimitReached = effectiveLimit;
|
||||
}
|
||||
if (truncation.truncated) details.truncation = truncation;
|
||||
if (linesTruncated) details.linesTruncated = true;
|
||||
|
||||
const resultBuilder = toolResult(details)
|
||||
.text(output)
|
||||
.limits({
|
||||
matchLimit: limitMeta.matchLimit?.reached,
|
||||
matchLimit: result.limitReached ? effectiveLimit : undefined,
|
||||
columnMax: linesTruncated ? DEFAULT_MAX_COLUMN : undefined,
|
||||
});
|
||||
|
||||
if (truncation.truncated) {
|
||||
resultBuilder.truncation(truncation, { direction: "head" });
|
||||
}
|
||||
@@ -702,7 +313,7 @@ export const grepToolRenderer = {
|
||||
if (args.path) meta.push(`in ${args.path}`);
|
||||
if (args.glob) meta.push(`glob:${args.glob}`);
|
||||
if (args.type) meta.push(`type:${args.type}`);
|
||||
if (args.output_mode && args.output_mode !== "files_with_matches") meta.push(`mode:${args.output_mode}`);
|
||||
if (args.output_mode && args.output_mode !== "filesWithMatches") meta.push(`mode:${args.output_mode}`);
|
||||
if (args.i) meta.push("case:insensitive");
|
||||
if (args.n === false) meta.push("no-line-numbers");
|
||||
if (args.context !== undefined && args.context > 0) meta.push(`context:${args.context}`);
|
||||
@@ -758,7 +369,7 @@ export const grepToolRenderer = {
|
||||
|
||||
const matchCount = details?.matchCount ?? 0;
|
||||
const fileCount = details?.fileCount ?? 0;
|
||||
const mode = details?.mode ?? "files_with_matches";
|
||||
const mode = details?.mode ?? "filesWithMatches";
|
||||
const truncation = details?.meta?.truncation;
|
||||
const limits = details?.meta?.limits;
|
||||
const truncated = Boolean(
|
||||
@@ -775,7 +386,7 @@ export const grepToolRenderer = {
|
||||
}
|
||||
|
||||
const summaryParts =
|
||||
mode === "files_with_matches"
|
||||
mode === "filesWithMatches"
|
||||
? [formatCount("file", fileCount)]
|
||||
: [formatCount("match", matchCount), formatCount("file", fileCount)];
|
||||
const meta = [...summaryParts];
|
||||
|
||||
@@ -2,9 +2,10 @@ import * as os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { find as wasmFind } from "@oh-my-pi/pi-natives";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import { globPaths, ptree } from "@oh-my-pi/pi-utils";
|
||||
import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { CONFIG_DIR_NAME } from "../config";
|
||||
import { renderPromptTemplate } from "../config/prompt-templates";
|
||||
@@ -165,27 +166,31 @@ async function listCandidateFiles(
|
||||
_notify?: (message: string) => void,
|
||||
): Promise<{ files: string[]; truncated: boolean; error?: string }> {
|
||||
let files: string[];
|
||||
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
||||
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
||||
try {
|
||||
files = await globPaths("**/*", {
|
||||
cwd: searchRoot,
|
||||
gitignore: true,
|
||||
dot: true,
|
||||
signal,
|
||||
timeoutMs: GLOB_TIMEOUT_MS,
|
||||
});
|
||||
const result = await untilAborted(combinedSignal, () =>
|
||||
wasmFind({
|
||||
pattern: "**/*",
|
||||
path: searchRoot,
|
||||
fileType: "file",
|
||||
hidden: true,
|
||||
}),
|
||||
);
|
||||
files = result.matches.map(match => match.path);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
if (timeoutSignal.aborted && !signal?.aborted) {
|
||||
const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
|
||||
return { files: [], truncated: false, error: `find timed out after ${timeoutSeconds}s` };
|
||||
}
|
||||
throw new ToolAbortError();
|
||||
}
|
||||
if (error instanceof Error && error.name === "TimeoutError") {
|
||||
const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
|
||||
return { files: [], truncated: false, error: `glob timed out after ${timeoutSeconds}s` };
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return { files: [], truncated: false, error: message };
|
||||
}
|
||||
|
||||
const normalizedFiles = files.map(line => line.replace(/\r$/, "").trim()).filter(line => line.length > 0);
|
||||
const normalizedFiles = files.filter(line => line.length > 0);
|
||||
const truncated = normalizedFiles.length > MAX_FUZZY_CANDIDATES;
|
||||
const limited = truncated ? normalizedFiles.slice(0, MAX_FUZZY_CANDIDATES) : normalizedFiles;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as photon from "../vendor/photon";
|
||||
import { PhotonImage } from "@oh-my-pi/pi-natives";
|
||||
|
||||
/**
|
||||
* Convert image to PNG format for terminal display.
|
||||
@@ -14,16 +14,12 @@ export async function convertToPng(
|
||||
}
|
||||
|
||||
try {
|
||||
const image = photon.PhotonImage.new_from_byteslice(new Uint8Array(Buffer.from(base64Data, "base64")));
|
||||
try {
|
||||
const pngBuffer = image.get_bytes();
|
||||
return {
|
||||
data: Buffer.from(pngBuffer).toString("base64"),
|
||||
mimeType: "image/png",
|
||||
};
|
||||
} finally {
|
||||
image.free();
|
||||
}
|
||||
using image = await PhotonImage.new_from_byteslice(new Uint8Array(Buffer.from(base64Data, "base64")));
|
||||
const pngBuffer = await image.get_bytes();
|
||||
return {
|
||||
data: Buffer.from(pngBuffer).toString("base64"),
|
||||
mimeType: "image/png",
|
||||
};
|
||||
} catch {
|
||||
// Conversion failed
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
||||
import * as photon from "../vendor/photon";
|
||||
import { PhotonImage, resize, SamplingFilter } from "@oh-my-pi/pi-natives";
|
||||
|
||||
export interface ImageResizeOptions {
|
||||
maxWidth?: number; // Default: 2000
|
||||
@@ -53,9 +53,8 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const inputBuffer = Buffer.from(img.data, "base64");
|
||||
|
||||
let image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;
|
||||
try {
|
||||
image = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));
|
||||
using image = await PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));
|
||||
|
||||
const originalWidth = image.get_width();
|
||||
const originalHeight = image.get_height();
|
||||
@@ -89,24 +88,19 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
||||
}
|
||||
|
||||
// Helper to resize and encode in both formats, returning the smaller one
|
||||
function tryBothFormats(
|
||||
async function tryBothFormats(
|
||||
width: number,
|
||||
height: number,
|
||||
jpegQuality: number,
|
||||
): { buffer: Uint8Array; mimeType: string } {
|
||||
const resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3);
|
||||
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
||||
using resized = await resize(image!, width, height, SamplingFilter.Lanczos3);
|
||||
|
||||
try {
|
||||
const pngBuffer = resized.get_bytes();
|
||||
const jpegBuffer = resized.get_bytes_jpeg(jpegQuality);
|
||||
const [pngBuffer, jpegBuffer] = await Promise.all([resized.get_bytes(), resized.get_bytes_jpeg(jpegQuality)]);
|
||||
|
||||
return pickSmaller(
|
||||
{ buffer: pngBuffer, mimeType: "image/png" },
|
||||
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
||||
);
|
||||
} finally {
|
||||
resized.free();
|
||||
}
|
||||
return pickSmaller(
|
||||
{ buffer: pngBuffer, mimeType: "image/png" },
|
||||
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
||||
);
|
||||
}
|
||||
|
||||
// Try to produce an image under maxBytes
|
||||
@@ -118,7 +112,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
||||
let finalHeight = targetHeight;
|
||||
|
||||
// First attempt: resize to target dimensions, try both formats
|
||||
best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
||||
best = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
||||
|
||||
if (best.buffer.length <= opts.maxBytes) {
|
||||
return {
|
||||
@@ -134,7 +128,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
||||
|
||||
// Still too large - try JPEG with decreasing quality
|
||||
for (const quality of qualitySteps) {
|
||||
best = tryBothFormats(targetWidth, targetHeight, quality);
|
||||
best = await tryBothFormats(targetWidth, targetHeight, quality);
|
||||
|
||||
if (best.buffer.length <= opts.maxBytes) {
|
||||
return {
|
||||
@@ -159,7 +153,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
||||
}
|
||||
|
||||
for (const quality of qualitySteps) {
|
||||
best = tryBothFormats(finalWidth, finalHeight, quality);
|
||||
best = await tryBothFormats(finalWidth, finalHeight, quality);
|
||||
|
||||
if (best.buffer.length <= opts.maxBytes) {
|
||||
return {
|
||||
@@ -196,10 +190,6 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
||||
height: 0,
|
||||
wasResized: false,
|
||||
};
|
||||
} finally {
|
||||
if (image) {
|
||||
image.free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { logger, TempDir } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import { APP_NAME, getBinDir } from "../config";
|
||||
import { APP_NAME, getToolsDir } from "../config";
|
||||
|
||||
const TOOLS_DIR = getBinDir();
|
||||
const TOOLS_DIR = getToolsDir();
|
||||
const TOOL_DOWNLOAD_TIMEOUT_MS = 15000;
|
||||
|
||||
interface ToolConfig {
|
||||
@@ -18,46 +18,6 @@ interface ToolConfig {
|
||||
}
|
||||
|
||||
const TOOLS: Record<string, ToolConfig> = {
|
||||
fd: {
|
||||
name: "fd",
|
||||
repo: "sharkdp/fd",
|
||||
binaryName: "fd",
|
||||
tagPrefix: "v",
|
||||
getAssetName: (version, plat, architecture) => {
|
||||
if (plat === "darwin") {
|
||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
||||
return `fd-v${version}-${archStr}-apple-darwin.tar.gz`;
|
||||
} else if (plat === "linux") {
|
||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
||||
return `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;
|
||||
} else if (plat === "win32") {
|
||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
||||
return `fd-v${version}-${archStr}-pc-windows-msvc.zip`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
rg: {
|
||||
name: "ripgrep",
|
||||
repo: "BurntSushi/ripgrep",
|
||||
binaryName: "rg",
|
||||
tagPrefix: "",
|
||||
getAssetName: (version, plat, architecture) => {
|
||||
if (plat === "darwin") {
|
||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
||||
return `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;
|
||||
} else if (plat === "linux") {
|
||||
if (architecture === "arm64") {
|
||||
return `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;
|
||||
}
|
||||
return `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;
|
||||
} else if (plat === "win32") {
|
||||
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
||||
return `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
sd: {
|
||||
name: "sd",
|
||||
repo: "chmln/sd",
|
||||
@@ -135,7 +95,7 @@ const PYTHON_TOOLS: Record<string, PythonToolConfig> = {
|
||||
},
|
||||
};
|
||||
|
||||
export type ToolName = "fd" | "rg" | "sd" | "sg" | "yt-dlp" | "markitdown" | "html2text";
|
||||
export type ToolName = "sd" | "sg" | "yt-dlp" | "markitdown" | "html2text";
|
||||
|
||||
// Get the path to a tool (system-wide or in our tools dir)
|
||||
export async function getToolPath(tool: ToolName): Promise<string | null> {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { abortableSleep, once, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2023, Silvia O'Dwyer
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="" rel="noopener">
|
||||
<img src="https://i.imgur.com/GxrKNOb.png" alt="Photon banner, showing the Photon logo on a dark background"></a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[]()
|
||||
[](https://github.com/silvia-odwyer/photon/issues)
|
||||
[](https://github.com/silvia-odwyer/p/pulls)
|
||||
[](https://gitter.im/photonlibrary/community "Gitter chat")
|
||||
[](https://crates.io/crates/photon-rs)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<p align="center"> High-performance, cross-platform Rust/WebAssembly image processing library
|
||||
<br>
|
||||
</p>
|
||||
|
||||
## 📝 Table of Contents
|
||||
- [Get Started with WebAssembly](https://github.com/silvia-odwyer/photon#-get-started-with-webassembly)
|
||||
- [Get Started Natively](https://github.com/silvia-odwyer/photon#getting-started)
|
||||
- [Documentation](https://docs.rs/photon-rs/0.1.0/)
|
||||
- [Official Website.](https://silvia-odwyer.github.io/photon/)
|
||||
- [All Available Functions.](https://silvia-odwyer.github.io/photon/docs/photon/all.html)
|
||||
- [Got Questions? Ask Here!](https://github.com/silvia-odwyer/photon#got-questions)
|
||||
|
||||
Photon is a high-performance Rust image processing library, which compiles to WebAssembly, allowing for
|
||||
safe, blazing-fast image processing both natively and on the web.
|
||||
|
||||
### Features
|
||||
- **Fast:** Photon outperforms even the fastest of libraries, including ImageMagick. On the web, its high-performance allows for near-native-speed image processing on the web. Benchmarks coming soon.
|
||||
- **Call with JS:** Want to use Photon on the web or with Node? Using a simple npm package, you're good to go. Get all the benefits of WebAssembly
|
||||
with zero-cost abstraction.
|
||||
- **Use Natively:** For command-line apps, native photo editing apps, and so forth, Photon's core codebase is in Rust, allowing for cross-platform
|
||||
development.
|
||||
|
||||
### Live Demo
|
||||
View the [official demo of WASM in action](https://silvia-odwyer.github.io/photon/demo.html).
|
||||
|
||||
### Get Started
|
||||
#### Getting Started Guide
|
||||
Check out Photon's [getting started guide, complete with tutorials, installation instructions, and more](https://silvia-odwyer.github.io/photon/guide)
|
||||
|
||||
#### Documentation
|
||||
### 📚 Documentation
|
||||
View the [official documentation](https://docs.rs/photon-rs/).
|
||||
|
||||
### Photon In Action
|
||||
|
||||

|
||||
|
||||
### Functionality
|
||||
96+ customisable functions are available.
|
||||
|
||||
Resize, transform, correct, and filter images: apply as many effects as desired.
|
||||
|
||||
Functions include:
|
||||
- **Image correction**: Hue rotation, sharpening, brightness adjustment, adjusting saturation, lightening/darkening all within various colour spaces.
|
||||
- **Convolutions**: Sobel filters, blurs, Laplace effects, edge detection, etc.,
|
||||
- **Channel manipulation**: Increasing/decreasing RGB channel values, swapping channels, removing channels, etc.
|
||||
- **Transform**: Resize, crop, rotate and flip images.
|
||||
- **Monochrome effects**: Duotoning, greyscaling of various forms, thresholding, sepia, averaging RGB values
|
||||
- **Colour manipulation**: Work with the image in various colour spaces such as HSL, LCh, and sRGB, and adjust the colours accordingly.
|
||||
- **Filters**: Over 30 pre-set filters available, incorporating various effects and transformations.
|
||||
- **Text**: Apply text to imagery in artistic ways, or to watermark, etc.,
|
||||
- **Watermarking**: Watermark images in multiple formats.
|
||||
- **Blending**: Blend images together using 10 different techniques, change image backgrounds.
|
||||
|
||||
## Install
|
||||
### Native
|
||||
Add the following line to the dependencies section of your Rust project's Cargo.toml:
|
||||
|
||||
###### Cargo.toml
|
||||
```toml
|
||||
[dependencies]
|
||||
photon-rs = "0.3.2"
|
||||
```
|
||||
|
||||
### Web
|
||||
Install Photon as an npm module:
|
||||
|
||||
```bash
|
||||
npm install --save @silvia-odwyer/photon
|
||||
```
|
||||
|
||||
### Node.JS
|
||||
To install Photon as an npm module for Node.JS use:
|
||||
|
||||
```bash
|
||||
npm install --save @silvia-odwyer/photon-node
|
||||
```
|
||||
|
||||
#### Using Photon Natively
|
||||
The following code opens an image from the filesystem, applies an effect, and saves it.
|
||||
|
||||
Here is a code sample to get you started:
|
||||
|
||||
```rs
|
||||
extern crate photon_rs;
|
||||
use photon_rs::native::{open_image, save_image};
|
||||
|
||||
fn main() {
|
||||
// Open the image (a PhotonImage is returned)
|
||||
let mut img = open_image("test_image.png");
|
||||
|
||||
// Increment the red channel by 40
|
||||
photon_rs::channels::alter_red_channel(&mut img, 40);
|
||||
|
||||
// Write file to filesystem.
|
||||
save_image(img, "raw_image.jpg");
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
[For more examples, check out the guide on how to get started with Photon natively.](https://silvia-odwyer.github.io/photon/guide/using-photon-natively/)
|
||||
|
||||
#### Using Photon On The Web
|
||||
|
||||
## Modules
|
||||
Photon contains a series of modules, which include:
|
||||
|
||||
- `effects`: Various image effects, including adding offsets, thresholding, duotoning, solarization, etc.,
|
||||
- `channels`: Functions related to increasing/decreasing the red, green, and blue channels of the image data.
|
||||
- `transform`: Resize, crop, flip, and rotate images.
|
||||
- `filters`: Preset filters, which alter the RGB channels of the image. Contains over 20.
|
||||
- `conv`: Laplace, Sobel, emboss; image proc functions which require image convolution.
|
||||
- `noise`: Noise generation of varying tints and hues.
|
||||
- `multiple`: A module for dealing with multiple images, such as watermarking images, etc.,
|
||||
- `correction`: Hue rotation, adjusting saturation, lightening/darkening: all techniques available in multiple colour spaces, which lead to varying effects.
|
||||
|
||||
All effects and filters can be viewed below and on the official website.
|
||||
|
||||
### Run Examples
|
||||
Clone this crate's official GitHub repo:
|
||||
```sh
|
||||
git clone https://github.com/silvia-odwyer/photon
|
||||
```
|
||||
|
||||
Run the binary, which will perform an image processing function on an image:
|
||||
```sh
|
||||
cd crate
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
Compare the original image with the outputted image, and you'll see the desired effect has been applied.
|
||||
|
||||
### Got Questions?
|
||||
If you'd like to chat to the developer about your potential use case, or have further questions about this library,
|
||||
just submit them here, and I'll get back to you!
|
||||
|
||||
- [Spectrum Chat](https://spectrum.chat/photonlibrary)
|
||||
- [Gitter](https://gitter.im/photonlibrary/community)
|
||||
|
||||
## License
|
||||
This project is licensed under the Apache 2.0 License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
-3013
File diff suppressed because it is too large
Load Diff
-4521
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e51491958652d0c0523cc2a974913d57aaf8bed537c0d244c01df32f9928b6f6
|
||||
size 1539011
|
||||
@@ -1,193 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const adjust_brightness: (a: number, b: number) => void;
|
||||
export const adjust_contrast: (a: number, b: number) => void;
|
||||
export const color_horizontal_strips: (a: number, b: number, c: number) => void;
|
||||
export const color_vertical_strips: (a: number, b: number, c: number) => void;
|
||||
export const colorize: (a: number) => void;
|
||||
export const dec_brightness: (a: number, b: number) => void;
|
||||
export const dither: (a: number, b: number) => void;
|
||||
export const duotone: (a: number, b: number, c: number) => void;
|
||||
export const frosted_glass: (a: number) => void;
|
||||
export const halftone: (a: number) => void;
|
||||
export const horizontal_strips: (a: number, b: number) => void;
|
||||
export const inc_brightness: (a: number, b: number) => void;
|
||||
export const multiple_offsets: (a: number, b: number, c: number, d: number) => void;
|
||||
export const normalize: (a: number) => void;
|
||||
export const offset: (a: number, b: number, c: number) => void;
|
||||
export const offset_blue: (a: number, b: number) => void;
|
||||
export const offset_green: (a: number, b: number) => void;
|
||||
export const offset_red: (a: number, b: number) => void;
|
||||
export const oil: (a: number, b: number, c: number) => void;
|
||||
export const pixelize: (a: number, b: number) => void;
|
||||
export const primary: (a: number) => void;
|
||||
export const solarize: (a: number) => void;
|
||||
export const solarize_retimg: (a: number) => number;
|
||||
export const tint: (a: number, b: number, c: number, d: number) => void;
|
||||
export const vertical_strips: (a: number, b: number) => void;
|
||||
export const apply_gradient: (a: number) => void;
|
||||
export const blend: (a: number, b: number, c: number, d: number) => void;
|
||||
export const create_gradient: (a: number, b: number) => number;
|
||||
export const watermark: (a: number, b: number, c: bigint, d: bigint) => void;
|
||||
export const b_grayscale: (a: number) => void;
|
||||
export const darken_hsl: (a: number, b: number) => void;
|
||||
export const darken_hsluv: (a: number, b: number) => void;
|
||||
export const darken_hsv: (a: number, b: number) => void;
|
||||
export const darken_lch: (a: number, b: number) => void;
|
||||
export const decompose_max: (a: number) => void;
|
||||
export const decompose_min: (a: number) => void;
|
||||
export const desaturate: (a: number) => void;
|
||||
export const desaturate_hsl: (a: number, b: number) => void;
|
||||
export const desaturate_hsluv: (a: number, b: number) => void;
|
||||
export const desaturate_hsv: (a: number, b: number) => void;
|
||||
export const desaturate_lch: (a: number, b: number) => void;
|
||||
export const draw_text: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
export const draw_text_with_border: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
export const g_grayscale: (a: number) => void;
|
||||
export const gamma_correction: (a: number, b: number, c: number, d: number) => void;
|
||||
export const grayscale: (a: number) => void;
|
||||
export const grayscale_human_corrected: (a: number) => void;
|
||||
export const grayscale_shades: (a: number, b: number) => void;
|
||||
export const hsl: (a: number, b: number, c: number, d: number) => void;
|
||||
export const hsluv: (a: number, b: number, c: number, d: number) => void;
|
||||
export const hsv: (a: number, b: number, c: number, d: number) => void;
|
||||
export const hue_rotate_hsl: (a: number, b: number) => void;
|
||||
export const hue_rotate_hsluv: (a: number, b: number) => void;
|
||||
export const hue_rotate_hsv: (a: number, b: number) => void;
|
||||
export const hue_rotate_lch: (a: number, b: number) => void;
|
||||
export const lch: (a: number, b: number, c: number, d: number) => void;
|
||||
export const lighten_hsl: (a: number, b: number) => void;
|
||||
export const lighten_hsluv: (a: number, b: number) => void;
|
||||
export const lighten_hsv: (a: number, b: number) => void;
|
||||
export const lighten_lch: (a: number, b: number) => void;
|
||||
export const mix_with_colour: (a: number, b: number, c: number) => void;
|
||||
export const monochrome: (a: number, b: number, c: number, d: number) => void;
|
||||
export const r_grayscale: (a: number) => void;
|
||||
export const saturate_hsl: (a: number, b: number) => void;
|
||||
export const saturate_hsluv: (a: number, b: number) => void;
|
||||
export const saturate_hsv: (a: number, b: number) => void;
|
||||
export const saturate_lch: (a: number, b: number) => void;
|
||||
export const sepia: (a: number) => void;
|
||||
export const single_channel_grayscale: (a: number, b: number) => void;
|
||||
export const threshold: (a: number, b: number) => void;
|
||||
export const crop: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||
export const crop_img_browser: (a: any, b: number, c: number, d: number, e: number) => any;
|
||||
export const fliph: (a: number) => void;
|
||||
export const flipv: (a: number) => void;
|
||||
export const padding_bottom: (a: number, b: number, c: number) => number;
|
||||
export const padding_left: (a: number, b: number, c: number) => number;
|
||||
export const padding_right: (a: number, b: number, c: number) => number;
|
||||
export const padding_top: (a: number, b: number, c: number) => number;
|
||||
export const padding_uniform: (a: number, b: number, c: number) => number;
|
||||
export const resample: (a: number, b: number, c: number) => number;
|
||||
export const resize: (a: number, b: number, c: number, d: number) => number;
|
||||
export const resize_img_browser: (a: number, b: number, c: number, d: number) => any;
|
||||
export const rotate: (a: number, b: number) => number;
|
||||
export const seam_carve: (a: number, b: number, c: number) => number;
|
||||
export const shearx: (a: number, b: number) => number;
|
||||
export const sheary: (a: number, b: number) => number;
|
||||
export const __wbg_photonimage_free: (a: number, b: number) => void;
|
||||
export const __wbg_rgb_free: (a: number, b: number) => void;
|
||||
export const __wbg_rgba_free: (a: number, b: number) => void;
|
||||
export const base64_to_image: (a: number, b: number) => number;
|
||||
export const base64_to_vec: (a: number, b: number) => [number, number];
|
||||
export const get_image_data: (a: any, b: any) => any;
|
||||
export const open_image: (a: any, b: any) => number;
|
||||
export const photonimage_get_base64: (a: number) => [number, number];
|
||||
export const photonimage_get_bytes: (a: number) => [number, number];
|
||||
export const photonimage_get_bytes_jpeg: (a: number, b: number) => [number, number];
|
||||
export const photonimage_get_bytes_webp: (a: number) => [number, number];
|
||||
export const photonimage_get_estimated_filesize: (a: number) => bigint;
|
||||
export const photonimage_get_height: (a: number) => number;
|
||||
export const photonimage_get_image_data: (a: number) => any;
|
||||
export const photonimage_get_raw_pixels: (a: number) => [number, number];
|
||||
export const photonimage_get_width: (a: number) => number;
|
||||
export const photonimage_new: (a: number, b: number, c: number, d: number) => number;
|
||||
export const photonimage_new_from_blob: (a: any) => number;
|
||||
export const photonimage_new_from_byteslice: (a: number, b: number) => number;
|
||||
export const photonimage_new_from_image: (a: any) => number;
|
||||
export const photonimage_set_imgdata: (a: number, b: any) => void;
|
||||
export const putImageData: (a: any, b: any, c: number) => void;
|
||||
export const rgb_get_blue: (a: number) => number;
|
||||
export const rgb_get_green: (a: number) => number;
|
||||
export const rgb_get_red: (a: number) => number;
|
||||
export const rgb_new: (a: number, b: number, c: number) => number;
|
||||
export const rgb_set_blue: (a: number, b: number) => void;
|
||||
export const rgb_set_green: (a: number, b: number) => void;
|
||||
export const rgb_set_red: (a: number, b: number) => void;
|
||||
export const rgba_get_alpha: (a: number) => number;
|
||||
export const rgba_new: (a: number, b: number, c: number, d: number) => number;
|
||||
export const rgba_set_alpha: (a: number, b: number) => void;
|
||||
export const run: () => [number, number];
|
||||
export const to_image_data: (a: number) => any;
|
||||
export const to_raw_pixels: (a: any) => [number, number];
|
||||
export const rgba_get_blue: (a: number) => number;
|
||||
export const rgba_get_green: (a: number) => number;
|
||||
export const rgba_get_red: (a: number) => number;
|
||||
export const photonimage_new_from_base64: (a: number, b: number) => number;
|
||||
export const rgba_set_blue: (a: number, b: number) => void;
|
||||
export const rgba_set_green: (a: number, b: number) => void;
|
||||
export const rgba_set_red: (a: number, b: number) => void;
|
||||
export const box_blur: (a: number) => void;
|
||||
export const detect_135_deg_lines: (a: number) => void;
|
||||
export const detect_45_deg_lines: (a: number) => void;
|
||||
export const detect_horizontal_lines: (a: number) => void;
|
||||
export const detect_vertical_lines: (a: number) => void;
|
||||
export const edge_detection: (a: number) => void;
|
||||
export const edge_one: (a: number) => void;
|
||||
export const emboss: (a: number) => void;
|
||||
export const gaussian_blur: (a: number, b: number) => void;
|
||||
export const identity: (a: number) => void;
|
||||
export const laplace: (a: number) => void;
|
||||
export const noise_reduction: (a: number) => void;
|
||||
export const prewitt_horizontal: (a: number) => void;
|
||||
export const sharpen: (a: number) => void;
|
||||
export const sobel_global: (a: number) => void;
|
||||
export const sobel_horizontal: (a: number) => void;
|
||||
export const sobel_vertical: (a: number) => void;
|
||||
export const add_noise_rand: (a: number) => void;
|
||||
export const alter_blue_channel: (a: number, b: number) => void;
|
||||
export const alter_channel: (a: number, b: number, c: number) => void;
|
||||
export const alter_channels: (a: number, b: number, c: number, d: number) => void;
|
||||
export const alter_green_channel: (a: number, b: number) => void;
|
||||
export const alter_red_channel: (a: number, b: number) => void;
|
||||
export const alter_two_channels: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||
export const cali: (a: number) => void;
|
||||
export const dramatic: (a: number) => void;
|
||||
export const duotone_horizon: (a: number) => void;
|
||||
export const duotone_lilac: (a: number) => void;
|
||||
export const duotone_ochre: (a: number) => void;
|
||||
export const duotone_tint: (a: number, b: number) => void;
|
||||
export const duotone_violette: (a: number) => void;
|
||||
export const filter: (a: number, b: number, c: number) => void;
|
||||
export const firenze: (a: number) => void;
|
||||
export const golden: (a: number) => void;
|
||||
export const invert: (a: number) => void;
|
||||
export const lix: (a: number) => void;
|
||||
export const lofi: (a: number) => void;
|
||||
export const monochrome_tint: (a: number, b: number) => void;
|
||||
export const neue: (a: number) => void;
|
||||
export const obsidian: (a: number) => void;
|
||||
export const pastel_pink: (a: number) => void;
|
||||
export const pink_noise: (a: number) => void;
|
||||
export const remove_blue_channel: (a: number, b: number) => void;
|
||||
export const remove_channel: (a: number, b: number, c: number) => void;
|
||||
export const remove_green_channel: (a: number, b: number) => void;
|
||||
export const remove_red_channel: (a: number, b: number) => void;
|
||||
export const ryo: (a: number) => void;
|
||||
export const selective_color_convert: (a: number, b: number, c: number, d: number) => void;
|
||||
export const selective_desaturate: (a: number, b: number, c: number) => void;
|
||||
export const selective_greyscale: (a: number, b: number) => void;
|
||||
export const selective_hue_rotate: (a: number, b: number, c: number) => void;
|
||||
export const selective_lighten: (a: number, b: number, c: number) => void;
|
||||
export const selective_saturate: (a: number, b: number, c: number) => void;
|
||||
export const swap_channels: (a: number, b: number, c: number) => void;
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __wbindgen_exn_store: (a: number) => void;
|
||||
export const __externref_table_alloc: () => number;
|
||||
export const __wbindgen_externrefs: WebAssembly.Table;
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __externref_table_dealloc: (a: number) => void;
|
||||
export const __wbindgen_start: () => void;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
|
||||
import { YAML } from "bun";
|
||||
@@ -11,7 +12,7 @@ describe("SettingsManager", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// Use random UUID to isolate parallel test runs (SQLite files can't be shared)
|
||||
testDir = path.join(process.cwd(), "test-settings-tmp", crypto.randomUUID());
|
||||
testDir = path.join(os.tmpdir(), "test-settings-tmp", crypto.randomUUID());
|
||||
agentDir = path.join(testDir, "agent");
|
||||
projectDir = path.join(testDir, "project");
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# @oh-my-pi/pi-natives
|
||||
|
||||
Native Rust functionality compiled to WebAssembly via wasm-bindgen.
|
||||
|
||||
## What's Inside
|
||||
|
||||
- **Grep**: Regex-based search powered by ripgrep's engine (WASM handles matching, JS handles I/O + gitignore-aware file walking)
|
||||
- **Find**: Glob-based file/directory discovery with gitignore support (pure TypeScript via `globPaths`)
|
||||
- **Image**: Image processing via photon-rs (resize, format conversion)
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { grep, find, PhotonImage, resize, SamplingFilter } from "@oh-my-pi/pi-natives";
|
||||
|
||||
// Grep for a pattern
|
||||
const results = await grep({
|
||||
pattern: "TODO",
|
||||
path: "/path/to/project",
|
||||
glob: "*.ts",
|
||||
context: 2,
|
||||
});
|
||||
|
||||
// Find files
|
||||
const files = await find({
|
||||
pattern: "*.rs",
|
||||
path: "/path/to/project",
|
||||
fileType: "file",
|
||||
});
|
||||
|
||||
// Image processing
|
||||
using image = await PhotonImage.new_from_byteslice(bytes);
|
||||
using resized = await resize(image, 800, 600, SamplingFilter.Lanczos3);
|
||||
const pngBytes = await resized.get_bytes();
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Build WASM from workspace root (requires Rust + wasm-pack)
|
||||
bun run build:wasm
|
||||
|
||||
# Type check
|
||||
bun run check
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
crates/pi-natives/ # Rust source (workspace member)
|
||||
src/lib.rs # Grep/search + wasm-bindgen bindings
|
||||
src/image.rs # Image processing (photon-rs)
|
||||
Cargo.toml # Rust dependencies
|
||||
wasm/ # Generated WASM output
|
||||
pi_natives.wasm # Compiled WASM module
|
||||
pi_natives.js # wasm-bindgen generated JS glue
|
||||
pi_natives.d.ts # TypeScript definitions
|
||||
src/ # TypeScript wrappers
|
||||
index.ts # Public API
|
||||
grep/ # Grep with worker pool
|
||||
image/ # Async image processing via worker
|
||||
pool.ts # Generic worker pool infrastructure
|
||||
```
|
||||
@@ -0,0 +1,136 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import * as path from "node:path";
|
||||
import { grep, grepPool, terminate } from "../src/index.js";
|
||||
|
||||
const ITERATIONS = 50;
|
||||
const CONCURRENCY = 8;
|
||||
|
||||
const packages = path.resolve(import.meta.dir, "../..");
|
||||
|
||||
interface BenchCase {
|
||||
name: string;
|
||||
path: string;
|
||||
pattern: string;
|
||||
glob?: string;
|
||||
}
|
||||
|
||||
const cases: BenchCase[] = [
|
||||
{ name: "Medium (50 files)", path: path.resolve(packages, "tui/src"), pattern: "export", glob: "*.ts" },
|
||||
{ name: "Large (200+ files)", path: path.resolve(packages, "coding-agent/src"), pattern: "import", glob: "*.ts" },
|
||||
];
|
||||
|
||||
// Warmup
|
||||
await grep({ pattern: "test", path: path.resolve(packages, "tui/src") });
|
||||
await grepPool({ pattern: "test", path: path.resolve(packages, "tui/src") });
|
||||
|
||||
console.log(`Benchmark: ${ITERATIONS} iterations per case\n`);
|
||||
|
||||
for (const c of cases) {
|
||||
// Main thread sequential
|
||||
const mainTimes: number[] = [];
|
||||
let mainMatches = 0;
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const start = performance.now();
|
||||
const result = await grep({ pattern: c.pattern, path: c.path, glob: c.glob });
|
||||
mainTimes.push(performance.now() - start);
|
||||
mainMatches = result.totalMatches;
|
||||
}
|
||||
|
||||
// Main thread concurrent (8x parallel)
|
||||
const mainConcurrentTimes: number[] = [];
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const start = performance.now();
|
||||
await Promise.all(
|
||||
Array.from({ length: CONCURRENCY }, () => grep({ pattern: c.pattern, path: c.path, glob: c.glob })),
|
||||
);
|
||||
mainConcurrentTimes.push(performance.now() - start);
|
||||
}
|
||||
|
||||
// Worker pool sequential
|
||||
const poolTimes: number[] = [];
|
||||
let poolMatches = 0;
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const start = performance.now();
|
||||
const result = await grepPool({ pattern: c.pattern, path: c.path, glob: c.glob });
|
||||
poolTimes.push(performance.now() - start);
|
||||
poolMatches = result.totalMatches;
|
||||
}
|
||||
|
||||
// Worker pool concurrent (8x parallel)
|
||||
const poolConcurrentTimes: number[] = [];
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const start = performance.now();
|
||||
await Promise.all(
|
||||
Array.from({ length: CONCURRENCY }, () => grepPool({ pattern: c.pattern, path: c.path, glob: c.glob })),
|
||||
);
|
||||
poolConcurrentTimes.push(performance.now() - start);
|
||||
}
|
||||
|
||||
// Subprocess rg sequential
|
||||
const rgTimes: number[] = [];
|
||||
let rgMatches = 0;
|
||||
const rgDefaultArgs = ["--hidden", "--no-ignore", "--no-ignore-vcs"];
|
||||
const globArg = c.glob ? ["-g", c.glob] : [];
|
||||
|
||||
const runRg = (): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const proc = spawn("rg", ["--json", ...rgDefaultArgs, ...globArg, c.pattern, c.path], {
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
let stdout = "";
|
||||
proc.stdout.on("data", (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
proc.on("close", () => resolve(stdout));
|
||||
proc.on("error", reject);
|
||||
});
|
||||
|
||||
const countMatches = (result: string): number => {
|
||||
const lines = result.split("\n").filter((l) => l.trim());
|
||||
let matches = 0;
|
||||
for (const line of lines) {
|
||||
try {
|
||||
if (JSON.parse(line).type === "match") matches++;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const start = performance.now();
|
||||
const result = await runRg();
|
||||
rgTimes.push(performance.now() - start);
|
||||
rgMatches = countMatches(result);
|
||||
}
|
||||
|
||||
// Subprocess rg concurrent (8x parallel)
|
||||
const rgConcurrentTimes: number[] = [];
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const start = performance.now();
|
||||
await Promise.all(Array.from({ length: CONCURRENCY }, () => runRg()));
|
||||
rgConcurrentTimes.push(performance.now() - start);
|
||||
}
|
||||
|
||||
const avg = (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length;
|
||||
|
||||
console.log(`${c.name}:`);
|
||||
console.log(` Main thread: ${avg(mainTimes).toFixed(2)}ms (${mainMatches} matches)`);
|
||||
console.log(` Main thread 8x: ${avg(mainConcurrentTimes).toFixed(2)}ms`);
|
||||
console.log(` Worker pool: ${avg(poolTimes).toFixed(2)}ms (${poolMatches} matches)`);
|
||||
console.log(` Worker pool 8x: ${avg(poolConcurrentTimes).toFixed(2)}ms`);
|
||||
console.log(` Subprocess rg: ${avg(rgTimes).toFixed(2)}ms (${rgMatches} matches)`);
|
||||
console.log(` Subprocess rg 8x: ${avg(rgConcurrentTimes).toFixed(2)}ms`);
|
||||
|
||||
const mainVsRg = avg(rgTimes) / avg(mainTimes);
|
||||
const poolVsRgConcurrent = avg(rgConcurrentTimes) / avg(poolConcurrentTimes);
|
||||
console.log(
|
||||
` => Main thread is ${mainVsRg > 1 ? `${mainVsRg.toFixed(1)}x faster` : `${(1 / mainVsRg).toFixed(1)}x slower`} than rg (sequential)`,
|
||||
);
|
||||
console.log(
|
||||
` => Worker pool is ${poolVsRgConcurrent > 1 ? `${poolVsRgConcurrent.toFixed(1)}x faster` : `${(1 / poolVsRgConcurrent).toFixed(1)}x slower`} than rg (8x concurrent)\n`,
|
||||
);
|
||||
}
|
||||
|
||||
terminate();
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@oh-my-pi/pi-natives",
|
||||
"version": "0.1.0",
|
||||
"description": "Native Rust functionality compiled to WebAssembly via wasm-bindgen",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"wasm"
|
||||
],
|
||||
"scripts": {
|
||||
"build:wasm": "bun scripts/build-wasm.ts",
|
||||
"check": "biome check . && tsgo -p tsconfig.json",
|
||||
"fix": "biome check --write --unsafe .",
|
||||
"test": "bun test",
|
||||
"bench": "bun bench/grep.ts"
|
||||
},
|
||||
"author": "Can Bölük",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/can1357/oh-my-pi.git",
|
||||
"directory": "packages/natives"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oh-my-pi/pi-utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.3.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { $ } from "bun";
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs/promises";
|
||||
|
||||
const repoRoot = path.join(import.meta.dir, "../../..");
|
||||
const rustDir = path.join(repoRoot, "crates/pi-natives");
|
||||
const wasmDir = path.join(import.meta.dir, "../wasm");
|
||||
|
||||
await fs.mkdir(wasmDir, { recursive: true });
|
||||
|
||||
console.log("Building Rust WASM with wasm-bindgen...");
|
||||
|
||||
// Build with wasm-pack (wasm-opt disabled in Cargo.toml)
|
||||
const buildResult = await $`wasm-pack build --target deno --release --out-dir pkg`
|
||||
.cwd(rustDir)
|
||||
.nothrow();
|
||||
|
||||
if (buildResult.exitCode !== 0) {
|
||||
console.error("wasm-pack build failed");
|
||||
console.error(buildResult.stderr.toString());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pkgDir = path.join(rustDir, "pkg");
|
||||
|
||||
// Crate name is pi-natives -> output files are pi_natives_*
|
||||
const wasmSrc = path.join(pkgDir, "pi_natives_bg.wasm");
|
||||
const wasmDst = path.join(wasmDir, "pi_natives_bg.wasm");
|
||||
|
||||
// Optimize with wasm-opt with bulk memory enabled
|
||||
const optResult = await $`wasm-opt \
|
||||
--enable-bulk-memory \
|
||||
--enable-nontrapping-float-to-int \
|
||||
-O3 \
|
||||
--strip-debug \
|
||||
${wasmSrc} \
|
||||
-o ${wasmDst}`.nothrow();
|
||||
|
||||
if (optResult.exitCode !== 0) {
|
||||
console.log("wasm-opt failed or not available, copying unoptimized build");
|
||||
await fs.copyFile(wasmSrc, wasmDst);
|
||||
}
|
||||
|
||||
// Copy JS bindings
|
||||
await fs.copyFile(path.join(pkgDir, "pi_natives.js"), path.join(wasmDir, "pi_natives.js"));
|
||||
|
||||
// Copy d.ts bindings
|
||||
await fs.copyFile(path.join(pkgDir, "pi_natives.d.ts"), path.join(wasmDir, "pi_natives.d.ts"));
|
||||
await fs.copyFile(path.join(pkgDir, "pi_natives_bg.wasm.d.ts"), path.join(wasmDir, "pi_natives_bg.wasm.d.ts"));
|
||||
|
||||
const stat = await fs.stat(wasmDst);
|
||||
console.log(`Done: ${wasmDst} (${(stat.size / 1024).toFixed(0)}KB)`);
|
||||
|
||||
// Clean up pkg directory
|
||||
await fs.rm(pkgDir, { recursive: true, force: true });
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as path from "node:path";
|
||||
|
||||
export interface TypeFilter {
|
||||
extensions?: string[];
|
||||
names?: string[];
|
||||
}
|
||||
|
||||
const TYPE_ALIASES: Record<string, TypeFilter> = {
|
||||
js: { extensions: ["js", "jsx", "mjs", "cjs"] },
|
||||
javascript: { extensions: ["js", "jsx", "mjs", "cjs"] },
|
||||
ts: { extensions: ["ts", "tsx", "mts", "cts"] },
|
||||
typescript: { extensions: ["ts", "tsx", "mts", "cts"] },
|
||||
json: { extensions: ["json", "jsonc", "json5"] },
|
||||
yaml: { extensions: ["yaml", "yml"] },
|
||||
yml: { extensions: ["yaml", "yml"] },
|
||||
toml: { extensions: ["toml"] },
|
||||
md: { extensions: ["md", "markdown", "mdx"] },
|
||||
markdown: { extensions: ["md", "markdown", "mdx"] },
|
||||
py: { extensions: ["py", "pyi"] },
|
||||
python: { extensions: ["py", "pyi"] },
|
||||
rs: { extensions: ["rs"] },
|
||||
rust: { extensions: ["rs"] },
|
||||
go: { extensions: ["go"] },
|
||||
java: { extensions: ["java"] },
|
||||
kt: { extensions: ["kt", "kts"] },
|
||||
kotlin: { extensions: ["kt", "kts"] },
|
||||
c: { extensions: ["c", "h"] },
|
||||
cpp: { extensions: ["cpp", "cc", "cxx", "hpp", "hxx", "hh"] },
|
||||
cxx: { extensions: ["cpp", "cc", "cxx", "hpp", "hxx", "hh"] },
|
||||
cs: { extensions: ["cs", "csx"] },
|
||||
csharp: { extensions: ["cs", "csx"] },
|
||||
php: { extensions: ["php", "phtml"] },
|
||||
rb: { extensions: ["rb", "rake", "gemspec"] },
|
||||
ruby: { extensions: ["rb", "rake", "gemspec"] },
|
||||
sh: { extensions: ["sh", "bash", "zsh", "fish"] },
|
||||
bash: { extensions: ["sh", "bash", "zsh"] },
|
||||
zsh: { extensions: ["zsh"] },
|
||||
fish: { extensions: ["fish"] },
|
||||
html: { extensions: ["html", "htm"] },
|
||||
css: { extensions: ["css"] },
|
||||
scss: { extensions: ["scss"] },
|
||||
sass: { extensions: ["sass"] },
|
||||
less: { extensions: ["less"] },
|
||||
xml: { extensions: ["xml"] },
|
||||
docker: { names: ["dockerfile"] },
|
||||
dockerfile: { names: ["dockerfile"] },
|
||||
make: { names: ["makefile"] },
|
||||
makefile: { names: ["makefile"] },
|
||||
};
|
||||
|
||||
export function buildGlobPattern(glob?: string): string {
|
||||
const trimmed = glob?.trim();
|
||||
if (!trimmed) return "**/*";
|
||||
const normalized = trimmed.replace(/\\/g, "/");
|
||||
if (normalized.includes("/") || normalized.startsWith("**/")) return normalized;
|
||||
return `**/${normalized}`;
|
||||
}
|
||||
|
||||
export function resolveTypeFilter(type?: string): TypeFilter | undefined {
|
||||
if (!type) return undefined;
|
||||
const trimmed = type.trim();
|
||||
if (!trimmed) return undefined;
|
||||
const normalized = trimmed.toLowerCase();
|
||||
const withoutDot = normalized.startsWith(".") ? normalized.slice(1) : normalized;
|
||||
return TYPE_ALIASES[withoutDot] ?? { extensions: [withoutDot] };
|
||||
}
|
||||
|
||||
export function matchesTypeFilter(filePath: string, filter?: TypeFilter): boolean {
|
||||
if (!filter) return true;
|
||||
const baseName = path.basename(filePath).toLowerCase();
|
||||
if (filter.names?.some(name => name.toLowerCase() === baseName)) {
|
||||
return true;
|
||||
}
|
||||
const ext = path.extname(baseName).slice(1).toLowerCase();
|
||||
if (!ext) return false;
|
||||
return filter.extensions?.includes(ext) ?? false;
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Native ripgrep wrapper using wasm-bindgen.
|
||||
*
|
||||
* JS handles filesystem operations (directory walking, file reading).
|
||||
* WASM handles pure regex matching using ripgrep's engine.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { globPaths } from "@oh-my-pi/pi-utils";
|
||||
import {
|
||||
CompiledPattern as WasmCompiledPattern,
|
||||
has_match as wasmHasMatch,
|
||||
search as wasmSearch,
|
||||
} from "../../wasm/pi_natives";
|
||||
import { WorkerPool } from "../pool";
|
||||
import { buildGlobPattern, matchesTypeFilter, resolveTypeFilter } from "./filters";
|
||||
import type {
|
||||
ContextLine,
|
||||
GrepMatch,
|
||||
GrepOptions,
|
||||
GrepResult,
|
||||
GrepSummary,
|
||||
WasmSearchResult,
|
||||
WorkerRequest,
|
||||
WorkerResponse,
|
||||
} from "./types";
|
||||
|
||||
export type { ContextLine, GrepMatch, GrepOptions, GrepResult, GrepSummary };
|
||||
|
||||
// =============================================================================
|
||||
// File Walking
|
||||
// =============================================================================
|
||||
|
||||
function filterUndefined<T extends Record<string, unknown>>(obj: T): T {
|
||||
const result = {} as T;
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value !== undefined) {
|
||||
(result as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Grep Implementation
|
||||
// =============================================================================
|
||||
|
||||
const GREP_WORKERS = (() => {
|
||||
const val = process.env.OMP_GREP_WORKERS;
|
||||
if (val === undefined) return true;
|
||||
const n = Number.parseInt(val, 10);
|
||||
return Number.isNaN(n) || n > 0;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Search files for a regex pattern (direct, single-threaded).
|
||||
*/
|
||||
async function grepDirect(options: GrepOptions, onMatch?: (match: GrepMatch) => void): Promise<GrepResult> {
|
||||
const searchPath = path.resolve(options.path);
|
||||
const outputMode = options.mode ?? "content";
|
||||
const wasmMode = outputMode === "content" ? "content" : "count";
|
||||
|
||||
const stat = await fs.stat(searchPath);
|
||||
const isFile = stat.isFile();
|
||||
|
||||
using compiledPattern = new WasmCompiledPattern(
|
||||
filterUndefined({
|
||||
pattern: options.pattern,
|
||||
ignoreCase: options.ignoreCase,
|
||||
multiline: options.multiline,
|
||||
context: options.context,
|
||||
maxColumns: options.maxColumns,
|
||||
mode: wasmMode,
|
||||
}),
|
||||
);
|
||||
|
||||
const typeFilter = resolveTypeFilter(options.type);
|
||||
const globPattern = buildGlobPattern(options.glob);
|
||||
|
||||
const matches: GrepMatch[] = [];
|
||||
let totalMatches = 0;
|
||||
let filesWithMatches = 0;
|
||||
let filesSearched = 0;
|
||||
let limitReached = false;
|
||||
const maxCount = options.maxCount;
|
||||
const globalOffset = options.offset ?? 0;
|
||||
|
||||
if (isFile) {
|
||||
if (typeFilter && !matchesTypeFilter(searchPath, typeFilter)) {
|
||||
return {
|
||||
matches,
|
||||
totalMatches,
|
||||
filesWithMatches,
|
||||
filesSearched,
|
||||
limitReached: limitReached || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const content = Bun.mmap(searchPath);
|
||||
filesSearched = 1;
|
||||
|
||||
const result = compiledPattern.search_bytes(
|
||||
content,
|
||||
maxCount,
|
||||
globalOffset > 0 ? globalOffset : undefined,
|
||||
) as WasmSearchResult;
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
if (result.matchCount > 0) {
|
||||
filesWithMatches = 1;
|
||||
totalMatches = result.matchCount;
|
||||
|
||||
if (outputMode === "content") {
|
||||
for (const m of result.matches) {
|
||||
const match: GrepMatch = {
|
||||
path: searchPath,
|
||||
lineNumber: m.lineNumber,
|
||||
line: m.line,
|
||||
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
||||
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
||||
truncated: m.truncated || undefined,
|
||||
};
|
||||
matches.push(match);
|
||||
onMatch?.(match);
|
||||
}
|
||||
} else {
|
||||
const match: GrepMatch = {
|
||||
path: searchPath,
|
||||
lineNumber: 0,
|
||||
line: "",
|
||||
matchCount: result.matchCount,
|
||||
};
|
||||
matches.push(match);
|
||||
onMatch?.(match);
|
||||
}
|
||||
|
||||
limitReached = result.limitReached || (maxCount !== undefined && totalMatches >= maxCount);
|
||||
}
|
||||
} else {
|
||||
const paths = await globPaths(globPattern, {
|
||||
cwd: searchPath,
|
||||
dot: options.hidden ?? true,
|
||||
onlyFiles: true,
|
||||
gitignore: true,
|
||||
});
|
||||
|
||||
for (const relativePath of paths) {
|
||||
if (limitReached) break;
|
||||
if (typeFilter && !matchesTypeFilter(relativePath, typeFilter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedPath = relativePath.replace(/\\/g, "/");
|
||||
const fullPath = path.join(searchPath, normalizedPath);
|
||||
|
||||
let content: Uint8Array;
|
||||
try {
|
||||
content = Bun.mmap(fullPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
filesSearched++;
|
||||
|
||||
if (!compiledPattern.has_match_bytes(content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileOffset = globalOffset > 0 ? Math.max(globalOffset - totalMatches, 0) : 0;
|
||||
const remaining = maxCount !== undefined ? Math.max(maxCount - totalMatches, 0) : undefined;
|
||||
if (remaining === 0) {
|
||||
limitReached = true;
|
||||
break;
|
||||
}
|
||||
const result = compiledPattern.search_bytes(
|
||||
content,
|
||||
remaining,
|
||||
fileOffset > 0 ? fileOffset : undefined,
|
||||
) as WasmSearchResult;
|
||||
|
||||
if (result.error) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.matchCount > 0) {
|
||||
filesWithMatches++;
|
||||
totalMatches += result.matchCount;
|
||||
|
||||
if (outputMode === "content") {
|
||||
for (const m of result.matches) {
|
||||
const match: GrepMatch = {
|
||||
path: normalizedPath,
|
||||
lineNumber: m.lineNumber,
|
||||
line: m.line,
|
||||
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
||||
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
||||
truncated: m.truncated || undefined,
|
||||
};
|
||||
matches.push(match);
|
||||
onMatch?.(match);
|
||||
}
|
||||
} else {
|
||||
const match: GrepMatch = {
|
||||
path: normalizedPath,
|
||||
lineNumber: 0,
|
||||
line: "",
|
||||
matchCount: result.matchCount,
|
||||
};
|
||||
matches.push(match);
|
||||
onMatch?.(match);
|
||||
}
|
||||
|
||||
if (result.limitReached || (maxCount !== undefined && totalMatches >= maxCount)) {
|
||||
limitReached = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
matches,
|
||||
totalMatches,
|
||||
filesWithMatches,
|
||||
filesSearched,
|
||||
limitReached: limitReached || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Content Search (lower-level API)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Search a single file's content for a pattern.
|
||||
* Lower-level API for when you already have file content.
|
||||
*/
|
||||
export function searchContent(
|
||||
content: string,
|
||||
options: {
|
||||
pattern: string;
|
||||
ignoreCase?: boolean;
|
||||
multiline?: boolean;
|
||||
maxCount?: number;
|
||||
offset?: number;
|
||||
context?: number;
|
||||
maxColumns?: number;
|
||||
mode?: "content" | "count";
|
||||
},
|
||||
): WasmSearchResult {
|
||||
return wasmSearch(content, filterUndefined(options)) as WasmSearchResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick check if content contains a pattern match.
|
||||
*/
|
||||
export function hasMatch(
|
||||
content: string,
|
||||
pattern: string,
|
||||
options?: { ignoreCase?: boolean; multiline?: boolean },
|
||||
): boolean {
|
||||
return wasmHasMatch(content, pattern, options?.ignoreCase ?? false, options?.multiline ?? false);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Public API
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Search files for a regex pattern.
|
||||
*
|
||||
* Uses worker pool by default. Set `OMP_GREP_WORKERS=0` to disable.
|
||||
*/
|
||||
export async function grep(options: GrepOptions, onMatch?: (match: GrepMatch) => void): Promise<GrepResult> {
|
||||
if (GREP_WORKERS) {
|
||||
return await grepPoolInternal(options);
|
||||
}
|
||||
return await grepDirect(options, onMatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search files using worker pool (always, ignores OMP_GREP_WORKERS).
|
||||
*/
|
||||
export async function grepPool(options: GrepOptions): Promise<GrepResult> {
|
||||
return await grepPoolInternal(options);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Worker Pool
|
||||
// =============================================================================
|
||||
|
||||
const pool = new WorkerPool<WorkerRequest, WorkerResponse>({
|
||||
workerUrl: new URL("./worker.ts", import.meta.url).href,
|
||||
maxWorkers: 4,
|
||||
idleTimeoutMs: 30_000,
|
||||
});
|
||||
|
||||
async function grepPoolInternal(request: GrepOptions): Promise<GrepResult> {
|
||||
const response = await pool.request<Extract<WorkerResponse, { type: "result" }>>({
|
||||
type: "grep",
|
||||
request,
|
||||
});
|
||||
return response.result;
|
||||
}
|
||||
|
||||
/** Terminate all grep workers. */
|
||||
export function terminate(): void {
|
||||
pool.terminate();
|
||||
}
|
||||
|
||||
export { grepDirect };
|
||||
@@ -0,0 +1,82 @@
|
||||
/** Options for searching files. */
|
||||
export interface GrepOptions {
|
||||
/** Regex pattern to search for */
|
||||
pattern: string;
|
||||
/** Directory or file to search */
|
||||
path: string;
|
||||
/** Glob filter for filenames (e.g., "*.ts") */
|
||||
glob?: string;
|
||||
/** Filter by file type (e.g., "js", "py", "rust") */
|
||||
type?: string;
|
||||
/** Case-insensitive search */
|
||||
ignoreCase?: boolean;
|
||||
/** Enable multiline matching */
|
||||
multiline?: boolean;
|
||||
/** Include hidden files (default: true) */
|
||||
hidden?: boolean;
|
||||
/** Maximum number of matches to return */
|
||||
maxCount?: number;
|
||||
/** Skip first N matches */
|
||||
offset?: number;
|
||||
/** Lines of context before/after matches */
|
||||
context?: number;
|
||||
/** Truncate lines longer than this (characters) */
|
||||
maxColumns?: number;
|
||||
/** Output mode */
|
||||
mode?: "content" | "filesWithMatches" | "count";
|
||||
}
|
||||
|
||||
export interface ContextLine {
|
||||
lineNumber: number;
|
||||
line: string;
|
||||
}
|
||||
|
||||
export interface GrepMatch {
|
||||
path: string;
|
||||
lineNumber: number;
|
||||
line: string;
|
||||
contextBefore?: ContextLine[];
|
||||
contextAfter?: ContextLine[];
|
||||
truncated?: boolean;
|
||||
matchCount?: number;
|
||||
}
|
||||
|
||||
export interface GrepSummary {
|
||||
totalMatches: number;
|
||||
filesWithMatches: number;
|
||||
filesSearched: number;
|
||||
limitReached?: boolean;
|
||||
}
|
||||
|
||||
export interface GrepResult extends GrepSummary {
|
||||
matches: GrepMatch[];
|
||||
}
|
||||
|
||||
/** WASM match result from the compiled pattern. */
|
||||
export interface WasmMatch {
|
||||
lineNumber: number;
|
||||
line: string;
|
||||
contextBefore: ContextLine[];
|
||||
contextAfter: ContextLine[];
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/** WASM search result. */
|
||||
export interface WasmSearchResult {
|
||||
matches: WasmMatch[];
|
||||
matchCount: number;
|
||||
limitReached: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Message types from main thread to worker. */
|
||||
export type WorkerRequest =
|
||||
| { type: "init"; id: number }
|
||||
| { type: "grep"; id: number; request: GrepOptions }
|
||||
| { type: "destroy" };
|
||||
|
||||
/** Message types from worker to main thread. */
|
||||
export type WorkerResponse =
|
||||
| { type: "ready"; id: number }
|
||||
| { type: "result"; id: number; result: GrepResult }
|
||||
| { type: "error"; id: number; error: string };
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Worker script for running wasm-bindgen grep.
|
||||
* Each worker loads its own WASM instance and processes requests.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { globPaths } from "@oh-my-pi/pi-utils";
|
||||
import { CompiledPattern } from "../../wasm/pi_natives";
|
||||
import { buildGlobPattern, matchesTypeFilter, resolveTypeFilter } from "./filters";
|
||||
import type { GrepMatch, GrepOptions, GrepResult, WasmSearchResult, WorkerRequest, WorkerResponse } from "./types";
|
||||
|
||||
function filterUndefined<T extends Record<string, unknown>>(obj: T): T {
|
||||
const result = {} as T;
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value !== undefined) {
|
||||
(result as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function runGrep(request: GrepOptions): Promise<GrepResult> {
|
||||
const searchPath = path.resolve(request.path);
|
||||
const stat = await fs.stat(searchPath);
|
||||
const isFile = stat.isFile();
|
||||
|
||||
using compiledPattern = new CompiledPattern(
|
||||
filterUndefined({
|
||||
pattern: request.pattern,
|
||||
ignoreCase: request.ignoreCase,
|
||||
multiline: request.multiline,
|
||||
context: request.context,
|
||||
maxColumns: request.maxColumns,
|
||||
mode: request.mode === "content" || !request.mode ? "content" : "count",
|
||||
}),
|
||||
);
|
||||
|
||||
const matches: GrepMatch[] = [];
|
||||
let totalMatches = 0;
|
||||
let filesWithMatches = 0;
|
||||
let filesSearched = 0;
|
||||
let limitReached = false;
|
||||
const maxCount = request.maxCount;
|
||||
const globalOffset = request.offset ?? 0;
|
||||
const typeFilter = resolveTypeFilter(request.type);
|
||||
const globPattern = buildGlobPattern(request.glob);
|
||||
|
||||
if (isFile) {
|
||||
if (typeFilter && !matchesTypeFilter(searchPath, typeFilter)) {
|
||||
return {
|
||||
matches,
|
||||
totalMatches,
|
||||
filesWithMatches,
|
||||
filesSearched,
|
||||
limitReached: limitReached || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const content = Bun.mmap(searchPath);
|
||||
filesSearched = 1;
|
||||
|
||||
const result = compiledPattern.search_bytes(
|
||||
content,
|
||||
maxCount,
|
||||
globalOffset > 0 ? globalOffset : undefined,
|
||||
) as WasmSearchResult;
|
||||
|
||||
if (!result.error && result.matchCount > 0) {
|
||||
filesWithMatches = 1;
|
||||
totalMatches = result.matchCount;
|
||||
|
||||
if (request.mode === "content" || !request.mode) {
|
||||
for (const m of result.matches) {
|
||||
matches.push({
|
||||
path: searchPath,
|
||||
lineNumber: m.lineNumber,
|
||||
line: m.line,
|
||||
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
||||
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
||||
truncated: m.truncated || undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
matches.push({
|
||||
path: searchPath,
|
||||
lineNumber: 0,
|
||||
line: "",
|
||||
matchCount: result.matchCount,
|
||||
});
|
||||
}
|
||||
|
||||
limitReached = result.limitReached || (maxCount !== undefined && totalMatches >= maxCount);
|
||||
}
|
||||
} else {
|
||||
const paths = await globPaths(globPattern, {
|
||||
cwd: searchPath,
|
||||
dot: request.hidden ?? true,
|
||||
onlyFiles: true,
|
||||
gitignore: true,
|
||||
});
|
||||
|
||||
for (const relativePath of paths) {
|
||||
if (limitReached) break;
|
||||
if (typeFilter && !matchesTypeFilter(relativePath, typeFilter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedPath = relativePath.replace(/\\/g, "/");
|
||||
const fullPath = path.join(searchPath, normalizedPath);
|
||||
|
||||
let content: Uint8Array;
|
||||
try {
|
||||
content = Bun.mmap(fullPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
filesSearched++;
|
||||
|
||||
if (!compiledPattern.has_match_bytes(content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileOffset = globalOffset > 0 ? Math.max(globalOffset - totalMatches, 0) : 0;
|
||||
const remaining = maxCount !== undefined ? Math.max(maxCount - totalMatches, 0) : undefined;
|
||||
if (remaining === 0) {
|
||||
limitReached = true;
|
||||
break;
|
||||
}
|
||||
const result = compiledPattern.search_bytes(
|
||||
content,
|
||||
remaining,
|
||||
fileOffset > 0 ? fileOffset : undefined,
|
||||
) as WasmSearchResult;
|
||||
|
||||
if (result.error) continue;
|
||||
|
||||
if (result.matchCount > 0) {
|
||||
filesWithMatches++;
|
||||
totalMatches += result.matchCount;
|
||||
|
||||
if (request.mode === "content" || !request.mode) {
|
||||
for (const m of result.matches) {
|
||||
matches.push({
|
||||
path: normalizedPath,
|
||||
lineNumber: m.lineNumber,
|
||||
line: m.line,
|
||||
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
||||
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
||||
truncated: m.truncated || undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
matches.push({
|
||||
path: normalizedPath,
|
||||
lineNumber: 0,
|
||||
line: "",
|
||||
matchCount: result.matchCount,
|
||||
});
|
||||
}
|
||||
|
||||
if (result.limitReached || (maxCount !== undefined && totalMatches >= maxCount)) {
|
||||
limitReached = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
matches,
|
||||
totalMatches,
|
||||
filesWithMatches,
|
||||
filesSearched,
|
||||
limitReached: limitReached || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
declare const self: Worker;
|
||||
|
||||
self.addEventListener("message", async (e: MessageEvent<WorkerRequest>) => {
|
||||
const msg = e.data;
|
||||
|
||||
switch (msg.type) {
|
||||
case "init":
|
||||
self.postMessage({ type: "ready", id: msg.id } satisfies WorkerResponse);
|
||||
break;
|
||||
|
||||
case "grep":
|
||||
try {
|
||||
const result = await runGrep(msg.request);
|
||||
self.postMessage({ type: "result", id: msg.id, result } satisfies WorkerResponse);
|
||||
} catch (err) {
|
||||
self.postMessage({
|
||||
type: "error",
|
||||
id: msg.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
} satisfies WorkerResponse);
|
||||
}
|
||||
break;
|
||||
|
||||
case "destroy":
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Async image processing via worker.
|
||||
*
|
||||
* All heavy lifting happens in a worker thread to avoid blocking the main thread.
|
||||
* Uses transferable ArrayBuffers to avoid copying image data.
|
||||
*/
|
||||
|
||||
import { WorkerPool } from "../pool";
|
||||
import type { ImageRequest, ImageResponse } from "./types";
|
||||
|
||||
// Re-export the enum for filter selection
|
||||
export { SamplingFilter } from "../../wasm/pi_natives";
|
||||
|
||||
const pool = new WorkerPool<ImageRequest, ImageResponse>({
|
||||
workerUrl: new URL("./worker.ts", import.meta.url).href,
|
||||
maxWorkers: 1,
|
||||
idleTimeoutMs: 0, // Keep alive - stateful (image handles)
|
||||
});
|
||||
|
||||
/**
|
||||
* Image handle for async operations.
|
||||
* Must call free() when done to release WASM memory.
|
||||
*/
|
||||
export class PhotonImage {
|
||||
#handle: number;
|
||||
#width: number;
|
||||
#height: number;
|
||||
#freed = false;
|
||||
|
||||
private constructor(handle: number, width: number, height: number) {
|
||||
this.#handle = handle;
|
||||
this.#width = width;
|
||||
this.#height = height;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
static _create(handle: number, width: number, height: number): PhotonImage {
|
||||
return new PhotonImage(handle, width, height);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_getHandle(): number {
|
||||
if (this.#freed) throw new Error("Image already freed");
|
||||
return this.#handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an image from encoded bytes (PNG, JPEG, WebP, GIF).
|
||||
* The bytes are transferred to the worker (zero-copy).
|
||||
*/
|
||||
static async new_from_byteslice(bytes: Uint8Array): Promise<PhotonImage> {
|
||||
const response = await pool.request<Extract<ImageResponse, { type: "loaded" }>>({ type: "load", bytes }, [
|
||||
bytes.buffer,
|
||||
]);
|
||||
return new PhotonImage(response.handle, response.width, response.height);
|
||||
}
|
||||
|
||||
/** Get image width in pixels. */
|
||||
get_width(): number {
|
||||
return this.#width;
|
||||
}
|
||||
|
||||
/** Get image height in pixels. */
|
||||
get_height(): number {
|
||||
return this.#height;
|
||||
}
|
||||
|
||||
/** Export as PNG bytes. */
|
||||
async get_bytes(): Promise<Uint8Array> {
|
||||
if (this.#freed) throw new Error("Image already freed");
|
||||
const response = await pool.request<Extract<ImageResponse, { type: "bytes" }>>({
|
||||
type: "get_png",
|
||||
handle: this.#handle,
|
||||
});
|
||||
return response.bytes;
|
||||
}
|
||||
|
||||
/** Export as JPEG bytes with specified quality (0-100). */
|
||||
async get_bytes_jpeg(quality: number): Promise<Uint8Array> {
|
||||
if (this.#freed) throw new Error("Image already freed");
|
||||
const response = await pool.request<Extract<ImageResponse, { type: "bytes" }>>({
|
||||
type: "get_jpeg",
|
||||
handle: this.#handle,
|
||||
quality,
|
||||
});
|
||||
return response.bytes;
|
||||
}
|
||||
|
||||
/** Release WASM memory. Must be called when done with the image. */
|
||||
free() {
|
||||
if (this.#freed) return;
|
||||
this.#freed = true;
|
||||
pool.request({ type: "free", handle: this.#handle }).catch(() => {});
|
||||
}
|
||||
|
||||
/** Alias for free() to support using-declarations. */
|
||||
[Symbol.dispose](): void {
|
||||
this.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize an image to the specified dimensions.
|
||||
* Returns a new PhotonImage (original is not modified).
|
||||
*/
|
||||
export async function resize(image: PhotonImage, width: number, height: number, filter: number): Promise<PhotonImage> {
|
||||
const handle = image._getHandle();
|
||||
const response = await pool.request<Extract<ImageResponse, { type: "resized" }>>({
|
||||
type: "resize",
|
||||
handle,
|
||||
width,
|
||||
height,
|
||||
filter,
|
||||
});
|
||||
return PhotonImage._create(response.handle, response.width, response.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate the image worker.
|
||||
* Call this when shutting down to clean up resources.
|
||||
*/
|
||||
export function terminate(): void {
|
||||
pool.terminate();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Types for image worker communication.
|
||||
*/
|
||||
|
||||
export type ImageRequest =
|
||||
| { type: "init"; id: number }
|
||||
| { type: "destroy" }
|
||||
| {
|
||||
type: "load";
|
||||
id: number;
|
||||
/** Image bytes (transferred, not copied) */
|
||||
bytes: Uint8Array;
|
||||
}
|
||||
| {
|
||||
type: "resize";
|
||||
id: number;
|
||||
/** Handle returned from load */
|
||||
handle: number;
|
||||
width: number;
|
||||
height: number;
|
||||
filter: number;
|
||||
}
|
||||
| {
|
||||
type: "get_dimensions";
|
||||
id: number;
|
||||
handle: number;
|
||||
}
|
||||
| {
|
||||
type: "get_png";
|
||||
id: number;
|
||||
handle: number;
|
||||
}
|
||||
| {
|
||||
type: "get_jpeg";
|
||||
id: number;
|
||||
handle: number;
|
||||
quality: number;
|
||||
}
|
||||
| {
|
||||
type: "free";
|
||||
id: number;
|
||||
handle: number;
|
||||
};
|
||||
|
||||
export type ImageResponse =
|
||||
| { type: "ready"; id: number }
|
||||
| { type: "error"; id: number; error: string }
|
||||
| { type: "loaded"; id: number; handle: number; width: number; height: number }
|
||||
| { type: "resized"; id: number; handle: number; width: number; height: number }
|
||||
| { type: "dimensions"; id: number; width: number; height: number }
|
||||
| { type: "bytes"; id: number; bytes: Uint8Array }
|
||||
| { type: "freed"; id: number };
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Worker for image processing operations.
|
||||
* Uses WASM for actual processing, communicates via transferable buffers.
|
||||
*/
|
||||
|
||||
import { PhotonImage, type SamplingFilter, resize as wasmResize } from "../../wasm/pi_natives";
|
||||
import type { ImageRequest, ImageResponse } from "./types";
|
||||
|
||||
declare const self: Worker;
|
||||
|
||||
/** Map of handle -> PhotonImage */
|
||||
const images = new Map<number, PhotonImage>();
|
||||
let nextHandle = 1;
|
||||
|
||||
function respond(msg: ImageResponse, transfer?: ArrayBufferLike[]): void {
|
||||
if (transfer) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
self.postMessage(msg, transfer as any);
|
||||
} else {
|
||||
self.postMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener("message", (e: MessageEvent<ImageRequest>) => {
|
||||
const msg = e.data;
|
||||
|
||||
switch (msg.type) {
|
||||
case "init":
|
||||
respond({ type: "ready", id: msg.id });
|
||||
break;
|
||||
|
||||
case "destroy":
|
||||
for (const img of images.values()) {
|
||||
img.free();
|
||||
}
|
||||
images.clear();
|
||||
break;
|
||||
|
||||
case "load": {
|
||||
try {
|
||||
const img = PhotonImage.new_from_byteslice(msg.bytes);
|
||||
const handle = nextHandle++;
|
||||
images.set(handle, img);
|
||||
respond({
|
||||
type: "loaded",
|
||||
id: msg.id,
|
||||
handle,
|
||||
width: img.get_width(),
|
||||
height: img.get_height(),
|
||||
});
|
||||
} catch (err) {
|
||||
respond({
|
||||
type: "error",
|
||||
id: msg.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "resize": {
|
||||
const img = images.get(msg.handle);
|
||||
if (!img) {
|
||||
respond({ type: "error", id: msg.id, error: "Invalid image handle" });
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const filter = msg.filter as SamplingFilter;
|
||||
const resized = wasmResize(img, msg.width, msg.height, filter);
|
||||
const handle = nextHandle++;
|
||||
images.set(handle, resized);
|
||||
respond({
|
||||
type: "resized",
|
||||
id: msg.id,
|
||||
handle,
|
||||
width: resized.get_width(),
|
||||
height: resized.get_height(),
|
||||
});
|
||||
} catch (err) {
|
||||
respond({
|
||||
type: "error",
|
||||
id: msg.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "get_dimensions": {
|
||||
const img = images.get(msg.handle);
|
||||
if (!img) {
|
||||
respond({ type: "error", id: msg.id, error: "Invalid image handle" });
|
||||
break;
|
||||
}
|
||||
respond({
|
||||
type: "dimensions",
|
||||
id: msg.id,
|
||||
width: img.get_width(),
|
||||
height: img.get_height(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "get_png": {
|
||||
const img = images.get(msg.handle);
|
||||
if (!img) {
|
||||
respond({ type: "error", id: msg.id, error: "Invalid image handle" });
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const bytes = img.get_bytes();
|
||||
respond({ type: "bytes", id: msg.id, bytes }, [bytes.buffer]);
|
||||
} catch (err) {
|
||||
respond({
|
||||
type: "error",
|
||||
id: msg.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "get_jpeg": {
|
||||
const img = images.get(msg.handle);
|
||||
if (!img) {
|
||||
respond({ type: "error", id: msg.id, error: "Invalid image handle" });
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const bytes = img.get_bytes_jpeg(msg.quality);
|
||||
respond({ type: "bytes", id: msg.id, bytes }, [bytes.buffer]);
|
||||
} catch (err) {
|
||||
respond({
|
||||
type: "error",
|
||||
id: msg.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "free": {
|
||||
const img = images.get(msg.handle);
|
||||
if (img) {
|
||||
img.free();
|
||||
images.delete(msg.handle);
|
||||
}
|
||||
respond({ type: "freed", id: msg.id });
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Native utilities powered by WASM.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { globPaths } from "@oh-my-pi/pi-utils";
|
||||
|
||||
// =============================================================================
|
||||
// Grep (ripgrep-based regex search)
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
type ContextLine,
|
||||
type GrepMatch,
|
||||
type GrepOptions,
|
||||
type GrepResult,
|
||||
type GrepSummary,
|
||||
grep,
|
||||
grepDirect,
|
||||
grepPool,
|
||||
hasMatch,
|
||||
searchContent,
|
||||
terminate,
|
||||
} from "./grep/index";
|
||||
|
||||
// =============================================================================
|
||||
// WASI implementation
|
||||
// =============================================================================
|
||||
|
||||
export { WASI1, WASIError, WASIExitError, type WASIOptions } from "./wasix";
|
||||
|
||||
// =============================================================================
|
||||
// Find (file discovery)
|
||||
// =============================================================================
|
||||
|
||||
export interface FindOptions {
|
||||
/** Glob pattern to match (e.g., `*.ts`) */
|
||||
pattern: string;
|
||||
/** Directory to search */
|
||||
path: string;
|
||||
/** Filter by file type: "file", "dir", or "symlink" */
|
||||
fileType?: "file" | "dir" | "symlink";
|
||||
/** Include hidden files (default: false) */
|
||||
hidden?: boolean;
|
||||
/** Maximum number of results */
|
||||
maxResults?: number;
|
||||
/** Respect .gitignore files (default: true) */
|
||||
gitignore?: boolean;
|
||||
}
|
||||
|
||||
export interface FindMatch {
|
||||
path: string;
|
||||
fileType: "file" | "dir" | "symlink";
|
||||
}
|
||||
|
||||
export interface FindResult {
|
||||
matches: FindMatch[];
|
||||
totalMatches: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find files matching a glob pattern.
|
||||
* Respects .gitignore by default.
|
||||
*/
|
||||
export async function find(options: FindOptions, onMatch?: (match: FindMatch) => void): Promise<FindResult> {
|
||||
const searchPath = path.resolve(options.path);
|
||||
const pattern = options.pattern || "*";
|
||||
|
||||
// Convert simple patterns to recursive globs if needed
|
||||
const globPattern = pattern.includes("/") || pattern.startsWith("**") ? pattern : `**/${pattern}`;
|
||||
|
||||
const paths = await globPaths(globPattern, {
|
||||
cwd: searchPath,
|
||||
dot: options.hidden ?? false,
|
||||
onlyFiles: options.fileType === "file",
|
||||
gitignore: options.gitignore ?? true,
|
||||
});
|
||||
|
||||
const matches: FindMatch[] = [];
|
||||
const maxResults = options.maxResults ?? Number.MAX_SAFE_INTEGER;
|
||||
|
||||
for (const p of paths) {
|
||||
if (matches.length >= maxResults) {
|
||||
break;
|
||||
}
|
||||
|
||||
const normalizedPath = p.replace(/\\/g, "/");
|
||||
if (!normalizedPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let stats: Awaited<ReturnType<typeof fs.lstat>>;
|
||||
try {
|
||||
stats = await fs.lstat(path.join(searchPath, normalizedPath));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileType: "file" | "dir" | "symlink" = stats.isSymbolicLink()
|
||||
? "symlink"
|
||||
: stats.isDirectory()
|
||||
? "dir"
|
||||
: "file";
|
||||
|
||||
if (options.fileType && options.fileType !== fileType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match: FindMatch = {
|
||||
path: normalizedPath,
|
||||
fileType,
|
||||
};
|
||||
|
||||
matches.push(match);
|
||||
onMatch?.(match);
|
||||
}
|
||||
|
||||
return {
|
||||
matches,
|
||||
totalMatches: matches.length,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Image processing (photon-compatible API)
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
PhotonImage,
|
||||
resize,
|
||||
SamplingFilter,
|
||||
terminate as terminateImageWorker,
|
||||
} from "./image/index";
|
||||
|
||||
// =============================================================================
|
||||
// Worker Pool (shared infrastructure)
|
||||
// =============================================================================
|
||||
|
||||
export { type BaseRequest, type BaseResponse, WorkerPool, type WorkerPoolOptions } from "./pool";
|
||||
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Generic worker pool for WASM-based operations.
|
||||
*
|
||||
* Supports both single-worker (maxWorkers: 1) and multi-worker scenarios.
|
||||
* Workers are lazily created and auto-terminated after idle timeout.
|
||||
*/
|
||||
|
||||
/** Base request type - workers must accept messages with this shape. */
|
||||
export interface BaseRequest {
|
||||
type: string;
|
||||
id?: number;
|
||||
}
|
||||
|
||||
/** Base response type - workers must respond with this shape. */
|
||||
export interface BaseResponse {
|
||||
type: string;
|
||||
id: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface WorkerPoolOptions {
|
||||
/** URL to the worker script. */
|
||||
workerUrl: string | URL;
|
||||
/** Maximum number of workers (default: 4). */
|
||||
maxWorkers?: number;
|
||||
/** Idle timeout in ms before terminating unused workers (0 = never, default: 30000). */
|
||||
idleTimeoutMs?: number;
|
||||
/** Timeout for worker initialization in ms (default: 10000). */
|
||||
initTimeoutMs?: number;
|
||||
}
|
||||
|
||||
interface PooledWorker {
|
||||
worker: Worker;
|
||||
busy: boolean;
|
||||
lastUsed: number;
|
||||
currentRequestId: number | null;
|
||||
}
|
||||
|
||||
interface PendingRequest<T> {
|
||||
resolve: (result: T) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeout?: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A pool of workers that process requests in parallel.
|
||||
*
|
||||
* @typeParam TReq - Request message type (must extend BaseRequest)
|
||||
* @typeParam TRes - Response message type (must extend BaseResponse)
|
||||
*/
|
||||
export class WorkerPool<TReq extends BaseRequest, TRes extends BaseResponse> {
|
||||
readonly #options: Required<WorkerPoolOptions>;
|
||||
readonly #pool: PooledWorker[] = [];
|
||||
readonly #waiters: Array<(worker: PooledWorker) => void> = [];
|
||||
readonly #pending = new Map<number, PendingRequest<TRes>>();
|
||||
#nextRequestId = 1;
|
||||
#idleCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor(options: WorkerPoolOptions) {
|
||||
this.#options = {
|
||||
workerUrl: options.workerUrl,
|
||||
maxWorkers: options.maxWorkers ?? 4,
|
||||
idleTimeoutMs: options.idleTimeoutMs ?? 30_000,
|
||||
initTimeoutMs: options.initTimeoutMs ?? 10_000,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to a worker and wait for the response.
|
||||
* Workers are acquired from the pool (or created if under limit).
|
||||
*/
|
||||
async request<T extends TRes = TRes>(
|
||||
msg: TReq | (Omit<TReq, "id"> & { id?: number }),
|
||||
transfer?: ArrayBufferLike[],
|
||||
): Promise<T> {
|
||||
const worker = await this.#acquireWorker();
|
||||
const id = msg.id ?? this.#nextRequestId++;
|
||||
const fullMsg = { ...msg, id } as TReq;
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
||||
this.#pending.set(id, {
|
||||
resolve: resolve as (result: TRes) => void,
|
||||
reject,
|
||||
});
|
||||
|
||||
worker.currentRequestId = id;
|
||||
if (transfer) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
worker.worker.postMessage(fullMsg, transfer as any);
|
||||
} else {
|
||||
worker.worker.postMessage(fullMsg);
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/** Terminate all workers and clear pending requests. */
|
||||
terminate(): void {
|
||||
if (this.#idleCheckInterval) {
|
||||
clearInterval(this.#idleCheckInterval);
|
||||
this.#idleCheckInterval = null;
|
||||
}
|
||||
|
||||
for (const w of [...this.#pool]) {
|
||||
this.#removeWorker(w);
|
||||
}
|
||||
|
||||
this.#waiters.length = 0;
|
||||
|
||||
for (const pending of this.#pending.values()) {
|
||||
pending.reject(new Error("Worker pool terminated"));
|
||||
if (pending.timeout) clearTimeout(pending.timeout);
|
||||
}
|
||||
this.#pending.clear();
|
||||
}
|
||||
|
||||
#createWorker(): PooledWorker {
|
||||
const worker = new Worker(this.#options.workerUrl);
|
||||
|
||||
const pooledWorker: PooledWorker = {
|
||||
worker,
|
||||
busy: false,
|
||||
lastUsed: Date.now(),
|
||||
currentRequestId: null,
|
||||
};
|
||||
|
||||
worker.onmessage = (e: MessageEvent<TRes>) => {
|
||||
this.#handleMessage(pooledWorker, e.data);
|
||||
};
|
||||
|
||||
worker.onerror = (e: ErrorEvent) => {
|
||||
const requestId = pooledWorker.currentRequestId;
|
||||
if (requestId !== null) {
|
||||
this.#rejectRequest(requestId, new Error(`Worker error: ${e.message}`));
|
||||
}
|
||||
this.#removeWorker(pooledWorker);
|
||||
};
|
||||
|
||||
return pooledWorker;
|
||||
}
|
||||
|
||||
#handleMessage(pooledWorker: PooledWorker, msg: TRes): void {
|
||||
const pending = this.#pending.get(msg.id);
|
||||
if (!pending) return;
|
||||
|
||||
this.#pending.delete(msg.id);
|
||||
if (pending.timeout) clearTimeout(pending.timeout);
|
||||
|
||||
if (msg.type === "error" && "error" in msg) {
|
||||
pending.reject(new Error(msg.error ?? "Unknown error"));
|
||||
} else {
|
||||
pending.resolve(msg);
|
||||
}
|
||||
|
||||
// Release worker back to pool (unless it was the init request)
|
||||
if (msg.type !== "ready") {
|
||||
pooledWorker.currentRequestId = null;
|
||||
this.#releaseWorker(pooledWorker);
|
||||
}
|
||||
}
|
||||
|
||||
#rejectRequest(id: number, error: Error): void {
|
||||
const pending = this.#pending.get(id);
|
||||
if (pending) {
|
||||
this.#pending.delete(id);
|
||||
if (pending.timeout) clearTimeout(pending.timeout);
|
||||
pending.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
#removeWorker(pooledWorker: PooledWorker): void {
|
||||
const idx = this.#pool.indexOf(pooledWorker);
|
||||
if (idx !== -1) {
|
||||
this.#pool.splice(idx, 1);
|
||||
}
|
||||
pooledWorker.worker.postMessage({ type: "destroy" } satisfies BaseRequest);
|
||||
pooledWorker.worker.terminate();
|
||||
}
|
||||
|
||||
#releaseWorker(pooledWorker: PooledWorker): void {
|
||||
pooledWorker.busy = false;
|
||||
pooledWorker.lastUsed = Date.now();
|
||||
|
||||
if (this.#waiters.length) {
|
||||
const waiter = this.#waiters.shift()!;
|
||||
pooledWorker.busy = true;
|
||||
waiter(pooledWorker);
|
||||
}
|
||||
}
|
||||
|
||||
#checkIdleWorkers(): void {
|
||||
if (this.#options.idleTimeoutMs === 0) return;
|
||||
|
||||
const now = Date.now();
|
||||
const toRemove: PooledWorker[] = [];
|
||||
|
||||
for (const w of this.#pool) {
|
||||
if (!w.busy && now - w.lastUsed > this.#options.idleTimeoutMs && !this.#waiters.length) {
|
||||
toRemove.push(w);
|
||||
}
|
||||
}
|
||||
|
||||
for (const w of toRemove) {
|
||||
this.#removeWorker(w);
|
||||
}
|
||||
|
||||
if (this.#pool.length === 0 && this.#idleCheckInterval) {
|
||||
clearInterval(this.#idleCheckInterval);
|
||||
this.#idleCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
#ensureIdleCheck(): void {
|
||||
if (this.#options.idleTimeoutMs > 0 && !this.#idleCheckInterval) {
|
||||
this.#idleCheckInterval = setInterval(() => this.#checkIdleWorkers(), 10_000);
|
||||
}
|
||||
}
|
||||
|
||||
async #initializeWorker(pooledWorker: PooledWorker): Promise<void> {
|
||||
const id = this.#nextRequestId++;
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.#rejectRequest(id, new Error("Worker initialization timeout"));
|
||||
}, this.#options.initTimeoutMs);
|
||||
|
||||
this.#pending.set(id, {
|
||||
resolve: () => resolve(),
|
||||
reject,
|
||||
timeout,
|
||||
} as PendingRequest<TRes>);
|
||||
|
||||
pooledWorker.worker.postMessage({ type: "init", id } satisfies BaseRequest);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async #acquireWorker(): Promise<PooledWorker> {
|
||||
// Try to find an idle worker
|
||||
for (const w of this.#pool) {
|
||||
if (!w.busy) {
|
||||
w.busy = true;
|
||||
return w;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new worker if under limit
|
||||
if (this.#pool.length < this.#options.maxWorkers) {
|
||||
const worker = this.#createWorker();
|
||||
worker.busy = true;
|
||||
this.#pool.push(worker);
|
||||
this.#ensureIdleCheck();
|
||||
await this.#initializeWorker(worker);
|
||||
return worker;
|
||||
}
|
||||
|
||||
// Wait for a worker to become available
|
||||
const { promise, resolve } = Promise.withResolvers<PooledWorker>();
|
||||
this.#waiters.push(w => {
|
||||
w.busy = true;
|
||||
resolve(w);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { type FindMatch, find, grep } from "../src/index";
|
||||
|
||||
let testDir: string;
|
||||
|
||||
async function setupFixtures() {
|
||||
testDir = await fs.mkdtemp(path.join(os.tmpdir(), "natives-test-"));
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(testDir, "file1.ts"),
|
||||
`export function hello() {
|
||||
// TODO: implement
|
||||
return "hello";
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(testDir, "file2.ts"),
|
||||
`export function world() {
|
||||
// FIXME: fix this
|
||||
return "world";
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(testDir, "readme.md"),
|
||||
`# Test README
|
||||
|
||||
This is a test file.
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
async function cleanupFixtures() {
|
||||
await fs.rm(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
describe("pi-natives", () => {
|
||||
beforeAll(async () => {
|
||||
await setupFixtures();
|
||||
return async () => {
|
||||
await cleanupFixtures();
|
||||
};
|
||||
});
|
||||
|
||||
describe("grep", () => {
|
||||
it("should find patterns in files", async () => {
|
||||
const result = await grep({
|
||||
pattern: "TODO",
|
||||
path: testDir,
|
||||
});
|
||||
|
||||
expect(result.totalMatches).toBe(1);
|
||||
expect(result.matches.length).toBe(1);
|
||||
expect(result.matches[0].line).toContain("TODO");
|
||||
});
|
||||
|
||||
it("should respect glob patterns", async () => {
|
||||
const result = await grep({
|
||||
pattern: "test",
|
||||
path: testDir,
|
||||
glob: "*.md",
|
||||
ignoreCase: true,
|
||||
});
|
||||
|
||||
expect(result.totalMatches).toBe(2); // "Test" in title + "test" in body
|
||||
});
|
||||
|
||||
it("should return filesWithMatches mode", async () => {
|
||||
const result = await grep({
|
||||
pattern: "return",
|
||||
path: testDir,
|
||||
mode: "filesWithMatches",
|
||||
});
|
||||
|
||||
expect(result.filesWithMatches).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("find", () => {
|
||||
it("should find files matching pattern", async () => {
|
||||
const result = await find({
|
||||
pattern: "*.ts",
|
||||
path: testDir,
|
||||
});
|
||||
|
||||
expect(result.totalMatches).toBe(2);
|
||||
expect(result.matches.every((m: FindMatch) => m.path.endsWith(".ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should filter by file type", async () => {
|
||||
const result = await find({
|
||||
pattern: "*",
|
||||
path: testDir,
|
||||
fileType: "file",
|
||||
});
|
||||
|
||||
expect(result.totalMatches).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts", "bench/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* A compiled regex matcher that can be reused across multiple searches.
|
||||
*/
|
||||
export class CompiledPattern {
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/**
|
||||
* Check if content has any matches (faster than full search).
|
||||
*/
|
||||
has_match(content: string): boolean;
|
||||
/**
|
||||
* Check if bytes have any matches (faster than full search).
|
||||
*/
|
||||
has_match_bytes(content: Uint8Array): boolean;
|
||||
/**
|
||||
* Compile a regex pattern for reuse.
|
||||
*/
|
||||
constructor(options: any);
|
||||
/**
|
||||
* Search content using this compiled pattern.
|
||||
* Returns matches as a JS object.
|
||||
*/
|
||||
search(content: string, max_count?: number | null, offset?: number | null): any;
|
||||
/**
|
||||
* Search bytes directly (avoids UTF-16 to UTF-8 conversion).
|
||||
* Use with `Bun.mmap()` for best performance.
|
||||
*/
|
||||
search_bytes(content: Uint8Array, max_count?: number | null, offset?: number | null): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image container for WASM interop.
|
||||
*/
|
||||
export class PhotonImage {
|
||||
private constructor();
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/**
|
||||
* Export image as PNG bytes.
|
||||
*/
|
||||
get_bytes(): Uint8Array;
|
||||
/**
|
||||
* Export image as JPEG bytes with specified quality (0-100).
|
||||
*/
|
||||
get_bytes_jpeg(quality: number): Uint8Array;
|
||||
/**
|
||||
* Get the height of the image.
|
||||
*/
|
||||
get_height(): number;
|
||||
/**
|
||||
* Get the width of the image.
|
||||
*/
|
||||
get_width(): number;
|
||||
/**
|
||||
* Create a new PhotonImage from encoded image bytes (PNG, JPEG, WebP, GIF).
|
||||
*/
|
||||
static new_from_byteslice(bytes: Uint8Array): PhotonImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sampling filter for resize operations.
|
||||
*/
|
||||
export enum SamplingFilter {
|
||||
Nearest = 1,
|
||||
Triangle = 2,
|
||||
CatmullRom = 3,
|
||||
Gaussian = 4,
|
||||
Lanczos3 = 5,
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick check if content matches a pattern.
|
||||
*/
|
||||
export function has_match(content: string, pattern: string, ignore_case: boolean, multiline: boolean): boolean;
|
||||
|
||||
/**
|
||||
* Resize an image to the specified dimensions.
|
||||
*/
|
||||
export function resize(image: PhotonImage, width: number, height: number, filter: SamplingFilter): PhotonImage;
|
||||
|
||||
/**
|
||||
* Search content for a pattern (one-shot, compiles pattern each time).
|
||||
* For repeated searches with the same pattern, use [`CompiledPattern`].
|
||||
*/
|
||||
export function search(content: string, options: any): any;
|
||||
@@ -0,0 +1,656 @@
|
||||
/* @ts-self-types="./pi_natives.d.ts" */
|
||||
|
||||
/**
|
||||
* A compiled regex matcher that can be reused across multiple searches.
|
||||
*/
|
||||
export class CompiledPattern {
|
||||
__destroy_into_raw() {
|
||||
const ptr = this.__wbg_ptr;
|
||||
this.__wbg_ptr = 0;
|
||||
CompiledPatternFinalization.unregister(this);
|
||||
return ptr;
|
||||
}
|
||||
free() {
|
||||
const ptr = this.__destroy_into_raw();
|
||||
wasm.__wbg_compiledpattern_free(ptr, 0);
|
||||
}
|
||||
/**
|
||||
* Check if content has any matches (faster than full search).
|
||||
* @param {string} content
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has_match(content) {
|
||||
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.compiledpattern_has_match(this.__wbg_ptr, ptr0, len0);
|
||||
return ret !== 0;
|
||||
}
|
||||
/**
|
||||
* Check if bytes have any matches (faster than full search).
|
||||
* @param {Uint8Array} content
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has_match_bytes(content) {
|
||||
const ptr0 = passArray8ToWasm0(content, wasm.__wbindgen_export);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.compiledpattern_has_match(this.__wbg_ptr, ptr0, len0);
|
||||
return ret !== 0;
|
||||
}
|
||||
/**
|
||||
* Compile a regex pattern for reuse.
|
||||
* @param {any} options
|
||||
*/
|
||||
constructor(options) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
wasm.compiledpattern_new(retptr, addHeapObject(options));
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||
if (r2) {
|
||||
throw takeObject(r1);
|
||||
}
|
||||
this.__wbg_ptr = r0 >>> 0;
|
||||
CompiledPatternFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Search content using this compiled pattern.
|
||||
* Returns matches as a JS object.
|
||||
* @param {string} content
|
||||
* @param {number | null} [max_count]
|
||||
* @param {number | null} [offset]
|
||||
* @returns {any}
|
||||
*/
|
||||
search(content, max_count, offset) {
|
||||
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.compiledpattern_search(this.__wbg_ptr, ptr0, len0, isLikeNone(max_count) ? 0x100000001 : (max_count) >>> 0, isLikeNone(offset) ? 0x100000001 : (offset) >>> 0);
|
||||
return takeObject(ret);
|
||||
}
|
||||
/**
|
||||
* Search bytes directly (avoids UTF-16 to UTF-8 conversion).
|
||||
* Use with `Bun.mmap()` for best performance.
|
||||
* @param {Uint8Array} content
|
||||
* @param {number | null} [max_count]
|
||||
* @param {number | null} [offset]
|
||||
* @returns {any}
|
||||
*/
|
||||
search_bytes(content, max_count, offset) {
|
||||
const ptr0 = passArray8ToWasm0(content, wasm.__wbindgen_export);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.compiledpattern_search(this.__wbg_ptr, ptr0, len0, isLikeNone(max_count) ? 0x100000001 : (max_count) >>> 0, isLikeNone(offset) ? 0x100000001 : (offset) >>> 0);
|
||||
return takeObject(ret);
|
||||
}
|
||||
}
|
||||
if (Symbol.dispose) CompiledPattern.prototype[Symbol.dispose] = CompiledPattern.prototype.free;
|
||||
|
||||
/**
|
||||
* Image container for WASM interop.
|
||||
*/
|
||||
export class PhotonImage {
|
||||
static __wrap(ptr) {
|
||||
ptr = ptr >>> 0;
|
||||
const obj = Object.create(PhotonImage.prototype);
|
||||
obj.__wbg_ptr = ptr;
|
||||
PhotonImageFinalization.register(obj, obj.__wbg_ptr, obj);
|
||||
return obj;
|
||||
}
|
||||
__destroy_into_raw() {
|
||||
const ptr = this.__wbg_ptr;
|
||||
this.__wbg_ptr = 0;
|
||||
PhotonImageFinalization.unregister(this);
|
||||
return ptr;
|
||||
}
|
||||
free() {
|
||||
const ptr = this.__destroy_into_raw();
|
||||
wasm.__wbg_photonimage_free(ptr, 0);
|
||||
}
|
||||
/**
|
||||
* Export image as PNG bytes.
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
get_bytes() {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
wasm.photonimage_get_bytes(retptr, this.__wbg_ptr);
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
|
||||
if (r3) {
|
||||
throw takeObject(r2);
|
||||
}
|
||||
var v1 = getArrayU8FromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_export3(r0, r1 * 1, 1);
|
||||
return v1;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Export image as JPEG bytes with specified quality (0-100).
|
||||
* @param {number} quality
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
get_bytes_jpeg(quality) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
wasm.photonimage_get_bytes_jpeg(retptr, this.__wbg_ptr, quality);
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
|
||||
if (r3) {
|
||||
throw takeObject(r2);
|
||||
}
|
||||
var v1 = getArrayU8FromWasm0(r0, r1).slice();
|
||||
wasm.__wbindgen_export3(r0, r1 * 1, 1);
|
||||
return v1;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the height of the image.
|
||||
* @returns {number}
|
||||
*/
|
||||
get_height() {
|
||||
const ret = wasm.photonimage_get_height(this.__wbg_ptr);
|
||||
return ret >>> 0;
|
||||
}
|
||||
/**
|
||||
* Get the width of the image.
|
||||
* @returns {number}
|
||||
*/
|
||||
get_width() {
|
||||
const ret = wasm.photonimage_get_width(this.__wbg_ptr);
|
||||
return ret >>> 0;
|
||||
}
|
||||
/**
|
||||
* Create a new PhotonImage from encoded image bytes (PNG, JPEG, WebP, GIF).
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {PhotonImage}
|
||||
*/
|
||||
static new_from_byteslice(bytes) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_export);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
wasm.photonimage_new_from_byteslice(retptr, ptr0, len0);
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||
if (r2) {
|
||||
throw takeObject(r1);
|
||||
}
|
||||
return PhotonImage.__wrap(r0);
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Symbol.dispose) PhotonImage.prototype[Symbol.dispose] = PhotonImage.prototype.free;
|
||||
|
||||
/**
|
||||
* Sampling filter for resize operations.
|
||||
* @enum {1 | 2 | 3 | 4 | 5}
|
||||
*/
|
||||
export const SamplingFilter = Object.freeze({
|
||||
Nearest: 1, "1": "Nearest",
|
||||
Triangle: 2, "2": "Triangle",
|
||||
CatmullRom: 3, "3": "CatmullRom",
|
||||
Gaussian: 4, "4": "Gaussian",
|
||||
Lanczos3: 5, "5": "Lanczos3",
|
||||
});
|
||||
|
||||
/**
|
||||
* Quick check if content matches a pattern.
|
||||
* @param {string} content
|
||||
* @param {string} pattern
|
||||
* @param {boolean} ignore_case
|
||||
* @param {boolean} multiline
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function has_match(content, pattern, ignore_case, multiline) {
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(pattern, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
wasm.has_match(retptr, ptr0, len0, ptr1, len1, ignore_case, multiline);
|
||||
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
||||
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
||||
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
||||
if (r2) {
|
||||
throw takeObject(r1);
|
||||
}
|
||||
return r0 !== 0;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize an image to the specified dimensions.
|
||||
* @param {PhotonImage} image
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {SamplingFilter} filter
|
||||
* @returns {PhotonImage}
|
||||
*/
|
||||
export function resize(image, width, height, filter) {
|
||||
_assertClass(image, PhotonImage);
|
||||
const ret = wasm.resize(image.__wbg_ptr, width, height, filter);
|
||||
return PhotonImage.__wrap(ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search content for a pattern (one-shot, compiles pattern each time).
|
||||
* For repeated searches with the same pattern, use [`CompiledPattern`].
|
||||
* @param {string} content
|
||||
* @param {any} options
|
||||
* @returns {any}
|
||||
*/
|
||||
export function search(content, options) {
|
||||
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.search(ptr0, len0, addHeapObject(options));
|
||||
return takeObject(ret);
|
||||
}
|
||||
|
||||
function __wbg_get_imports() {
|
||||
const import0 = {
|
||||
__proto__: null,
|
||||
__wbg_Error_8c4e43fe74559d73: function(arg0, arg1) {
|
||||
const ret = Error(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbg_Number_04624de7d0e8332d: function(arg0) {
|
||||
const ret = Number(getObject(arg0));
|
||||
return ret;
|
||||
},
|
||||
__wbg_String_8f0eb39a4a4c2f66: function(arg0, arg1) {
|
||||
const ret = String(getObject(arg1));
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbg___wbindgen_bigint_get_as_i64_8fcf4ce7f1ca72a2: function(arg0, arg1) {
|
||||
const v = getObject(arg1);
|
||||
const ret = typeof(v) === 'bigint' ? v : undefined;
|
||||
getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||
},
|
||||
__wbg___wbindgen_boolean_get_bbbb1c18aa2f5e25: function(arg0) {
|
||||
const v = getObject(arg0);
|
||||
const ret = typeof(v) === 'boolean' ? v : undefined;
|
||||
return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;
|
||||
},
|
||||
__wbg___wbindgen_debug_string_0bc8482c6e3508ae: function(arg0, arg1) {
|
||||
const ret = debugString(getObject(arg1));
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbg___wbindgen_in_47fa6863be6f2f25: function(arg0, arg1) {
|
||||
const ret = getObject(arg0) in getObject(arg1);
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_is_bigint_31b12575b56f32fc: function(arg0) {
|
||||
const ret = typeof(getObject(arg0)) === 'bigint';
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_is_object_5ae8e5880f2c1fbd: function(arg0) {
|
||||
const val = getObject(arg0);
|
||||
const ret = typeof(val) === 'object' && val !== null;
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_is_string_cd444516edc5b180: function(arg0) {
|
||||
const ret = typeof(getObject(arg0)) === 'string';
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_is_undefined_9e4d92534c42d778: function(arg0) {
|
||||
const ret = getObject(arg0) === undefined;
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_jsval_eq_11888390b0186270: function(arg0, arg1) {
|
||||
const ret = getObject(arg0) === getObject(arg1);
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_jsval_loose_eq_9dd77d8cd6671811: function(arg0, arg1) {
|
||||
const ret = getObject(arg0) == getObject(arg1);
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_number_get_8ff4255516ccad3e: function(arg0, arg1) {
|
||||
const obj = getObject(arg1);
|
||||
const ret = typeof(obj) === 'number' ? obj : undefined;
|
||||
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||
},
|
||||
__wbg___wbindgen_string_get_72fb696202c56729: function(arg0, arg1) {
|
||||
const obj = getObject(arg1);
|
||||
const ret = typeof(obj) === 'string' ? obj : undefined;
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbg___wbindgen_throw_be289d5034ed271b: function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
},
|
||||
__wbg_entries_58c7934c745daac7: function(arg0) {
|
||||
const ret = Object.entries(getObject(arg0));
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbg_error_7534b8e9a36f1ab4: function(arg0, arg1) {
|
||||
let deferred0_0;
|
||||
let deferred0_1;
|
||||
try {
|
||||
deferred0_0 = arg0;
|
||||
deferred0_1 = arg1;
|
||||
console.error(getStringFromWasm0(arg0, arg1));
|
||||
} finally {
|
||||
wasm.__wbindgen_export3(deferred0_0, deferred0_1, 1);
|
||||
}
|
||||
},
|
||||
__wbg_get_9b94d73e6221f75c: function(arg0, arg1) {
|
||||
const ret = getObject(arg0)[arg1 >>> 0];
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbg_get_with_ref_key_1dc361bd10053bfe: function(arg0, arg1) {
|
||||
const ret = getObject(arg0)[getObject(arg1)];
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbg_instanceof_ArrayBuffer_c367199e2fa2aa04: function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof ArrayBuffer;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
},
|
||||
__wbg_instanceof_Uint8Array_9b9075935c74707c: function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof Uint8Array;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
},
|
||||
__wbg_isSafeInteger_bfbc7332a9768d2a: function(arg0) {
|
||||
const ret = Number.isSafeInteger(getObject(arg0));
|
||||
return ret;
|
||||
},
|
||||
__wbg_length_32ed9a279acd054c: function(arg0) {
|
||||
const ret = getObject(arg0).length;
|
||||
return ret;
|
||||
},
|
||||
__wbg_length_35a7bace40f36eac: function(arg0) {
|
||||
const ret = getObject(arg0).length;
|
||||
return ret;
|
||||
},
|
||||
__wbg_new_361308b2356cecd0: function() {
|
||||
const ret = new Object();
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbg_new_3eb36ae241fe6f44: function() {
|
||||
const ret = new Array();
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbg_new_8a6f238a6ece86ea: function() {
|
||||
const ret = new Error();
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbg_new_dd2b680c8bf6ae29: function(arg0) {
|
||||
const ret = new Uint8Array(getObject(arg0));
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbg_prototypesetcall_bdcdcc5842e4d77d: function(arg0, arg1, arg2) {
|
||||
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2));
|
||||
},
|
||||
__wbg_set_3f1d0b984ed272ed: function(arg0, arg1, arg2) {
|
||||
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
|
||||
},
|
||||
__wbg_set_f43e577aea94465b: function(arg0, arg1, arg2) {
|
||||
getObject(arg0)[arg1 >>> 0] = takeObject(arg2);
|
||||
},
|
||||
__wbg_stack_0ed75d68575b0f3c: function(arg0, arg1) {
|
||||
const ret = getObject(arg1).stack;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbindgen_cast_0000000000000001: function(arg0) {
|
||||
// Cast intrinsic for `F64 -> Externref`.
|
||||
const ret = arg0;
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Ref(String) -> Externref`.
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbindgen_cast_0000000000000003: function(arg0) {
|
||||
// Cast intrinsic for `U64 -> Externref`.
|
||||
const ret = BigInt.asUintN(64, arg0);
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbindgen_object_clone_ref: function(arg0) {
|
||||
const ret = getObject(arg0);
|
||||
return addHeapObject(ret);
|
||||
},
|
||||
__wbindgen_object_drop_ref: function(arg0) {
|
||||
takeObject(arg0);
|
||||
},
|
||||
};
|
||||
return {
|
||||
__proto__: null,
|
||||
"./pi_natives_bg.js": import0,
|
||||
};
|
||||
}
|
||||
|
||||
const CompiledPatternFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||
? { register: () => {}, unregister: () => {} }
|
||||
: new FinalizationRegistry(ptr => wasm.__wbg_compiledpattern_free(ptr >>> 0, 1));
|
||||
const PhotonImageFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||
? { register: () => {}, unregister: () => {} }
|
||||
: new FinalizationRegistry(ptr => wasm.__wbg_photonimage_free(ptr >>> 0, 1));
|
||||
|
||||
function addHeapObject(obj) {
|
||||
if (heap_next === heap.length) heap.push(heap.length + 1);
|
||||
const idx = heap_next;
|
||||
heap_next = heap[idx];
|
||||
|
||||
heap[idx] = obj;
|
||||
return idx;
|
||||
}
|
||||
|
||||
function _assertClass(instance, klass) {
|
||||
if (!(instance instanceof klass)) {
|
||||
throw new Error(`expected instance of ${klass.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
function debugString(val) {
|
||||
// primitive types
|
||||
const type = typeof val;
|
||||
if (type == 'number' || type == 'boolean' || val == null) {
|
||||
return `${val}`;
|
||||
}
|
||||
if (type == 'string') {
|
||||
return `"${val}"`;
|
||||
}
|
||||
if (type == 'symbol') {
|
||||
const description = val.description;
|
||||
if (description == null) {
|
||||
return 'Symbol';
|
||||
} else {
|
||||
return `Symbol(${description})`;
|
||||
}
|
||||
}
|
||||
if (type == 'function') {
|
||||
const name = val.name;
|
||||
if (typeof name == 'string' && name.length > 0) {
|
||||
return `Function(${name})`;
|
||||
} else {
|
||||
return 'Function';
|
||||
}
|
||||
}
|
||||
// objects
|
||||
if (Array.isArray(val)) {
|
||||
const length = val.length;
|
||||
let debug = '[';
|
||||
if (length > 0) {
|
||||
debug += debugString(val[0]);
|
||||
}
|
||||
for(let i = 1; i < length; i++) {
|
||||
debug += ', ' + debugString(val[i]);
|
||||
}
|
||||
debug += ']';
|
||||
return debug;
|
||||
}
|
||||
// Test for built-in
|
||||
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||
let className;
|
||||
if (builtInMatches && builtInMatches.length > 1) {
|
||||
className = builtInMatches[1];
|
||||
} else {
|
||||
// Failed to match the standard '[object ClassName]'
|
||||
return toString.call(val);
|
||||
}
|
||||
if (className == 'Object') {
|
||||
// we're a user defined class or Object
|
||||
// JSON.stringify avoids problems with cycles, and is generally much
|
||||
// easier than looping through ownProperties of `val`.
|
||||
try {
|
||||
return 'Object(' + JSON.stringify(val) + ')';
|
||||
} catch (_) {
|
||||
return 'Object';
|
||||
}
|
||||
}
|
||||
// errors
|
||||
if (val instanceof Error) {
|
||||
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||
}
|
||||
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||
return className;
|
||||
}
|
||||
|
||||
function dropObject(idx) {
|
||||
if (idx < 132) return;
|
||||
heap[idx] = heap_next;
|
||||
heap_next = idx;
|
||||
}
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||
}
|
||||
|
||||
let cachedDataViewMemory0 = null;
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return decodeText(ptr, len);
|
||||
}
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function getObject(idx) { return heap[idx]; }
|
||||
|
||||
let heap = new Array(128).fill(undefined);
|
||||
heap.push(undefined, null, true, false);
|
||||
|
||||
let heap_next = heap.length;
|
||||
|
||||
function isLikeNone(x) {
|
||||
return x === undefined || x === null;
|
||||
}
|
||||
|
||||
function passArray8ToWasm0(arg, malloc) {
|
||||
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
||||
getUint8ArrayMemory0().set(arg, ptr / 1);
|
||||
WASM_VECTOR_LEN = arg.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = cachedTextEncoder.encodeInto(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function takeObject(idx) {
|
||||
const ret = getObject(idx);
|
||||
dropObject(idx);
|
||||
return ret;
|
||||
}
|
||||
|
||||
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
function decodeText(ptr, len) {
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
const cachedTextEncoder = new TextEncoder();
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
const wasmUrl = new URL('pi_natives_bg.wasm', import.meta.url);
|
||||
const wasmInstantiated = await WebAssembly.instantiateStreaming(fetch(wasmUrl), __wbg_get_imports());
|
||||
const wasm = wasmInstantiated.instance.exports;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f1112971f9faa2106c20a5428f9d01684e6b35810482ff0878e65dd413fc6b91
|
||||
size 1897187
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const __wbg_compiledpattern_free: (a: number, b: number) => void;
|
||||
export const __wbg_photonimage_free: (a: number, b: number) => void;
|
||||
export const compiledpattern_has_match: (a: number, b: number, c: number) => number;
|
||||
export const compiledpattern_new: (a: number, b: number) => void;
|
||||
export const compiledpattern_search: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||
export const has_match: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
|
||||
export const photonimage_get_bytes: (a: number, b: number) => void;
|
||||
export const photonimage_get_bytes_jpeg: (a: number, b: number, c: number) => void;
|
||||
export const photonimage_get_height: (a: number) => number;
|
||||
export const photonimage_get_width: (a: number) => number;
|
||||
export const photonimage_new_from_byteslice: (a: number, b: number, c: number) => void;
|
||||
export const resize: (a: number, b: number, c: number, d: number) => number;
|
||||
export const search: (a: number, b: number, c: number) => number;
|
||||
export const compiledpattern_has_match_bytes: (a: number, b: number, c: number) => number;
|
||||
export const compiledpattern_search_bytes: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||
export const __wbindgen_export: (a: number, b: number) => number;
|
||||
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __wbindgen_export3: (a: number, b: number, c: number) => void;
|
||||
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "pi-mono",
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[imports]
|
||||
granularity = { enforce = true, group = "crate" }
|
||||
preferNoStd = false
|
||||
preferPrelude = true
|
||||
prefix = "crate"
|
||||
@@ -0,0 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rustfmt", "clippy", "rust-analyzer"]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
@@ -0,0 +1,73 @@
|
||||
# Edition
|
||||
edition = "2024"
|
||||
style_edition = "2024"
|
||||
|
||||
# Import Organization
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
imports_indent = "Block"
|
||||
reorder_imports = true
|
||||
|
||||
# Comments & Documentation
|
||||
comment_width = 80
|
||||
format_code_in_doc_comments = true
|
||||
normalize_doc_attributes = true
|
||||
wrap_comments = true
|
||||
|
||||
# Width & Length
|
||||
chain_width = 60
|
||||
max_width = 100
|
||||
single_line_if_else_max_width = 50
|
||||
single_line_let_else_max_width = 50
|
||||
|
||||
# Indentation
|
||||
hard_tabs = true
|
||||
indent_style = "Block"
|
||||
tab_spaces = 3
|
||||
|
||||
# Spacing
|
||||
binop_separator = "Front"
|
||||
space_after_colon = true
|
||||
space_before_colon = false
|
||||
spaces_around_ranges = false
|
||||
|
||||
# Trailing Delimiters
|
||||
match_block_trailing_comma = true
|
||||
trailing_comma = "Vertical"
|
||||
trailing_semicolon = true
|
||||
|
||||
# Formatting Behavior
|
||||
condense_wildcard_suffixes = true
|
||||
merge_derives = true
|
||||
overflow_delimited_expr = true
|
||||
remove_nested_parens = true
|
||||
use_field_init_shorthand = true
|
||||
use_small_heuristics = "Max"
|
||||
|
||||
# Macros & Strings
|
||||
format_macro_bodies = true
|
||||
format_macro_matchers = true
|
||||
format_strings = true
|
||||
|
||||
# Code Structure
|
||||
brace_style = "SameLineWhere"
|
||||
fn_params_layout = "Tall"
|
||||
reorder_impl_items = true
|
||||
where_single_line = false
|
||||
|
||||
# Match Expressions
|
||||
match_arm_blocks = true
|
||||
match_arm_leading_pipes = "Never"
|
||||
|
||||
# Alignment
|
||||
enum_discrim_align_threshold = 20
|
||||
struct_field_align_threshold = 20
|
||||
|
||||
# Blank Lines
|
||||
blank_lines_lower_bound = 0
|
||||
blank_lines_upper_bound = 1
|
||||
|
||||
# Miscellaneous
|
||||
force_explicit_abi = true
|
||||
hex_literal_case = "Lower"
|
||||
newline_style = "Unix"
|
||||
@@ -31,6 +31,8 @@
|
||||
"@oh-my-pi/pi-coding-agent": ["./packages/coding-agent/src/index.ts"],
|
||||
"@oh-my-pi/pi-coding-agent/hooks": ["./packages/coding-agent/src/extensibility/hooks/index.ts"],
|
||||
"@oh-my-pi/pi-coding-agent/*": ["./packages/coding-agent/src/*"],
|
||||
"@oh-my-pi/pi-natives": ["./packages/natives/src/index.ts"],
|
||||
"@oh-my-pi/pi-natives/*": ["./packages/natives/src/*"],
|
||||
"@oh-my-pi/omp-stats": ["./packages/stats/src/index.ts"],
|
||||
"@oh-my-pi/omp-stats/src/*": ["./packages/stats/src/*"],
|
||||
"@oh-my-pi/omp-stats/*": ["./packages/stats/src/*"],
|
||||
|
||||
Reference in New Issue
Block a user