plugin sdk
squash
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
/target/
|
/target/
|
||||||
/.tools/
|
/.tools/
|
||||||
/.worktrees
|
/.worktrees
|
||||||
|
/sdk/node_modules/
|
||||||
|
/sdk/dist/
|
||||||
|
|||||||
Generated
+669
-2
@@ -37,6 +37,21 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aligned-vec"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
|
||||||
|
dependencies = [
|
||||||
|
"equator",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-activity"
|
name = "android-activity"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -118,6 +133,18 @@ dependencies = [
|
|||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-channel"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -178,6 +205,148 @@ dependencies = [
|
|||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boa_ast"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9221b0a090572b4a118b46c5874c0781fe4bc9546b85f0a615b8bfb609ca4ed7"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"boa_interner",
|
||||||
|
"boa_macros",
|
||||||
|
"boa_string",
|
||||||
|
"indexmap",
|
||||||
|
"num-bigint",
|
||||||
|
"rustc-hash 2.1.3",
|
||||||
|
"strum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boa_engine"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d6aaf7fdc4299df68adb67e1a5440a51d34630a1f5271b9698fd74bc97b60d9"
|
||||||
|
dependencies = [
|
||||||
|
"aligned-vec",
|
||||||
|
"arrayvec",
|
||||||
|
"async-channel",
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"boa_ast",
|
||||||
|
"boa_gc",
|
||||||
|
"boa_interner",
|
||||||
|
"boa_macros",
|
||||||
|
"boa_parser",
|
||||||
|
"boa_string",
|
||||||
|
"bytemuck",
|
||||||
|
"cfg-if",
|
||||||
|
"cow-utils",
|
||||||
|
"dashmap",
|
||||||
|
"dynify",
|
||||||
|
"fast-float2",
|
||||||
|
"futures-concurrency",
|
||||||
|
"futures-lite",
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
"icu_normalizer",
|
||||||
|
"indexmap",
|
||||||
|
"intrusive-collections",
|
||||||
|
"itertools",
|
||||||
|
"num-bigint",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"num_enum",
|
||||||
|
"oneshot",
|
||||||
|
"pastey",
|
||||||
|
"portable-atomic",
|
||||||
|
"rand",
|
||||||
|
"regress",
|
||||||
|
"rustc-hash 2.1.3",
|
||||||
|
"ryu-js",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"small_btree",
|
||||||
|
"static_assertions",
|
||||||
|
"tag_ptr",
|
||||||
|
"tap",
|
||||||
|
"thin-vec",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boa_gc"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af82119558ab1002d1978c8459b53a3ebb506177d29a3aca37c07c1777b1d966"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"boa_macros",
|
||||||
|
"boa_string",
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
"thin-vec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boa_interner"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "317c631e791f5cb7d0017e43c0a758b0eaffd40c34c4c360e2a6be7752b78cd1"
|
||||||
|
dependencies = [
|
||||||
|
"boa_gc",
|
||||||
|
"boa_macros",
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
"indexmap",
|
||||||
|
"once_cell",
|
||||||
|
"phf",
|
||||||
|
"rustc-hash 2.1.3",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boa_macros"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b882fd440dc0f4b7441367cf89567bb077920356750c9a76a16c002d8ec2fa5c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cow-utils",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.5",
|
||||||
|
"synstructure 0.14.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boa_parser"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "641e81b3c8cbb7a2140547fb3ea534e9edf210b4e8c920b5fa864a919e11fbcf"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"boa_ast",
|
||||||
|
"boa_interner",
|
||||||
|
"boa_macros",
|
||||||
|
"fast-float2",
|
||||||
|
"icu_properties",
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"regress",
|
||||||
|
"rustc-hash 2.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boa_string"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30bf86c2bcff5bf2547acbfc516c5d3108ca527cb131511d749bf6fbef762c0e"
|
||||||
|
dependencies = [
|
||||||
|
"fast-float2",
|
||||||
|
"itoa",
|
||||||
|
"pastey",
|
||||||
|
"rustc-hash 2.1.3",
|
||||||
|
"ryu-js",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.3"
|
version = "3.20.3"
|
||||||
@@ -306,6 +475,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
@@ -413,6 +593,21 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cow-utils"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
@@ -440,6 +635,26 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashmap"
|
||||||
|
version = "6.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"lock_api",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -497,6 +712,26 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dynify"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81acb15628a3e22358bf73de5e7e62360b8a777dbcb5fc9ac7dfa9ae73723747"
|
||||||
|
dependencies = [
|
||||||
|
"dynify-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dynify-macros"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ec431cd708430d5029356535259c5d645d60edd3d39c54e5eea9782d46caa7d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecolor"
|
name = "ecolor"
|
||||||
version = "0.33.3"
|
version = "0.33.3"
|
||||||
@@ -618,6 +853,12 @@ dependencies = [
|
|||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "emath"
|
name = "emath"
|
||||||
version = "0.33.3"
|
version = "0.33.3"
|
||||||
@@ -651,6 +892,26 @@ version = "0.33.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862"
|
checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equator"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
|
||||||
|
dependencies = [
|
||||||
|
"equator-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equator-macro"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -673,6 +934,38 @@ version = "3.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
|
checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener"
|
||||||
|
version = "5.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
|
||||||
|
dependencies = [
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener-strategy"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fast-float2"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6e8948ce679d00a02a94739ea185595dca7118ed04feb991127e443bd3d761f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fax"
|
name = "fax"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -705,6 +998,12 @@ version = "0.1.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fixedbitset"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.10"
|
version = "1.1.10"
|
||||||
@@ -787,12 +1086,44 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-concurrency"
|
||||||
|
version = "7.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6"
|
||||||
|
dependencies = [
|
||||||
|
"fixedbitset",
|
||||||
|
"futures-core",
|
||||||
|
"futures-lite",
|
||||||
|
"pin-project",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-lite"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@@ -842,6 +1173,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -984,6 +1316,12 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -1007,6 +1345,17 @@ name = "hashbrown"
|
|||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
@@ -1058,6 +1407,8 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
"icu_provider",
|
"icu_provider",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"utf16_iter",
|
||||||
|
"write16",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1160,6 +1511,27 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "intrusive-collections"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4275b20e6057cd7733fd8df8a5a31701e4fe44497dad0f3fa0e1c4fb971506be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
version = "0.22.4"
|
version = "0.22.4"
|
||||||
@@ -1505,6 +1877,32 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -1537,6 +1935,15 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc"
|
name = "objc"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -1823,6 +2230,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oneshot"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orbclient"
|
name = "orbclient"
|
||||||
version = "0.3.55"
|
version = "0.3.55"
|
||||||
@@ -1851,6 +2264,12 @@ dependencies = [
|
|||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -1880,12 +2299,60 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pastey"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.13"
|
version = "1.1.13"
|
||||||
@@ -2002,6 +2469,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
@@ -2080,6 +2553,23 @@ version = "6.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -2113,6 +2603,15 @@ dependencies = [
|
|||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regress"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32eef8b209c3c1c15dbad02c1f30f9539f00dc7253e0cbcdaae442a50a09d7c1"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "renderdoc-sys"
|
name = "renderdoc-sys"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -2178,6 +2677,12 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu-js"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04d056b875a9d2e6cb9a61d127afee9ac5999b9f87bcb32079d1318e505be714"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -2235,6 +2740,19 @@ dependencies = [
|
|||||||
"syn 3.0.5",
|
"syn 3.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -2299,6 +2817,12 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -2312,6 +2836,7 @@ dependencies = [
|
|||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
"boa_engine",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
@@ -2319,8 +2844,10 @@ dependencies = [
|
|||||||
"png",
|
"png",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"shell-words",
|
"shell-words",
|
||||||
"toml",
|
"toml",
|
||||||
|
"ts-rs",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2333,6 +2860,15 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "small_btree"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ba60d2df92ba73864714808ca68c059734853e6ab722b40e1cf543ebb3a057a"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
@@ -2432,6 +2968,27 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.119"
|
version = "2.0.119"
|
||||||
@@ -2465,6 +3022,44 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "synstructure"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tag_ptr"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0e973b34477b7823833469eb0f5a3a60370fef7a453e02d751b59180d0a5a05"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tap"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thin-vec"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79def32ffcd477db1ff26f76dab9e3a91f0bd42a85ca96577089b24623056f9d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
@@ -2519,6 +3114,38 @@ dependencies = [
|
|||||||
"zune-jpeg",
|
"zune-jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.55"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"libc",
|
||||||
|
"num-conv",
|
||||||
|
"num_threads",
|
||||||
|
"powerfmt",
|
||||||
|
"serde_core",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
@@ -2621,6 +3248,28 @@ version = "0.1.36"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ts-rs"
|
||||||
|
version = "12.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"ts-rs-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ts-rs-macros"
|
||||||
|
version = "12.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
"termcolor",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
@@ -2669,6 +3318,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf16_iter"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -3419,6 +4074,12 @@ version = "0.57.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "write16"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -3508,7 +4169,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
"synstructure",
|
"synstructure 0.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3549,7 +4210,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
"synstructure",
|
"synstructure 0.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3591,6 +4252,12 @@ version = "0.6.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ license = "MIT"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
ab_glyph = "0.2"
|
ab_glyph = "0.2"
|
||||||
|
boa_engine = { version = "0.22", default-features = false }
|
||||||
fontdb = "0.23"
|
fontdb = "0.23"
|
||||||
eframe = { version = "0.33", default-features = false, features = ["default_fonts", "wgpu", "wayland", "x11"] }
|
eframe = { version = "0.33", default-features = false, features = ["default_fonts", "wgpu", "wayland", "x11"] }
|
||||||
egui = "0.33"
|
egui = "0.33"
|
||||||
@@ -16,8 +17,10 @@ libghostty-vt = { version = "=0.2.1", features = ["png"] }
|
|||||||
png = "0.18"
|
png = "0.18"
|
||||||
portable-pty = "0.9"
|
portable-pty = "0.9"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
shell-words = "1"
|
shell-words = "1"
|
||||||
toml = "0.9"
|
toml = "0.9"
|
||||||
|
ts-rs = "12"
|
||||||
wgpu = { version = "27", default-features = false, features = ["std", "vulkan", "gles", "metal", "wgsl"] }
|
wgpu = { version = "27", default-features = false, features = ["std", "vulkan", "gles", "metal", "wgsl"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -78,3 +78,10 @@ floating_max_height = 640
|
|||||||
"Ctrl+Shift+R" = "reload_config"
|
"Ctrl+Shift+R" = "reload_config"
|
||||||
"Ctrl+Shift+C" = "copy"
|
"Ctrl+Shift+C" = "copy"
|
||||||
"Ctrl+Shift+V" = "paste"
|
"Ctrl+Shift+V" = "paste"
|
||||||
|
|
||||||
|
# TypeScript plugins compile to JavaScript bundles and run in embedded Boa.
|
||||||
|
# See sdk/README.md. Relative paths resolve beside this configuration file.
|
||||||
|
# Restart Slerm after changing a plugin bundle or these entries.
|
||||||
|
# [[plugins]]
|
||||||
|
# id = "workspace"
|
||||||
|
# path = "plugins/workspace.js"
|
||||||
|
|||||||
@@ -177,6 +177,9 @@
|
|||||||
./Cargo.lock
|
./Cargo.lock
|
||||||
./config.example.toml
|
./config.example.toml
|
||||||
./src
|
./src
|
||||||
|
# Embedded runtime and the bindings checked by Rust tests.
|
||||||
|
./sdk/runtime.js
|
||||||
|
./sdk/src/generated.ts
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
|||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
# Slerm TypeScript plugin SDK
|
||||||
|
|
||||||
|
Slerm runs JavaScript bundles in **embedded Boa**, with a separate worker thread
|
||||||
|
and context for each plugin. End users do not need Node.js or Bun. Plugin authors
|
||||||
|
use TypeScript and a bundler during development.
|
||||||
|
|
||||||
|
## Try the example
|
||||||
|
|
||||||
|
From the Slerm repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm ci --prefix sdk
|
||||||
|
npm run example --prefix sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this to your Slerm configuration, replacing the absolute path:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[plugins]]
|
||||||
|
id = "workspace"
|
||||||
|
path = "/absolute/path/to/slerm/sdk/dist/workspace.js"
|
||||||
|
```
|
||||||
|
|
||||||
|
Start Slerm. The example opens **Workspace tools**, with controls to create a
|
||||||
|
floating pane, run a shell command, and inspect terminal text. Reopen the panel
|
||||||
|
from **Plugins → workspace → Show workspace tools**.
|
||||||
|
|
||||||
|
Bundle paths can also be relative to the configuration file's directory. Plugins
|
||||||
|
load at startup; restart Slerm after changing a bundle or the plugin configuration.
|
||||||
|
|
||||||
|
## Write a plugin
|
||||||
|
|
||||||
|
Build the SDK with `npm run build --prefix sdk`. In a separate plugin project,
|
||||||
|
install the local package with `npm install /absolute/path/to/slerm/sdk` and add
|
||||||
|
TypeScript and esbuild as development dependencies. The package has not been
|
||||||
|
published to a registry.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { definePlugin } from "@slerm/plugin-sdk";
|
||||||
|
|
||||||
|
definePlugin({
|
||||||
|
async activate(slerm) {
|
||||||
|
await slerm.registerCommand({
|
||||||
|
command: { id: "scratch", title: "New scratch pane" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async onEvent(event, slerm) {
|
||||||
|
if (event.event === "command" && event.command === "scratch") {
|
||||||
|
await slerm.execute({ action: "new_floating" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Type-check your entry point, then bundle it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx tsc --noEmit --strict --target es2022 --module nodenext plugin.ts
|
||||||
|
npx esbuild plugin.ts --bundle --format=iife --platform=neutral --target=es2022 --outfile=plugin.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Point a `[[plugins]]` entry at `plugin.js`. Each bundle calls `definePlugin` once.
|
||||||
|
All API calls return promises that resolve after Slerm applies the operation.
|
||||||
|
Events invoke `onEvent(event, slerm)`; callbacks can be async.
|
||||||
|
|
||||||
|
## Generated API
|
||||||
|
|
||||||
|
The Rust definitions in [`src/plugin_api.rs`](../src/plugin_api.rs) are the source
|
||||||
|
of truth. A Rust method list declares parameters and results; `ts-rs` generates
|
||||||
|
`src/generated.ts`, and TypeScript derives the client method signatures from those
|
||||||
|
types. Actions come from Slerm's existing Rust action enum. State, UI elements,
|
||||||
|
and events also use the same Rust types for serialization and type generation.
|
||||||
|
The generic runtime bridge does not duplicate a method list or handwritten types.
|
||||||
|
|
||||||
|
Regenerate after changing the Rust API:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix develop -c cargo run -- --generate-plugin-sdk
|
||||||
|
npm test --prefix sdk
|
||||||
|
nix develop -c cargo test
|
||||||
|
nix develop -c cargo test bundled_typescript_example_runs_in_boa -- --ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
Without Nix, run Cargo in an environment containing the project's Rust and Zig
|
||||||
|
build dependencies. Commit `sdk/src/generated.ts` with API changes. A Rust test
|
||||||
|
fails if the checked-in bindings differ from the schema. Bump `API_VERSION` for
|
||||||
|
incompatible changes; Slerm rejects bundles built for another API version.
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `getState()` | Read tabs, panes, titles, focus, floating state, and zoom |
|
||||||
|
| `execute({ action })` | Execute a built-in pane, tab, clipboard, or settings action |
|
||||||
|
| `focusPane({ pane })`, `focusTab({ tab })` | Focus by ID from `getState()` |
|
||||||
|
| `readTerminal({ pane })` | Read a plain-text screen and scrollback snapshot, up to 128 KiB |
|
||||||
|
| `writeTerminal({ pane, text })` | Send raw PTY input; include `\r` to submit a shell command |
|
||||||
|
| `registerCommand({ command })`, `unregisterCommand({ command })` | Manage entries in the Plugins menu |
|
||||||
|
| `setPanel({ panel })`, `removePanel({ panel })` | Create, replace, or remove a native panel |
|
||||||
|
| `reportError({ message })` | Display an error in Slerm |
|
||||||
|
|
||||||
|
Command and panel IDs are scoped to the plugin. Control IDs must be unique within
|
||||||
|
a panel. Pane and tab IDs come from Slerm and should be treated as opaque numbers.
|
||||||
|
|
||||||
|
Events are discriminated by `event.event`:
|
||||||
|
|
||||||
|
- `command`: `event.command` is the selected command ID.
|
||||||
|
- `state`: `event.state` is a snapshot when workspace metadata changes. Terminal
|
||||||
|
output changes alone do not produce this event; use `readTerminal` for snapshots.
|
||||||
|
- `ui`: `event.data` contains the panel/control IDs and either a `click`, or a
|
||||||
|
`change` with the new input `value`.
|
||||||
|
|
||||||
|
## Native UI
|
||||||
|
|
||||||
|
`setPanel` renders an egui floating window. Its `elements` accept `text`, `button`,
|
||||||
|
`input`, and `separator` variants. Inputs retain edits until replaced by another
|
||||||
|
`setPanel` call. UI focus receives typing without forwarding it to the terminal.
|
||||||
|
See [`examples/workspace.ts`](examples/workspace.ts) for a complete example.
|
||||||
|
|
||||||
|
## Runtime boundaries
|
||||||
|
|
||||||
|
The runtime supports ECMAScript and promises. This initial host does not expose
|
||||||
|
Node.js APIs, a DOM, filesystem/network APIs, or timers. Bundle imports ahead of
|
||||||
|
time and use libraries compatible with this environment. Report diagnostics with
|
||||||
|
`reportError`; `console` is not installed.
|
||||||
|
|
||||||
|
Plugins are trusted local extensions: terminal writes can run shell commands.
|
||||||
|
Boa contexts run in the same OS process as Slerm; worker threads and execution
|
||||||
|
limits are not an OS security sandbox. Slerm bounds loops, recursion, promise jobs,
|
||||||
|
request waits, bundle/message sizes, and UI counts. An uncaught callback error
|
||||||
|
stops that plugin and removes its commands and panels. Host-operation errors
|
||||||
|
reject the API promise and can be handled by the plugin.
|
||||||
|
|
||||||
|
Implementation references: [Boa](https://boajs.dev/docs/intro) and
|
||||||
|
[ts-rs](https://docs.rs/ts-rs/latest/ts_rs/).
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { definePlugin, type Slerm } from "../src/index.js";
|
||||||
|
|
||||||
|
let command = "pwd";
|
||||||
|
const show = (slerm: Slerm) => slerm.setPanel({ panel: {
|
||||||
|
id: "workspace", title: "Workspace tools", elements: [
|
||||||
|
{ type: "text", text: "Create a scratch terminal or run a command in the active pane." },
|
||||||
|
{ type: "button", id: "scratch", label: "New floating pane" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ type: "input", id: "command", label: "Shell command", value: command },
|
||||||
|
{ type: "button", id: "run", label: "Run in active pane" },
|
||||||
|
{ type: "button", id: "read", label: "Show terminal text" },
|
||||||
|
{ type: "button", id: "close", label: "Close tools" },
|
||||||
|
],
|
||||||
|
} });
|
||||||
|
|
||||||
|
definePlugin({
|
||||||
|
async activate(slerm) {
|
||||||
|
await slerm.registerCommand({ command: { id: "show-tools", title: "Show workspace tools" } });
|
||||||
|
await show(slerm);
|
||||||
|
},
|
||||||
|
async onEvent(event, slerm) {
|
||||||
|
if (event.event === "command" && event.command === "show-tools") await show(slerm);
|
||||||
|
if (event.event !== "ui" || event.data.panel !== "workspace") return;
|
||||||
|
const control = event.data;
|
||||||
|
if (control.kind === "change" && control.control === "command") command = control.value;
|
||||||
|
if (control.kind !== "click") return;
|
||||||
|
if (control.control === "scratch") await slerm.execute({ action: "new_floating" });
|
||||||
|
if (control.control === "close") await slerm.removePanel({ panel: "workspace" });
|
||||||
|
if (control.control === "run" || control.control === "read") {
|
||||||
|
const state = await slerm.getState();
|
||||||
|
const pane = state.tabs.find(tab => tab.id === state.activeTab)?.activePane;
|
||||||
|
if (pane === undefined) return;
|
||||||
|
if (control.control === "run") await slerm.writeTerminal({ pane, text: command + "\r" });
|
||||||
|
else await slerm.setPanel({ panel: {
|
||||||
|
id: "output", title: "Terminal snapshot",
|
||||||
|
elements: [{ type: "text", text: await slerm.readTerminal({ pane }) }],
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
Generated
+515
@@ -0,0 +1,515 @@
|
|||||||
|
{
|
||||||
|
"name": "@slerm/plugin-sdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@slerm/plugin-sdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.27.0",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.7",
|
||||||
|
"@esbuild/android-arm": "0.27.7",
|
||||||
|
"@esbuild/android-arm64": "0.27.7",
|
||||||
|
"@esbuild/android-x64": "0.27.7",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.7",
|
||||||
|
"@esbuild/darwin-x64": "0.27.7",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.7",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.7",
|
||||||
|
"@esbuild/linux-arm": "0.27.7",
|
||||||
|
"@esbuild/linux-arm64": "0.27.7",
|
||||||
|
"@esbuild/linux-ia32": "0.27.7",
|
||||||
|
"@esbuild/linux-loong64": "0.27.7",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.7",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.7",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.7",
|
||||||
|
"@esbuild/linux-s390x": "0.27.7",
|
||||||
|
"@esbuild/linux-x64": "0.27.7",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.7",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.7",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.7",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.7",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.7",
|
||||||
|
"@esbuild/sunos-x64": "0.27.7",
|
||||||
|
"@esbuild/win32-arm64": "0.27.7",
|
||||||
|
"@esbuild/win32-ia32": "0.27.7",
|
||||||
|
"@esbuild/win32-x64": "0.27.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@slerm/plugin-sdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Generated TypeScript API for Slerm's embedded Boa plugins",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"generate": "cargo run --manifest-path ../Cargo.toml -- --generate-plugin-sdk",
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"check": "npm run build && tsc -p tsconfig.examples.json",
|
||||||
|
"example": "npm run check && esbuild examples/workspace.ts --bundle --format=iife --platform=neutral --target=es2022 --outfile=dist/workspace.js",
|
||||||
|
"test": "npm run example && node --test test/*.test.mjs",
|
||||||
|
"prepack": "npm run build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.27.0",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Generic Boa bridge. API methods and their types come from the Rust schema.
|
||||||
|
(() => {
|
||||||
|
const call = __slerm_call;
|
||||||
|
const fail = __slerm_error;
|
||||||
|
let plugin;
|
||||||
|
const api = new Proxy(Object.create(null), {
|
||||||
|
get: (_, method) => method === "then" ? undefined : async (params = {}) => {
|
||||||
|
const reply = JSON.parse(call(JSON.stringify({ ...params, method })));
|
||||||
|
if (reply.error !== undefined) throw new Error(reply.error);
|
||||||
|
return reply.result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const invoke = (callback, ...args) => {
|
||||||
|
if (callback) Promise.resolve().then(() => callback(...args)).catch(error => fail(String(error)));
|
||||||
|
};
|
||||||
|
Object.defineProperty(globalThis, "__slerm", { value: Object.freeze({
|
||||||
|
activate(definition, version) {
|
||||||
|
if (version !== __slerm_version) throw new Error(`Unsupported Slerm API version ${version}`);
|
||||||
|
if (plugin) throw new Error("A bundle must define exactly one plugin");
|
||||||
|
plugin = definition;
|
||||||
|
invoke(plugin.activate, api);
|
||||||
|
},
|
||||||
|
}) });
|
||||||
|
Object.defineProperty(globalThis, "__slerm_dispatch", { value: event => {
|
||||||
|
if (!plugin) throw new Error("Bundle did not call definePlugin");
|
||||||
|
invoke(plugin.onEvent, event, api);
|
||||||
|
} });
|
||||||
|
})();
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Generated by cargo run -- --generate-plugin-sdk. Do not edit.
|
||||||
|
|
||||||
|
export const apiVersion = 1 as const;
|
||||||
|
|
||||||
|
export type Action = "split_right" | "split_down" | "new_floating" | "new_pane" | "toggle_floating" | "toggle_floats" | "next_pane" | "previous_pane" | "focus_left" | "focus_down" | "focus_up" | "focus_right" | "close_pane" | "zoom_pane" | "new_tab" | "next_tab" | "previous_tab" | "reload_config" | "copy" | "paste" | "open_scrollback";
|
||||||
|
|
||||||
|
export type Operation = { "method": "getState", } | { "method": "execute", action: Action, } | { "method": "focusPane", pane: number, } | { "method": "focusTab", tab: number, } | { "method": "readTerminal", pane: number, } | { "method": "writeTerminal", pane: number, text: string, } | { "method": "registerCommand", command: PluginCommand, } | { "method": "unregisterCommand", command: string, } | { "method": "setPanel", panel: Panel, } | { "method": "removePanel", panel: string, } | { "method": "reportError", message: string, };
|
||||||
|
|
||||||
|
export type Results = { getState: State, execute: null, focusPane: null, focusTab: null, readTerminal: string, writeTerminal: null, registerCommand: null, unregisterCommand: null, setPanel: null, removePanel: null, reportError: null, };
|
||||||
|
|
||||||
|
export type State = { activeTab: number | null, tabs: Array<TabState>, };
|
||||||
|
|
||||||
|
export type TabState = { id: number, activePane: number, floatsVisible: boolean, zoomed: boolean, panes: Array<PaneState>, };
|
||||||
|
|
||||||
|
export type PaneState = { id: number, title: string, floating: boolean, exited: boolean, };
|
||||||
|
|
||||||
|
export type PluginCommand = { id: string, title: string, };
|
||||||
|
|
||||||
|
export type Panel = { id: string, title: string, elements: Array<Element>, };
|
||||||
|
|
||||||
|
export type Element = { "type": "text", text: string, } | { "type": "button", id: string, label: string, } | { "type": "input", id: string, label: string, value: string, } | { "type": "separator" };
|
||||||
|
|
||||||
|
export type UIEvent = { "kind": "click", panel: string, control: string, } | { "kind": "change", panel: string, control: string, value: string, };
|
||||||
|
|
||||||
|
export type Event = { "event": "command", command: string, } | { "event": "state", state: State, } | { "event": "ui", data: UIEvent, };
|
||||||
|
|
||||||
|
type Params<M extends Operation["method"]> = Omit<Extract<Operation, { method: M }>, "method">;
|
||||||
|
export type Slerm = {
|
||||||
|
[M in Operation["method"]]: (...args: keyof Params<M> extends never ? [] : [params: Params<M>]) => Promise<Results[M] extends null ? void : Results[M]>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { apiVersion } from "./generated.js";
|
||||||
|
import type { Event, Slerm } from "./generated.js";
|
||||||
|
export * from "./generated.js";
|
||||||
|
|
||||||
|
export interface Plugin {
|
||||||
|
activate?(slerm: Slerm): void | Promise<void>;
|
||||||
|
onEvent?(event: Event, slerm: Slerm): void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const __slerm: { activate(plugin: Plugin, version: number): void };
|
||||||
|
|
||||||
|
/** Register one plugin in its embedded Boa runtime. Bundle this entry point. */
|
||||||
|
export function definePlugin(plugin: Plugin): void {
|
||||||
|
__slerm.activate(plugin, apiVersion);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { runInNewContext, runInContext } from "node:vm";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
const bridge = readFileSync(new URL("../runtime.js", import.meta.url), "utf8");
|
||||||
|
const example = readFileSync(new URL("../dist/workspace.js", import.meta.url), "utf8");
|
||||||
|
const settle = () => new Promise(resolve => setImmediate(resolve));
|
||||||
|
|
||||||
|
function start(reply = () => ({ result: null })) {
|
||||||
|
const operations = [];
|
||||||
|
const errors = [];
|
||||||
|
const context = {
|
||||||
|
__slerm_version: 1,
|
||||||
|
__slerm_call: text => {
|
||||||
|
const operation = JSON.parse(text);
|
||||||
|
operations.push(operation);
|
||||||
|
return JSON.stringify(reply(operation));
|
||||||
|
},
|
||||||
|
__slerm_error: error => errors.push(error),
|
||||||
|
};
|
||||||
|
runInNewContext(bridge, context);
|
||||||
|
return { context, operations, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("bundled TypeScript example uses generated methods and handles UI and command events", async () => {
|
||||||
|
const host = start(operation => ({ result: operation.method === "getState"
|
||||||
|
? { activeTab: 1, tabs: [{ id: 1, activePane: 2 }] }
|
||||||
|
: operation.method === "readTerminal" ? "hello from terminal" : null }));
|
||||||
|
runInContext(example, host.context);
|
||||||
|
await settle();
|
||||||
|
assert.deepEqual(host.operations.map(op => op.method), ["registerCommand", "setPanel"]);
|
||||||
|
assert.equal(host.operations[1].panel.elements[3].type, "input");
|
||||||
|
const ui = data => host.context.__slerm_dispatch({ event: "ui", data: { panel: "workspace", ...data } });
|
||||||
|
ui({ kind: "change", control: "command", value: "echo hello" });
|
||||||
|
await settle();
|
||||||
|
ui({ kind: "click", control: "run" });
|
||||||
|
await settle();
|
||||||
|
assert.deepEqual(host.operations.at(-1), { method: "writeTerminal", pane: 2, text: "echo hello\r" });
|
||||||
|
ui({ kind: "click", control: "scratch" });
|
||||||
|
await settle();
|
||||||
|
assert.deepEqual(host.operations.at(-1), { method: "execute", action: "new_floating" });
|
||||||
|
ui({ kind: "click", control: "read" });
|
||||||
|
await settle();
|
||||||
|
assert.equal(host.operations.at(-1).panel.elements[0].text, "hello from terminal");
|
||||||
|
ui({ kind: "click", control: "close" });
|
||||||
|
await settle();
|
||||||
|
assert.equal(host.operations.at(-1).method, "removePanel");
|
||||||
|
host.context.__slerm_dispatch({ event: "command", command: "show-tools" });
|
||||||
|
await settle();
|
||||||
|
assert.equal(host.operations.at(-1).method, "setPanel");
|
||||||
|
assert.deepEqual(host.errors, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("API mismatch and duplicate registration fail before activation", () => {
|
||||||
|
const { context } = start();
|
||||||
|
assert.throws(() => context.__slerm.activate({}, 2), /Unsupported Slerm API/);
|
||||||
|
context.__slerm.activate({}, 1);
|
||||||
|
assert.throws(() => context.__slerm.activate({}, 1), /exactly one plugin/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("host errors reject generated calls and surface callback failures", async () => {
|
||||||
|
const host = start(() => ({ error: "unknown pane" }));
|
||||||
|
runInContext(example, host.context);
|
||||||
|
await settle();
|
||||||
|
assert.deepEqual(host.errors, ["Error: unknown pane"]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Slerm, State } from "../src/index.js";
|
||||||
|
|
||||||
|
declare const api: Slerm;
|
||||||
|
const state: Promise<State> = api.getState();
|
||||||
|
const text: Promise<string> = api.readTerminal({ pane: 1 });
|
||||||
|
void state; void text;
|
||||||
|
// @ts-expect-error Generated methods reject unknown actions.
|
||||||
|
api.execute({ action: "missing" });
|
||||||
|
// @ts-expect-error Pane IDs are numbers in the JSON API.
|
||||||
|
api.focusPane({ pane: "1" });
|
||||||
|
// @ts-expect-error A terminal write requires text.
|
||||||
|
api.writeTerminal({ pane: 1 });
|
||||||
|
// @ts-expect-error Parameterless methods take no parameters.
|
||||||
|
api.getState({});
|
||||||
|
// @ts-expect-error Panel elements are generated from the Rust enum.
|
||||||
|
api.setPanel({ panel: { id: "test", title: "Test", elements: [{ type: "html", html: "" }] } });
|
||||||
|
// @ts-expect-error Return types come from the Rust method list.
|
||||||
|
const invalid: Promise<number> = api.readTerminal({ pane: 1 });
|
||||||
|
void invalid;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": { "noEmit": true, "rootDir": "." },
|
||||||
|
"include": ["examples/**/*.ts", "test/types.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"types": [],
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
+325
-3
@@ -3,6 +3,8 @@ use crate::{
|
|||||||
input,
|
input,
|
||||||
layout::{Axis, Direction, Layout, PaneId, Tab},
|
layout::{Axis, Direction, Layout, PaneId, Tab},
|
||||||
pane::{self, Pane},
|
pane::{self, Pane},
|
||||||
|
plugin_api::{Event, Operation, PaneState, State, TabState},
|
||||||
|
plugins::{self, Host},
|
||||||
theme::{self, Theme},
|
theme::{self, Theme},
|
||||||
};
|
};
|
||||||
use anyhow::{Result, ensure};
|
use anyhow::{Result, ensure};
|
||||||
@@ -22,6 +24,7 @@ pub struct App {
|
|||||||
next_id: u64,
|
next_id: u64,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
suppressed: HashSet<egui::Key>,
|
suppressed: HashSet<egui::Key>,
|
||||||
|
plugins: Host,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Width kept free at the left of the tab bar for the macOS traffic lights,
|
/// Width kept free at the left of the tab bar for the macOS traffic lights,
|
||||||
@@ -69,6 +72,11 @@ impl App {
|
|||||||
cc.egui_ctx.set_visuals(theme.visuals());
|
cc.egui_ctx.set_visuals(theme.visuals());
|
||||||
cc.egui_ctx.set_fonts(fonts(&config)?);
|
cc.egui_ctx.set_fonts(fonts(&config)?);
|
||||||
let pane = Pane::new(&config, &theme, &cc.egui_ctx, None)?;
|
let pane = Pane::new(&config, &theme, &cc.egui_ctx, None)?;
|
||||||
|
let config_dir = config_path
|
||||||
|
.parent()
|
||||||
|
.filter(|p| !p.as_os_str().is_empty())
|
||||||
|
.unwrap_or(std::path::Path::new("."));
|
||||||
|
let (plugins, errors) = Host::start(&config.plugins, config_dir, &cc.egui_ctx);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config,
|
config,
|
||||||
theme,
|
theme,
|
||||||
@@ -77,8 +85,9 @@ impl App {
|
|||||||
tabs: vec![Tab::new(1, 1)],
|
tabs: vec![Tab::new(1, 1)],
|
||||||
current: 0,
|
current: 0,
|
||||||
next_id: 2,
|
next_id: 2,
|
||||||
error: None,
|
error: (!errors.is_empty()).then(|| errors.join("\n")),
|
||||||
suppressed: HashSet::new(),
|
suppressed: HashSet::new(),
|
||||||
|
plugins,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +301,175 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn plugin_state(&self) -> State {
|
||||||
|
State {
|
||||||
|
active_tab: self.tabs.get(self.current).map(|tab| tab.id),
|
||||||
|
tabs: self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.map(|tab| TabState {
|
||||||
|
id: tab.id,
|
||||||
|
active_pane: tab.active,
|
||||||
|
floats_visible: tab.floats_visible,
|
||||||
|
zoomed: tab.zoomed,
|
||||||
|
panes: tab
|
||||||
|
.ids()
|
||||||
|
.iter()
|
||||||
|
.map(|id| PaneState {
|
||||||
|
id: *id,
|
||||||
|
floating: tab.floats.contains(id),
|
||||||
|
title: self.panes[id].terminal.title().unwrap_or("").into(),
|
||||||
|
exited: self.panes[id].dead,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plugin_request(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
operation: Operation,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
use serde_json::json;
|
||||||
|
match operation {
|
||||||
|
Operation::GetState {} => return Ok(serde_json::to_value(self.plugin_state())?),
|
||||||
|
Operation::Execute { action } => {
|
||||||
|
ensure!(!self.tabs.is_empty(), "no active tab");
|
||||||
|
self.action(action, ctx)?;
|
||||||
|
}
|
||||||
|
Operation::FocusPane { pane } => {
|
||||||
|
let index = self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.position(|tab| tab.ids().contains(&pane))
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("unknown pane {pane}"))?;
|
||||||
|
self.current = index;
|
||||||
|
let tab = &mut self.tabs[index];
|
||||||
|
tab.active = pane;
|
||||||
|
tab.floats_visible = tab.floats.contains(&pane);
|
||||||
|
}
|
||||||
|
Operation::FocusTab { tab } => {
|
||||||
|
self.current = self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.position(|candidate| candidate.id == tab)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("unknown tab {tab}"))?;
|
||||||
|
}
|
||||||
|
Operation::ReadTerminal { pane } => {
|
||||||
|
use libghostty_vt::fmt::{Formatter, FormatterOptions};
|
||||||
|
let pane = self
|
||||||
|
.panes
|
||||||
|
.get(&pane)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("unknown pane {pane}"))?;
|
||||||
|
let bytes = Formatter::new(
|
||||||
|
&pane.terminal,
|
||||||
|
FormatterOptions::new().with_trim(true).with_unwrap(true),
|
||||||
|
)?
|
||||||
|
.format_alloc(None)?;
|
||||||
|
ensure!(
|
||||||
|
bytes.len() <= 128 * 1024,
|
||||||
|
"terminal snapshot exceeds 128 KiB"
|
||||||
|
);
|
||||||
|
return Ok(json!(String::from_utf8_lossy(&bytes)));
|
||||||
|
}
|
||||||
|
Operation::WriteTerminal { pane, text } => {
|
||||||
|
self.panes
|
||||||
|
.get_mut(&pane)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("unknown pane {pane}"))?
|
||||||
|
.send(text.into_bytes())?;
|
||||||
|
}
|
||||||
|
Operation::RegisterCommand { command } => {
|
||||||
|
ensure!(plugins::valid_id(&command.id), "invalid command id");
|
||||||
|
ensure!(
|
||||||
|
!command.title.trim().is_empty() && command.title.len() <= 200,
|
||||||
|
"invalid command title"
|
||||||
|
);
|
||||||
|
let commands = &mut self.plugins.runtimes[index].commands;
|
||||||
|
ensure!(
|
||||||
|
commands.contains_key(&command.id) || commands.len() < 64,
|
||||||
|
"command limit reached"
|
||||||
|
);
|
||||||
|
commands.insert(command.id.clone(), command);
|
||||||
|
}
|
||||||
|
Operation::UnregisterCommand { command } => {
|
||||||
|
self.plugins.runtimes[index].commands.remove(&command);
|
||||||
|
}
|
||||||
|
Operation::SetPanel { panel } => {
|
||||||
|
panel.validate()?;
|
||||||
|
let panels = &mut self.plugins.runtimes[index].panels;
|
||||||
|
ensure!(
|
||||||
|
panels.contains_key(&panel.id) || panels.len() < 8,
|
||||||
|
"panel limit reached"
|
||||||
|
);
|
||||||
|
panels.insert(panel.id.clone(), panel);
|
||||||
|
}
|
||||||
|
Operation::RemovePanel { panel } => {
|
||||||
|
self.plugins.runtimes[index].panels.remove(&panel);
|
||||||
|
}
|
||||||
|
Operation::ReportError { message } => {
|
||||||
|
self.error = Some(format!(
|
||||||
|
"Plugin {}: {message}",
|
||||||
|
self.plugins.runtimes[index].id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(serde_json::Value::Null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_plugins(&mut self, ctx: &egui::Context) {
|
||||||
|
use serde_json::json;
|
||||||
|
if self.plugins.runtimes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for index in 0..self.plugins.runtimes.len() {
|
||||||
|
// Bound work per frame even when a plugin continuously sends requests.
|
||||||
|
for count in 0..32 {
|
||||||
|
let Some(request) = self.plugins.runtimes[index].poll() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let response = match request {
|
||||||
|
Ok(request) => {
|
||||||
|
let response = match self.plugin_request(index, request.operation, ctx) {
|
||||||
|
Ok(result) => json!({"result": result}),
|
||||||
|
Err(error) => json!({"error": error.to_string()}),
|
||||||
|
};
|
||||||
|
request
|
||||||
|
.reply
|
||||||
|
.try_send(response)
|
||||||
|
.map_err(|e| anyhow::anyhow!(e.to_string()))
|
||||||
|
}
|
||||||
|
Err(error) => Err(anyhow::anyhow!(error)),
|
||||||
|
};
|
||||||
|
if let Err(e) = response {
|
||||||
|
let plugin = &mut self.plugins.runtimes[index];
|
||||||
|
self.error = Some(format!("Plugin {}: {e}", plugin.id));
|
||||||
|
plugin.stop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if count == 31 {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let state = self.plugin_state();
|
||||||
|
let serialized = serde_json::to_value(&state).unwrap();
|
||||||
|
for plugin in &mut self.plugins.runtimes {
|
||||||
|
if plugin.is_running() && plugin.last_state.as_ref() != Some(&serialized) {
|
||||||
|
if let Err(e) = plugin.send(Event::State {
|
||||||
|
state: state.clone(),
|
||||||
|
}) {
|
||||||
|
self.error = Some(format!("Plugin {}: {e}", plugin.id));
|
||||||
|
plugin.stop();
|
||||||
|
} else {
|
||||||
|
plugin.last_state = Some(serialized.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn keyboard(&mut self, ctx: &egui::Context) -> Result<()> {
|
fn keyboard(&mut self, ctx: &egui::Context) -> Result<()> {
|
||||||
let events = ctx.input(|i| i.events.clone());
|
let events = ctx.input(|i| i.events.clone());
|
||||||
let modifiers = ctx.input(|i| i.modifiers);
|
let modifiers = ctx.input(|i| i.modifiers);
|
||||||
@@ -388,15 +566,24 @@ impl eframe::App for App {
|
|||||||
for pane in self.panes.values_mut() {
|
for pane in self.panes.values_mut() {
|
||||||
pane.drain(ctx);
|
pane.drain(ctx);
|
||||||
}
|
}
|
||||||
|
self.poll_plugins(ctx);
|
||||||
|
if self.tabs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Native plugin text fields must receive typing without forwarding it to the PTY.
|
||||||
|
let editing_ui = ctx.memory(|m| m.focused().is_some_and(|id| id != egui::Id::new("keys")));
|
||||||
|
if !editing_ui {
|
||||||
grab_keys(ctx);
|
grab_keys(ctx);
|
||||||
if let Err(e) = self.keyboard(ctx) {
|
if let Err(e) = self.keyboard(ctx) {
|
||||||
self.error = Some(e.to_string());
|
self.error = Some(e.to_string());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if self.tabs.is_empty() {
|
if self.tabs.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
let mut picked = None;
|
let mut picked = None;
|
||||||
|
let mut plugin_command = None;
|
||||||
egui::TopBottomPanel::top("tabs").show(ctx, |ui| {
|
egui::TopBottomPanel::top("tabs").show(ctx, |ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
@@ -440,6 +627,20 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if self.plugins.runtimes.iter().any(|p| !p.commands.is_empty()) {
|
||||||
|
ui.menu_button("Plugins", |ui| {
|
||||||
|
for (index, plugin) in self.plugins.runtimes.iter().enumerate() {
|
||||||
|
ui.menu_button(&plugin.id, |ui| {
|
||||||
|
for command in plugin.commands.values() {
|
||||||
|
if ui.button(&command.title).clicked() {
|
||||||
|
plugin_command = Some((index, command.id.clone()));
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
ui.menu_button("Settings", |ui| {
|
ui.menu_button("Settings", |ui| {
|
||||||
if ui.button("Reload config").clicked() {
|
if ui.button("Reload config").clicked() {
|
||||||
action = Some(Action::ReloadConfig);
|
action = Some(Action::ReloadConfig);
|
||||||
@@ -538,9 +739,18 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let active = tab.active;
|
let active = tab.active;
|
||||||
|
let active_visible = tab.is_visible(active);
|
||||||
|
for error in self.plugins.show_panels(ctx) {
|
||||||
|
self.error = Some(error);
|
||||||
|
}
|
||||||
|
if let Some((index, command)) = plugin_command
|
||||||
|
&& let Err(e) = self.plugins.invoke(index, &command)
|
||||||
|
{
|
||||||
|
self.error = Some(e.to_string());
|
||||||
|
}
|
||||||
for (id, pane) in &mut self.panes {
|
for (id, pane) in &mut self.panes {
|
||||||
if let Err(e) =
|
if let Err(e) = pane
|
||||||
pane.focus(*id == active && tab.is_visible(*id) && ctx.input(|i| i.focused))
|
.focus(*id == active && active_visible && !editing_ui && ctx.input(|i| i.focused))
|
||||||
{
|
{
|
||||||
pane.error = Some(e.to_string());
|
pane.error = Some(e.to_string());
|
||||||
}
|
}
|
||||||
@@ -728,6 +938,117 @@ fn pane_title(id: PaneId, pane: &Pane) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn pump_plugins(app: &mut App, ctx: &egui::Context, done: impl Fn(&App) -> bool) {
|
||||||
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(8);
|
||||||
|
loop {
|
||||||
|
for pane in app.panes.values_mut() {
|
||||||
|
pane.drain(ctx);
|
||||||
|
}
|
||||||
|
app.poll_plugins(ctx);
|
||||||
|
assert!(app.error.is_none(), "plugin error: {:?}", app.error);
|
||||||
|
if done(app) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assert!(std::time::Instant::now() < deadline, "plugin timed out");
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedded_plugin_controls_panes_and_native_ui() {
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
let mut app = test_app(&ctx);
|
||||||
|
let path = std::env::temp_dir().join(format!("slerm-plugin-{}.js", std::process::id()));
|
||||||
|
std::fs::write(&path, r#"
|
||||||
|
__slerm.activate({
|
||||||
|
async activate(api) {
|
||||||
|
let rejected = false;
|
||||||
|
try { await api.focusPane({pane:999}); } catch (_) { rejected = true; }
|
||||||
|
if (!rejected) throw Error('missing validation');
|
||||||
|
await api.execute({action:'new_floating'});
|
||||||
|
const state = await api.getState();
|
||||||
|
if (state.tabs[0].panes.length !== 2) throw Error('wrong state');
|
||||||
|
await api.writeTerminal({pane:state.tabs[0].activePane, text:'echo SDK_INPUT\r'});
|
||||||
|
if (typeof await api.readTerminal({pane:1}) !== 'string') throw Error('wrong terminal result');
|
||||||
|
await api.setPanel({panel:{id:'test', title:'Tools', elements:[
|
||||||
|
{type:'button', id:'done', label:'Done'},
|
||||||
|
{type:'input', id:'name', label:'Name', value:'Slerm'}
|
||||||
|
]}});
|
||||||
|
await api.registerCommand({command:{id:'ready',title:'Ready'}});
|
||||||
|
},
|
||||||
|
async onEvent(event, api) {
|
||||||
|
if (event.event === 'command') await api.execute({action:'new_tab'});
|
||||||
|
if (event.event === 'ui') {
|
||||||
|
await api.removePanel({panel:event.data.panel});
|
||||||
|
await api.registerCommand({command:{id:'passed',title:'Passed'}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
"#).unwrap();
|
||||||
|
let (host, errors) = Host::start(
|
||||||
|
&[config::Plugin {
|
||||||
|
id: "test".into(),
|
||||||
|
path: path.clone(),
|
||||||
|
}],
|
||||||
|
std::path::Path::new("."),
|
||||||
|
&ctx,
|
||||||
|
);
|
||||||
|
std::fs::remove_file(path).unwrap();
|
||||||
|
assert!(errors.is_empty(), "{errors:?}");
|
||||||
|
app.plugins = host;
|
||||||
|
pump_plugins(&mut app, &ctx, |app| {
|
||||||
|
app.plugins.runtimes[0].commands.contains_key("ready")
|
||||||
|
});
|
||||||
|
assert_eq!(app.tabs[0].floats.len(), 1);
|
||||||
|
assert_eq!(app.plugins.runtimes[0].panels.len(), 1);
|
||||||
|
let _ = ctx.run(egui::RawInput::default(), |ctx| {
|
||||||
|
assert!(app.plugins.show_panels(ctx).is_empty());
|
||||||
|
});
|
||||||
|
app.plugins.invoke(0, "ready").unwrap();
|
||||||
|
pump_plugins(&mut app, &ctx, |app| app.tabs.len() == 2);
|
||||||
|
app.plugins.runtimes[0]
|
||||||
|
.send(Event::Ui {
|
||||||
|
data: crate::plugin_api::UIEvent::Click {
|
||||||
|
panel: "test".into(),
|
||||||
|
control: "done".into(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
pump_plugins(&mut app, &ctx, |app| {
|
||||||
|
app.plugins.runtimes[0].commands.contains_key("passed")
|
||||||
|
});
|
||||||
|
assert!(app.plugins.runtimes[0].panels.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "build the example first: npm run example --prefix sdk"]
|
||||||
|
fn bundled_typescript_example_runs_in_boa() {
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
let mut app = test_app(&ctx);
|
||||||
|
let (host, errors) = Host::start(
|
||||||
|
&[config::Plugin {
|
||||||
|
id: "workspace".into(),
|
||||||
|
path: "sdk/dist/workspace.js".into(),
|
||||||
|
}],
|
||||||
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")),
|
||||||
|
&ctx,
|
||||||
|
);
|
||||||
|
assert!(errors.is_empty(), "{errors:?}");
|
||||||
|
app.plugins = host;
|
||||||
|
pump_plugins(&mut app, &ctx, |app| {
|
||||||
|
app.plugins.runtimes[0].panels.contains_key("workspace")
|
||||||
|
});
|
||||||
|
app.plugins.runtimes[0]
|
||||||
|
.send(Event::Ui {
|
||||||
|
data: crate::plugin_api::UIEvent::Click {
|
||||||
|
panel: "workspace".into(),
|
||||||
|
control: "scratch".into(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
pump_plugins(&mut app, &ctx, |app| app.tabs[0].floats.len() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
fn test_app(ctx: &egui::Context) -> App {
|
fn test_app(ctx: &egui::Context) -> App {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
shell: vec!["/bin/sh".into()],
|
shell: vec!["/bin/sh".into()],
|
||||||
@@ -745,6 +1066,7 @@ mod tests {
|
|||||||
next_id: 2,
|
next_id: 2,
|
||||||
error: None,
|
error: None,
|
||||||
suppressed: HashSet::new(),
|
suppressed: HashSet::new(),
|
||||||
|
plugins: Host::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -34,6 +34,15 @@ pub struct Config {
|
|||||||
pub kitty: Kitty,
|
pub kitty: Kitty,
|
||||||
pub multiplexer: Multiplexer,
|
pub multiplexer: Multiplexer,
|
||||||
pub keys: BTreeMap<String, Action>,
|
pub keys: BTreeMap<String, Action>,
|
||||||
|
pub plugins: Vec<Plugin>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Plugin {
|
||||||
|
pub id: String,
|
||||||
|
/// JavaScript bundle path, relative to the config file.
|
||||||
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
@@ -55,7 +64,7 @@ pub struct Multiplexer {
|
|||||||
pub floating_max_height: f32,
|
pub floating_max_height: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, ts_rs::TS)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
SplitRight,
|
SplitRight,
|
||||||
@@ -127,6 +136,7 @@ impl Default for Config {
|
|||||||
colors: BTreeMap::new(),
|
colors: BTreeMap::new(),
|
||||||
kitty: Kitty::default(),
|
kitty: Kitty::default(),
|
||||||
multiplexer: Multiplexer::default(),
|
multiplexer: Multiplexer::default(),
|
||||||
|
plugins: Vec::new(),
|
||||||
keys: [
|
keys: [
|
||||||
("Alt+F", Action::ToggleFloats),
|
("Alt+F", Action::ToggleFloats),
|
||||||
("Alt+M", Action::NewPane),
|
("Alt+M", Action::NewPane),
|
||||||
@@ -174,6 +184,18 @@ impl Config {
|
|||||||
|
|
||||||
pub fn parse(text: &str) -> Result<Self> {
|
pub fn parse(text: &str) -> Result<Self> {
|
||||||
let config: Self = toml::from_str(text)?;
|
let config: Self = toml::from_str(text)?;
|
||||||
|
let mut plugin_ids = std::collections::HashSet::new();
|
||||||
|
for plugin in &config.plugins {
|
||||||
|
ensure!(
|
||||||
|
crate::plugins::valid_id(&plugin.id) && plugin_ids.insert(&plugin.id),
|
||||||
|
"plugin ids must be unique and contain only letters, digits, '.', '-' or '_'"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
!plugin.path.as_os_str().is_empty(),
|
||||||
|
"plugin {} needs a bundle path",
|
||||||
|
plugin.id
|
||||||
|
);
|
||||||
|
}
|
||||||
ensure!(
|
ensure!(
|
||||||
config.font_path.is_none() || config.font_family.is_none(),
|
config.font_path.is_none() || config.font_family.is_none(),
|
||||||
"set either font_family or font_path, not both"
|
"set either font_family or font_path, not both"
|
||||||
|
|||||||
+10
-1
@@ -4,6 +4,8 @@ mod fonts;
|
|||||||
mod input;
|
mod input;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod pane;
|
mod pane;
|
||||||
|
mod plugin_api;
|
||||||
|
mod plugins;
|
||||||
mod pty;
|
mod pty;
|
||||||
mod render;
|
mod render;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -29,13 +31,20 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
"--generate-plugin-sdk" => {
|
||||||
|
let target =
|
||||||
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("sdk/src/generated.ts");
|
||||||
|
std::fs::write(&target, plugin_api::typescript())?;
|
||||||
|
println!("Generated {}", target.display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
"--version" => {
|
"--version" => {
|
||||||
println!("slerm {}", env!("CARGO_PKG_VERSION"));
|
println!("slerm {}", env!("CARGO_PKG_VERSION"));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
println!(
|
println!(
|
||||||
"slerm [--config PATH] [--check-config | --init-config | --list-themes]\n\nDefault config: {}",
|
"slerm [--config PATH] [--check-config | --init-config | --list-themes | --generate-plugin-sdk]\n\nDefault config: {}",
|
||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
//! The source of truth for the plugin API and its generated TypeScript bindings.
|
||||||
|
use crate::config::Action;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
pub const API_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
// Declare each method, its parameters and its result once. The enum drives Rust
|
||||||
|
// dispatch; the generated result map drives the TypeScript client's signatures.
|
||||||
|
macro_rules! plugin_api {
|
||||||
|
($($variant:ident => $method:ident { $($(#[$attr:meta])* $arg:ident: $ty:ty),* } -> $result:ty;)+) => {
|
||||||
|
#[derive(Debug, Deserialize, TS)]
|
||||||
|
#[serde(tag = "method", rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
pub enum Operation {
|
||||||
|
$($variant { $($(#[$attr])* $arg: $ty),* },)+
|
||||||
|
}
|
||||||
|
#[derive(TS)]
|
||||||
|
#[allow(dead_code, non_snake_case)]
|
||||||
|
pub struct Results { $(pub $method: $result,)+ }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
plugin_api! {
|
||||||
|
GetState => getState {} -> State;
|
||||||
|
Execute => execute { action: Action } -> ();
|
||||||
|
FocusPane => focusPane { #[ts(type = "number")] pane: u64 } -> ();
|
||||||
|
FocusTab => focusTab { #[ts(type = "number")] tab: u64 } -> ();
|
||||||
|
ReadTerminal => readTerminal { #[ts(type = "number")] pane: u64 } -> String;
|
||||||
|
WriteTerminal => writeTerminal { #[ts(type = "number")] pane: u64, text: String } -> ();
|
||||||
|
RegisterCommand => registerCommand { command: PluginCommand } -> ();
|
||||||
|
UnregisterCommand => unregisterCommand { command: String } -> ();
|
||||||
|
SetPanel => setPanel { panel: Panel } -> ();
|
||||||
|
RemovePanel => removePanel { panel: String } -> ();
|
||||||
|
ReportError => reportError { message: String } -> ();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct State {
|
||||||
|
#[ts(type = "number | null")]
|
||||||
|
pub active_tab: Option<u64>,
|
||||||
|
pub tabs: Vec<TabState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TabState {
|
||||||
|
#[ts(type = "number")]
|
||||||
|
pub id: u64,
|
||||||
|
#[ts(type = "number")]
|
||||||
|
pub active_pane: u64,
|
||||||
|
pub floats_visible: bool,
|
||||||
|
pub zoomed: bool,
|
||||||
|
pub panes: Vec<PaneState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, TS)]
|
||||||
|
pub struct PaneState {
|
||||||
|
#[ts(type = "number")]
|
||||||
|
pub id: u64,
|
||||||
|
pub title: String,
|
||||||
|
pub floating: bool,
|
||||||
|
pub exited: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, TS)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct PluginCommand {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, TS)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Panel {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub elements: Vec<Element>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, TS)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
pub enum Element {
|
||||||
|
Text {
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Button {
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
},
|
||||||
|
Input {
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
Separator,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, TS)]
|
||||||
|
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||||
|
pub enum UIEvent {
|
||||||
|
Click {
|
||||||
|
panel: String,
|
||||||
|
control: String,
|
||||||
|
},
|
||||||
|
Change {
|
||||||
|
panel: String,
|
||||||
|
control: String,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, TS)]
|
||||||
|
#[serde(tag = "event", rename_all = "camelCase")]
|
||||||
|
pub enum Event {
|
||||||
|
Command { command: String },
|
||||||
|
State { state: State },
|
||||||
|
Ui { data: UIEvent },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn typescript() -> String {
|
||||||
|
let mut output =
|
||||||
|
String::from("// Generated by cargo run -- --generate-plugin-sdk. Do not edit.\n\n");
|
||||||
|
output.push_str(&format!(
|
||||||
|
"export const apiVersion = {API_VERSION} as const;\n\n"
|
||||||
|
));
|
||||||
|
let config = ts_rs::Config::default();
|
||||||
|
for declaration in [
|
||||||
|
Action::decl(&config),
|
||||||
|
Operation::decl(&config),
|
||||||
|
Results::decl(&config),
|
||||||
|
State::decl(&config),
|
||||||
|
TabState::decl(&config),
|
||||||
|
PaneState::decl(&config),
|
||||||
|
PluginCommand::decl(&config),
|
||||||
|
Panel::decl(&config),
|
||||||
|
Element::decl(&config),
|
||||||
|
UIEvent::decl(&config),
|
||||||
|
Event::decl(&config),
|
||||||
|
] {
|
||||||
|
output.push_str("export ");
|
||||||
|
output.push_str(&declaration);
|
||||||
|
output.push_str("\n\n");
|
||||||
|
}
|
||||||
|
output.push_str(r#"type Params<M extends Operation["method"]> = Omit<Extract<Operation, { method: M }>, "method">;
|
||||||
|
export type Slerm = {
|
||||||
|
[M in Operation["method"]]: (...args: keyof Params<M> extends never ? [] : [params: Params<M>]) => Promise<Results[M] extends null ? void : Results[M]>;
|
||||||
|
};
|
||||||
|
"#);
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn generated_sdk_matches_rust_api() {
|
||||||
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("sdk/src/generated.ts");
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(path).unwrap(),
|
||||||
|
super::typescript(),
|
||||||
|
"Regenerate bindings with cargo run -- --generate-plugin-sdk"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+457
@@ -0,0 +1,457 @@
|
|||||||
|
//! Embedded Boa runtimes, one worker thread per plugin. UI mutations stay on the UI thread.
|
||||||
|
use crate::{
|
||||||
|
config::Plugin,
|
||||||
|
plugin_api::{API_VERSION, Element, Event, Operation, Panel, PluginCommand, UIEvent},
|
||||||
|
};
|
||||||
|
use anyhow::{Context as _, Result, ensure};
|
||||||
|
use boa_engine::{
|
||||||
|
Context, JsNativeError, JsResult, JsValue, NativeFunction, Source,
|
||||||
|
job::{Job, JobExecutor},
|
||||||
|
js_string,
|
||||||
|
property::Attribute,
|
||||||
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::{BTreeMap, VecDeque},
|
||||||
|
path::Path,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
mpsc::{self, Receiver, SyncSender, TryRecvError},
|
||||||
|
},
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_MESSAGE: usize = 1024 * 1024;
|
||||||
|
|
||||||
|
pub fn valid_id(id: &str) -> bool {
|
||||||
|
!id.is_empty()
|
||||||
|
&& id.len() <= 80
|
||||||
|
&& id
|
||||||
|
.bytes()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || b"._-".contains(&c))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Pending {
|
||||||
|
pub operation: Operation,
|
||||||
|
pub reply: SyncSender<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Bridge {
|
||||||
|
requests: SyncSender<Result<Pending, String>>,
|
||||||
|
repaint: egui::Context,
|
||||||
|
stopped: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_call(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||||
|
let text = args
|
||||||
|
.first()
|
||||||
|
.unwrap_or(&JsValue::undefined())
|
||||||
|
.to_string(ctx)?
|
||||||
|
.to_std_string_escaped();
|
||||||
|
if text.len() > MAX_MESSAGE {
|
||||||
|
return Err(JsNativeError::range()
|
||||||
|
.with_message("plugin request exceeds 1 MiB")
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
let operation = serde_json::from_str(&text)
|
||||||
|
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
|
||||||
|
let bridge = ctx.get_data::<Bridge>().unwrap();
|
||||||
|
let (reply, response) = mpsc::sync_channel(1);
|
||||||
|
bridge
|
||||||
|
.requests
|
||||||
|
.try_send(Ok(Pending { operation, reply }))
|
||||||
|
.map_err(|_| JsNativeError::error().with_message("plugin host is unavailable"))?;
|
||||||
|
bridge.repaint.request_repaint();
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(10);
|
||||||
|
loop {
|
||||||
|
if bridge.stopped.load(Ordering::Relaxed) || Instant::now() >= deadline {
|
||||||
|
return Err(JsNativeError::error()
|
||||||
|
.with_message("plugin stopped or host request timed out")
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
match response.recv_timeout(Duration::from_millis(50)) {
|
||||||
|
Ok(value) => return Ok(js_string!(serde_json::to_string(&value).unwrap()).into()),
|
||||||
|
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||||
|
Err(_) => {
|
||||||
|
return Err(JsNativeError::error()
|
||||||
|
.with_message("plugin host disconnected")
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_error(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||||
|
let text = args
|
||||||
|
.first()
|
||||||
|
.unwrap_or(&JsValue::undefined())
|
||||||
|
.to_string(ctx)?
|
||||||
|
.to_std_string_escaped();
|
||||||
|
let bridge = ctx.get_data::<Bridge>().unwrap();
|
||||||
|
let _ = bridge.requests.try_send(Err(text));
|
||||||
|
bridge.repaint.request_repaint();
|
||||||
|
Ok(JsValue::undefined())
|
||||||
|
}
|
||||||
|
|
||||||
|
// This runtime exposes promises, but no timers or native async jobs. Bound each
|
||||||
|
// promise checkpoint as well as JS loops, so runaway callbacks can be stopped.
|
||||||
|
struct Jobs {
|
||||||
|
queue: RefCell<VecDeque<Job>>,
|
||||||
|
stopped: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
impl JobExecutor for Jobs {
|
||||||
|
fn enqueue_job(self: Rc<Self>, job: Job, _: &mut Context) {
|
||||||
|
self.queue.borrow_mut().push_back(job);
|
||||||
|
}
|
||||||
|
fn run_jobs(self: Rc<Self>, ctx: &mut Context) -> JsResult<()> {
|
||||||
|
for _ in 0..10_000 {
|
||||||
|
if self.stopped.load(Ordering::Relaxed) {
|
||||||
|
return Err(JsNativeError::error().with_message("plugin stopped").into());
|
||||||
|
}
|
||||||
|
let Some(job) = self.queue.borrow_mut().pop_front() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
match job {
|
||||||
|
Job::PromiseJob(job) => {
|
||||||
|
job.call(ctx)?;
|
||||||
|
}
|
||||||
|
Job::GenericJob(job) => {
|
||||||
|
job.call(ctx)?;
|
||||||
|
}
|
||||||
|
Job::FinalizationRegistryCleanupJob(_) => {}
|
||||||
|
_ => {
|
||||||
|
return Err(JsNativeError::error()
|
||||||
|
.with_message("unsupported plugin job")
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(JsNativeError::error()
|
||||||
|
.with_message("plugin promise job limit exceeded")
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(source: String, events: Receiver<Event>, bridge: Bridge) -> Result<()> {
|
||||||
|
let jobs = Rc::new(Jobs {
|
||||||
|
queue: RefCell::new(VecDeque::new()),
|
||||||
|
stopped: bridge.stopped.clone(),
|
||||||
|
});
|
||||||
|
let mut ctx = Context::builder()
|
||||||
|
.job_executor(jobs)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
|
ctx.runtime_limits_mut().set_loop_iteration_limit(1_000_000);
|
||||||
|
ctx.runtime_limits_mut().set_recursion_limit(128);
|
||||||
|
ctx.insert_data(bridge);
|
||||||
|
let execute = |ctx: &mut Context, source: &str| -> Result<()> {
|
||||||
|
ctx.eval(Source::from_bytes(source))
|
||||||
|
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
|
ctx.run_jobs().map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
ctx.register_global_builtin_callable(
|
||||||
|
js_string!("__slerm_call"),
|
||||||
|
1,
|
||||||
|
NativeFunction::from_fn_ptr(host_call),
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
|
ctx.register_global_builtin_callable(
|
||||||
|
js_string!("__slerm_error"),
|
||||||
|
1,
|
||||||
|
NativeFunction::from_fn_ptr(host_error),
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
|
ctx.register_global_property(
|
||||||
|
js_string!("__slerm_version"),
|
||||||
|
API_VERSION,
|
||||||
|
Attribute::READONLY,
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
|
execute(&mut ctx, include_str!("../sdk/runtime.js"))?;
|
||||||
|
execute(&mut ctx, &source)?;
|
||||||
|
while let Ok(event) = events.recv() {
|
||||||
|
if ctx
|
||||||
|
.get_data::<Bridge>()
|
||||||
|
.unwrap()
|
||||||
|
.stopped
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
execute(
|
||||||
|
&mut ctx,
|
||||||
|
&format!("__slerm_dispatch({});", serde_json::to_string(&event)?),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Runtime {
|
||||||
|
pub id: String,
|
||||||
|
pub commands: BTreeMap<String, PluginCommand>,
|
||||||
|
pub panels: BTreeMap<String, Panel>,
|
||||||
|
pub last_state: Option<Value>,
|
||||||
|
outgoing: Option<SyncSender<Event>>,
|
||||||
|
incoming: Option<Receiver<Result<Pending, String>>>,
|
||||||
|
stopped: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Runtime {
|
||||||
|
pub fn spawn(config: &Plugin, config_dir: &Path, ctx: &egui::Context) -> Result<Self> {
|
||||||
|
let path = config_dir.join(&config.path);
|
||||||
|
let source = std::fs::read_to_string(&path)
|
||||||
|
.with_context(|| format!("Cannot read plugin {} at {}", config.id, path.display()))?;
|
||||||
|
ensure!(
|
||||||
|
source.len() <= 4 * MAX_MESSAGE,
|
||||||
|
"plugin bundle exceeds 4 MiB"
|
||||||
|
);
|
||||||
|
let (outgoing, events) = mpsc::sync_channel(64);
|
||||||
|
let (requests, incoming) = mpsc::sync_channel(64);
|
||||||
|
let stopped = Arc::new(AtomicBool::new(false));
|
||||||
|
let bridge = Bridge {
|
||||||
|
requests: requests.clone(),
|
||||||
|
repaint: ctx.clone(),
|
||||||
|
stopped: stopped.clone(),
|
||||||
|
};
|
||||||
|
let repaint = ctx.clone();
|
||||||
|
thread::Builder::new()
|
||||||
|
.name(format!("plugin-{}", config.id))
|
||||||
|
.spawn(move || {
|
||||||
|
if let Err(error) = run(source, events, bridge) {
|
||||||
|
let _ = requests.try_send(Err(error.to_string()));
|
||||||
|
repaint.request_repaint();
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(Self {
|
||||||
|
id: config.id.clone(),
|
||||||
|
commands: BTreeMap::new(),
|
||||||
|
panels: BTreeMap::new(),
|
||||||
|
last_state: None,
|
||||||
|
outgoing: Some(outgoing),
|
||||||
|
incoming: Some(incoming),
|
||||||
|
stopped,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll(&mut self) -> Option<Result<Pending, String>> {
|
||||||
|
match self.incoming.as_ref()?.try_recv() {
|
||||||
|
Ok(request) => Some(request),
|
||||||
|
Err(TryRecvError::Empty) => None,
|
||||||
|
Err(TryRecvError::Disconnected) => Some(Err("plugin runtime stopped".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, event: Event) -> Result<()> {
|
||||||
|
let outgoing = self
|
||||||
|
.outgoing
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("plugin is stopped"))?;
|
||||||
|
ensure!(
|
||||||
|
serde_json::to_vec(&event)?.len() < MAX_MESSAGE,
|
||||||
|
"plugin event exceeds 1 MiB"
|
||||||
|
);
|
||||||
|
outgoing
|
||||||
|
.try_send(event)
|
||||||
|
.context("plugin event queue is full or disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_running(&self) -> bool {
|
||||||
|
!self.stopped.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
self.stopped.store(true, Ordering::Relaxed);
|
||||||
|
self.commands.clear();
|
||||||
|
self.panels.clear();
|
||||||
|
self.outgoing.take();
|
||||||
|
self.incoming.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Runtime {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Panel {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
ensure!(valid_id(&self.id), "invalid panel id");
|
||||||
|
ensure!(
|
||||||
|
!self.title.trim().is_empty() && self.title.len() <= 200,
|
||||||
|
"invalid panel title"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
self.elements.len() <= 128,
|
||||||
|
"a panel supports at most 128 elements"
|
||||||
|
);
|
||||||
|
let mut ids = std::collections::HashSet::new();
|
||||||
|
for element in &self.elements {
|
||||||
|
let id = match element {
|
||||||
|
Element::Button { id, .. } | Element::Input { id, .. } => Some(id),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(id) = id {
|
||||||
|
ensure!(
|
||||||
|
valid_id(id) && ids.insert(id),
|
||||||
|
"invalid or duplicate control id"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Host {
|
||||||
|
pub runtimes: Vec<Runtime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Host {
|
||||||
|
pub fn start(
|
||||||
|
configs: &[Plugin],
|
||||||
|
config_dir: &Path,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
) -> (Self, Vec<String>) {
|
||||||
|
let mut host = Self::default();
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for config in configs {
|
||||||
|
match Runtime::spawn(config, config_dir, ctx) {
|
||||||
|
Ok(process) => host.runtimes.push(process),
|
||||||
|
Err(e) => errors.push(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(host, errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invoke(&self, index: usize, command: &str) -> Result<()> {
|
||||||
|
let plugin = &self.runtimes[index];
|
||||||
|
ensure!(
|
||||||
|
plugin.commands.contains_key(command),
|
||||||
|
"unknown plugin command"
|
||||||
|
);
|
||||||
|
plugin.send(Event::Command {
|
||||||
|
command: command.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_panels(&mut self, ctx: &egui::Context) -> Vec<String> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for plugin in &mut self.runtimes {
|
||||||
|
let mut events = Vec::new();
|
||||||
|
for panel in plugin.panels.values_mut() {
|
||||||
|
egui::Window::new(&panel.title)
|
||||||
|
.id(egui::Id::new(("plugin panel", &plugin.id, &panel.id)))
|
||||||
|
.order(egui::Order::Foreground)
|
||||||
|
.default_width(280.0)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.max_height(500.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for element in &mut panel.elements {
|
||||||
|
match element {
|
||||||
|
Element::Text { text } => {
|
||||||
|
ui.label(text.as_str());
|
||||||
|
}
|
||||||
|
Element::Separator => {
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
Element::Button { id, label } => {
|
||||||
|
if ui
|
||||||
|
.push_id(id.as_str(), |ui| {
|
||||||
|
ui.button(label.as_str())
|
||||||
|
})
|
||||||
|
.inner
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
events.push(Event::Ui {
|
||||||
|
data: UIEvent::Click {
|
||||||
|
panel: panel.id.clone(),
|
||||||
|
control: id.clone(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Element::Input { id, label, value } => {
|
||||||
|
ui.label(label.as_str());
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
egui::TextEdit::singleline(value)
|
||||||
|
.id_source(id.as_str()),
|
||||||
|
)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
events.push(Event::Ui {
|
||||||
|
data: UIEvent::Change {
|
||||||
|
panel: panel.id.clone(),
|
||||||
|
control: id.clone(),
|
||||||
|
value: value.clone(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for event in events {
|
||||||
|
if let Err(e) = plugin.send(event) {
|
||||||
|
errors.push(format!("{}: {e}", plugin.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_configuration_and_ui() {
|
||||||
|
assert!(crate::config::Config::parse("[[plugins]]\nid='x'\npath=''\n").is_err());
|
||||||
|
assert!(
|
||||||
|
crate::config::Config::parse(
|
||||||
|
"[[plugins]]\nid='x'\npath='a.js'\n[[plugins]]\nid='x'\npath='b.js'\n"
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(serde_json::from_str::<Operation>(r#"{"method":"unknown"}"#).is_err());
|
||||||
|
let panel: Panel =
|
||||||
|
serde_json::from_value(json!({"id":"panel", "title":"Panel", "elements":[
|
||||||
|
{"type":"button", "id":"same", "label":"One"},
|
||||||
|
{"type":"input", "id":"same", "label":"Two", "value":""}
|
||||||
|
]}))
|
||||||
|
.unwrap();
|
||||||
|
assert!(panel.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boa_reports_script_errors_and_limits_runaway_loops_and_promises() {
|
||||||
|
for source in [
|
||||||
|
"throw Error('broken plugin')",
|
||||||
|
"while (true) {}",
|
||||||
|
"function again() { Promise.resolve().then(again); } again();",
|
||||||
|
"__slerm.activate({}, 999)",
|
||||||
|
"process.exit(0)",
|
||||||
|
] {
|
||||||
|
let (_, events) = mpsc::sync_channel(1);
|
||||||
|
let (requests, _) = mpsc::sync_channel(1);
|
||||||
|
let bridge = Bridge {
|
||||||
|
requests,
|
||||||
|
repaint: egui::Context::default(),
|
||||||
|
stopped: Arc::new(AtomicBool::new(false)),
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
run(source.into(), events, bridge).is_err(),
|
||||||
|
"source should fail: {source}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user