notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit f9def5143208e65d5f14e8adf44c6b4f4998c15d
parent ff7601873acf80631a3e162fe13c3b6874e97cc2
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 22 Feb 2026 12:06:28 -0800

wasm: add notedeck_wasm crate with safe command buffer runtime

Add a WASM mini-app runtime for notedeck. WASM modules export
nd_update() and call host functions (nd_label, nd_heading, nd_button,
nd_add_space) to build UI. Host functions push commands to a buffer
which is rendered after nd_update() returns — zero unsafe code.

Buttons return click state from the previous frame (imperceptible
one-frame delay at 60fps). Button identity is by text content.

Includes:
- notedeck_wasm crate with wasmer 7 runtime
- Command buffer architecture (no raw pointers, no unsafe)
- App metadata via nd_app_name_ptr/len exports
- 11 tests using inline WAT modules
- build_hello_wasm example (667 byte hello world)
- Chrome integration behind wasm_apps feature flag
- Auto-loading from <data_dir>/cache/wasm_apps/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
MCargo.lock | 994++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
MCargo.toml | 2++
Mcrates/notedeck_chrome/Cargo.toml | 2++
Mcrates/notedeck_chrome/src/app.rs | 4++--
Mcrates/notedeck_chrome/src/chrome.rs | 117++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Acrates/notedeck_wasm/Cargo.toml | 16++++++++++++++++
Acrates/notedeck_wasm/api/notedeck_api.h | 27+++++++++++++++++++++++++++
Acrates/notedeck_wasm/examples/build_hello_wasm.rs | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/notedeck_wasm/examples/hello.c | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/notedeck_wasm/src/commands.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
Acrates/notedeck_wasm/src/host_fns.rs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/notedeck_wasm/src/lib.rs | 365+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 1742 insertions(+), 105 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -34,7 +34,7 @@ version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli", + "gimli 0.31.1", ] [[package]] @@ -55,7 +55,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array", ] @@ -159,7 +159,7 @@ dependencies = [ "jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)", "keyring", "ndk-context", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", ] @@ -192,9 +192,9 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -386,7 +386,7 @@ dependencies = [ "secrecy", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -610,7 +610,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.36.7", "rustc-demangle", "windows-targets 0.52.6", ] @@ -690,7 +690,7 @@ dependencies = [ "rustc-hash 1.1.0", "shlex", "syn 2.0.104", - "which", + "which 4.4.2", ] [[package]] @@ -852,6 +852,15 @@ dependencies = [ ] [[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + +[[package]] name = "block-padding" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -938,6 +947,32 @@ name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] name = "bytemuck" @@ -1014,10 +1049,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.27" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1045,7 +1081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon", + "target-lexicon 0.12.16", ] [[package]] @@ -1120,7 +1156,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", "zeroize", ] @@ -1152,7 +1188,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tracing", "typed-builder", @@ -1184,7 +1220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "termcolor", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1213,6 +1249,21 @@ dependencies = [ ] [[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1272,6 +1323,19 @@ dependencies = [ ] [[package]] +name = "corosensei" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2b4c7e3e97730e6b0b8c5ff5ca82c663d1a645e4f630f4fa4c24e80626787e" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", +] + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1281,6 +1345,125 @@ dependencies = [ ] [[package]] +name = "cranelift-assembler-x64" +version = "0.128.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0377b13bf002a0774fcccac4f1102a10f04893d24060cf4b7350c87e4cbb647c" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.128.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa027979140d023b25bf7509fb7ede3a54c3d3871fb5ead4673c4b633f671a2" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea7351476d0eb196e89150e7a6235ecd37c97848243faea7746c29676abeeac" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.128.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db53764b5dad233b37b8f5dc54d3caa9900c54579195e00f17ea21f03f71aaa7" + +[[package]] +name = "cranelift-codegen" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9e80ceb5153bb9dd0d048e685ec4df6fa20ce92d4ffffcb5d691623e1d8693" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.32.3", + "hashbrown 0.15.4", + "log", + "regalloc2", + "rustc-hash 2.1.1", + "serde", + "smallvec", + "target-lexicon 0.13.5", + "wasmtime-internal-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.128.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fcf1e3e6757834bd2584f4cbff023fcc198e9279dcb5d684b4bb27a9b19f54" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.128.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "205dcb9e6ccf9d368b7466be675ff6ee54a63e36da6fe20e72d45169cf6fd254" + +[[package]] +name = "cranelift-control" +version = "0.128.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "108eca9fcfe86026054f931eceaf57b722c1b97464bf8265323a9b5877238817" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e57c6f29da407f6ee9956197d011aedf4fd39bd03781ab5b44b85d45a448a27" +dependencies = [ + "cranelift-bitset", +] + +[[package]] +name = "cranelift-frontend" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add3991ccfeb20022443bae60b8adc56081f27caab0213b0ff26288954e44fe5" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon 0.13.5", +] + +[[package]] +name = "cranelift-isle" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc02707039d43c0e132526f1d3ac319b45468331b823a1749625825010f644e4" + +[[package]] +name = "cranelift-srcgen" +version = "0.128.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e378a54e7168a689486d67ee1f818b7e5356e54ae51a1d7a53f4f13f7f8b7a" + +[[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1363,6 +1546,15 @@ dependencies = [ ] [[package]] +name = "crypto-common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" +dependencies = [ + "hybrid-array", +] + +[[package]] name = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1374,8 +1566,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1393,12 +1595,36 @@ dependencies = [ ] [[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", "syn 2.0.104", ] @@ -1493,7 +1719,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.104", @@ -1510,6 +1736,29 @@ dependencies = [ ] [[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.104", + "unicode-xid", +] + +[[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1521,12 +1770,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", "subtle", ] [[package]] +name = "digest" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6" +dependencies = [ + "block-buffer 0.11.0", + "const-oid", + "crypto-common 0.2.0", +] + +[[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1863,8 +2123,8 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2", - "thiserror 2.0.12", + "sha2 0.10.9", + "thiserror 2.0.18", "tokenator", "tokio", "tracing", @@ -1872,6 +2132,26 @@ dependencies = [ ] [[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] name = "enum-map" version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1925,6 +2205,27 @@ dependencies = [ ] [[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1934,6 +2235,12 @@ dependencies = [ ] [[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] name = "epaint" version = "0.31.1" source = "git+https://github.com/damus-io/egui?rev=e05638c40ef734312b3b3e36397d389d0a78b10b#e05638c40ef734312b3b3e36397d389d0a78b10b" @@ -2061,6 +2368,12 @@ dependencies = [ ] [[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2091,6 +2404,12 @@ dependencies = [ ] [[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] name = "flatbuffers" version = "23.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2179,7 +2498,7 @@ dependencies = [ "fluent-fallback", "futures", "rustc-hash 2.1.1", - "thiserror 2.0.12", + "thiserror 2.0.18", "unic-langid", ] @@ -2190,7 +2509,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -2476,6 +2795,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap 2.9.0", + "stable_deref_trait", +] + +[[package]] name = "gl_generator" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2825,7 +3155,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2890,6 +3220,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3b1f728c459d27b12448862017b96ad4767b1ec2ec5e6434e99f1577f085b8" [[package]] +name = "hybrid-array" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +dependencies = [ + "typenum", +] + +[[package]] name = "hyper" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3304,7 +3643,7 @@ dependencies = [ "log", "once_cell", "static_assertions", - "thiserror 2.0.12", + "thiserror 2.0.18", "walkdir", ] @@ -3428,6 +3767,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] name = "lebe" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3435,9 +3786,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libdbus-sys" @@ -3494,6 +3845,12 @@ dependencies = [ ] [[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3522,6 +3879,12 @@ dependencies = [ ] [[package]] +name = "libunwind" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" + +[[package]] name = "libz-rs-sys" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3613,9 +3976,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "log-once" @@ -3661,6 +4024,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" [[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror 2.0.18", + "zerocopy", + "zerocopy-derive", +] + +[[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3706,9 +4095,18 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -3810,6 +4208,32 @@ dependencies = [ ] [[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] name = "naga" version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3827,7 +4251,7 @@ dependencies = [ "spirv", "strum", "termcolor", - "thiserror 2.0.12", + "thiserror 2.0.18", "unicode-xid", ] @@ -4032,7 +4456,7 @@ dependencies = [ "futures", "libc", "libsodium-sys-stable", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -4089,11 +4513,11 @@ dependencies = [ "secp256k1 0.30.0", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "strum", "strum_macros", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokenator", "tokio", "tracing", @@ -4124,6 +4548,7 @@ dependencies = [ "notedeck_nostrverse", "notedeck_notebook", "notedeck_ui", + "notedeck_wasm", "objc2 0.5.2", "profiling", "puffin", @@ -4200,11 +4625,11 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2", + "sha2 0.10.9", "strum", "strum_macros", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokenator", "tokio", "tracing", @@ -4260,7 +4685,7 @@ dependencies = [ "rfd", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "similar", "tempfile", "tokio", @@ -4337,6 +4762,18 @@ dependencies = [ ] [[package]] +name = "notedeck_wasm" +version = "0.7.1" +dependencies = [ + "dirs", + "egui", + "notedeck", + "tracing", + "wasmer", + "wat", +] + +[[package]] name = "ntapi" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4784,6 +5221,20 @@ dependencies = [ ] [[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.16.1", + "indexmap 2.9.0", + "memchr", + "ruzstd", +] + +[[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4985,7 +5436,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", + "digest 0.10.7", "hmac", ] @@ -5216,6 +5667,28 @@ dependencies = [ ] [[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5256,6 +5729,26 @@ name = "protoverse" version = "0.1.0" [[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] name = "puffin" version = "0.19.1" source = "git+https://github.com/jb55/puffin?rev=c6a6242adaf90b6292c0f462d2acd34d96d224d2#c6a6242adaf90b6292c0f462d2acd34d96d224d2" @@ -5326,7 +5819,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2 0.5.10", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tracing", "web-time 1.1.0", @@ -5347,7 +5840,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time 1.1.0", @@ -5383,6 +5876,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + +[[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5448,6 +5950,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" [[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] name = "rav1e" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5633,6 +6141,20 @@ dependencies = [ ] [[package]] +name = "regalloc2" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.4", + "log", + "rustc-hash 2.1.1", + "smallvec", +] + +[[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5677,6 +6199,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2 0.4.3", + "windows-sys 0.52.0", +] + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + +[[package]] name = "renderbud" version = "0.1.0" dependencies = [ @@ -5819,6 +6362,36 @@ dependencies = [ ] [[package]] +name = "rkyv" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.16.1", + "indexmap 2.9.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] name = "rle-decode-fast" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5985,6 +6558,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] +name = "ruzstd" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01" +dependencies = [ + "twox-hash", +] + +[[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6062,7 +6644,7 @@ dependencies = [ "password-hash", "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -6073,7 +6655,7 @@ checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" dependencies = [ "ab_glyph", "log", - "memmap2", + "memmap2 0.9.10", "smithay-client-toolkit", "tiny-skia", ] @@ -6170,18 +6752,39 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -6259,7 +6862,7 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.104", @@ -6273,7 +6876,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -6284,7 +6887,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0", ] [[package]] @@ -6297,6 +6911,16 @@ dependencies = [ ] [[package]] +name = "shared-buffer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" +dependencies = [ + "bytes", + "memmap2 0.6.2", +] + +[[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6327,6 +6951,12 @@ dependencies = [ ] [[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6386,7 +7016,7 @@ dependencies = [ "cursor-icon", "libc", "log", - "memmap2", + "memmap2 0.9.10", "rustix 0.38.44", "thiserror 1.0.69", "wayland-backend", @@ -6609,6 +7239,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] name = "tempfile" version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6641,11 +7277,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.18", ] [[package]] @@ -6661,9 +7297,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -7101,11 +7737,17 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.18", "utf-8", ] [[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] name = "type-map" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7228,6 +7870,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7239,7 +7887,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] @@ -7522,6 +8170,16 @@ dependencies = [ ] [[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] name = "wasm-streams" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7535,6 +8193,199 @@ dependencies = [ ] [[package]] +name = "wasmer" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b76134972161fb9ae6d956d3e79177a51c6c968d4f95fdba060a95897b2fe81c" +dependencies = [ + "bindgen 0.72.1", + "bytes", + "cfg-if", + "cmake", + "derive_more", + "indexmap 2.9.0", + "js-sys", + "more-asserts", + "paste", + "rustc-demangle", + "serde", + "serde-wasm-bindgen", + "shared-buffer", + "tar", + "target-lexicon 0.13.5", + "thiserror 2.0.18", + "tracing", + "wasm-bindgen", + "wasmer-compiler", + "wasmer-compiler-cranelift", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "wasmparser", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-compiler" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c21c166e89212d5bc31d08dffdb189fe689f4ca58756ccd924f9fc3ec2ee89da" +dependencies = [ + "backtrace", + "bytes", + "cfg-if", + "enum-iterator", + "enumset", + "itertools 0.14.0", + "leb128", + "libc", + "macho-unwind-info", + "memmap2 0.9.10", + "more-asserts", + "object 0.38.1", + "rangemap", + "region", + "rkyv", + "self_cell", + "shared-buffer", + "smallvec", + "target-lexicon 0.13.5", + "tempfile", + "thiserror 2.0.18", + "wasmer-types", + "wasmer-vm", + "wasmparser", + "which 8.0.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-compiler-cranelift" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3174d3c78dd581d590860195420305581e2708cb1082ce8bea05cf6ed0e432" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "gimli 0.32.3", + "indexmap 2.9.0", + "itertools 0.14.0", + "leb128", + "more-asserts", + "rayon", + "smallvec", + "target-lexicon 0.13.5", + "tracing", + "wasmer-compiler", + "wasmer-types", +] + +[[package]] +name = "wasmer-derive" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20424fca4c6a757d7115a39ad5c6a5d369f5f864a5c64263d6de14b40f060cc8" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "wasmer-types" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7f91b0cb63705afa0843b46a0aeaeaedff7be2e5b05691176e9e58e2dbe921" +dependencies = [ + "bytecheck", + "enum-iterator", + "enumset", + "getrandom 0.2.16", + "hex", + "indexmap 2.9.0", + "more-asserts", + "rkyv", + "sha2 0.11.0-rc.5", + "target-lexicon 0.13.5", + "thiserror 2.0.18", +] + +[[package]] +name = "wasmer-vm" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12da92bb2c2abd09628d28d112970880a9e671b7ccb55172e5f6db01b5d6add4" +dependencies = [ + "backtrace", + "cc", + "cfg-if", + "corosensei", + "crossbeam-queue", + "dashmap", + "enum-iterator", + "fnv", + "gimli 0.32.3", + "indexmap 2.9.0", + "libc", + "libunwind", + "mach2 0.6.0", + "memoffset", + "more-asserts", + "parking_lot", + "region", + "rustversion", + "scopeguard", + "thiserror 2.0.18", + "wasmer-types", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.9.1", + "indexmap 2.9.0", + "semver", +] + +[[package]] +name = "wasmtime-internal-math" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61fe7cfca53d0ce01dc480ce1db93ad48b6fa1f354d8ff0680ac6a76ef354a3" +dependencies = [ + "libm", +] + +[[package]] +name = "wast" +version = "244.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e7b9f9e23311275920e3d6b56d64137c160cf8af4f84a7283b36cfecbf4acb" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width 0.2.2", + "wasm-encoder", +] + +[[package]] +name = "wat" +version = "1.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf35b87ed352f9ab6cd0732abde5a67dd6153dfd02c493e61459218b19456fa" +dependencies = [ + "wast", +] + +[[package]] name = "wayland-backend" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7759,7 +8610,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "wgpu-hal", "wgpu-types", ] @@ -7802,7 +8653,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "wasm-bindgen", "web-sys", "wgpu-types", @@ -7835,6 +8686,17 @@ dependencies = [ ] [[package]] +name = "which" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +dependencies = [ + "env_home", + "rustix 1.0.7", + "winsafe", +] + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -8410,7 +9272,7 @@ dependencies = [ "dpi 0.1.1", "js-sys", "libc", - "memmap2", + "memmap2 0.9.10", "ndk", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -8462,7 +9324,7 @@ dependencies = [ "dpi 0.1.2", "js-sys", "libc", - "memmap2", + "memmap2 0.9.10", "ndk", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -8503,6 +9365,12 @@ dependencies = [ ] [[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/tokenator", "crates/enostr", "crates/protoverse", + "crates/notedeck_wasm", ] [workspace.dependencies] @@ -79,6 +80,7 @@ notedeck_notebook = { path = "crates/notedeck_notebook" } notedeck_nostrverse = { path = "crates/notedeck_nostrverse" } renderbud = { path = "../github/jb55/renderbud", features = ["egui"] } notedeck_ui = { path = "crates/notedeck_ui" } +notedeck_wasm = { path = "crates/notedeck_wasm" } tokenator = { path = "crates/tokenator" } md-stream = { path = "crates/md-stream" } once_cell = "1.19.0" diff --git a/crates/notedeck_chrome/Cargo.toml b/crates/notedeck_chrome/Cargo.toml @@ -23,6 +23,7 @@ notedeck_notebook = { workspace = true, optional = true } notedeck_nostrverse = { workspace = true, optional = true } notedeck_clndash = { workspace = true, optional = true } notedeck_dashboard = { workspace = true, optional = true } +notedeck_wasm = { workspace = true, optional = true } notedeck = { workspace = true } nostrdb = { workspace = true } puffin = { workspace = true, optional = true } @@ -61,6 +62,7 @@ notebook = ["notedeck_notebook"] nostrverse = ["notedeck_nostrverse"] clndash = ["notedeck_clndash"] dashboard = ["notedeck_dashboard"] +wasm_apps = ["notedeck_wasm"] [target.'cfg(target_os = "android")'.dependencies] tracing-logcat = "0.1.0" diff --git a/crates/notedeck_chrome/src/app.rs b/crates/notedeck_chrome/src/app.rs @@ -37,7 +37,7 @@ pub enum NotedeckApp { #[cfg(feature = "nostrverse")] Nostrverse(Box<NostrverseApp>), - Other(Box<dyn notedeck::App>), + Other(String, Box<dyn notedeck::App>), } impl notedeck::App for NotedeckApp { @@ -62,7 +62,7 @@ impl notedeck::App for NotedeckApp { #[cfg(feature = "nostrverse")] NotedeckApp::Nostrverse(nostrverse) => nostrverse.update(ctx, ui), - NotedeckApp::Other(other) => other.update(ctx, ui), + NotedeckApp::Other(_name, other) => other.update(ctx, ui), } } } diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs @@ -161,6 +161,11 @@ impl Chrome { cc.egui_ctx.clone(), context.path, ); + #[cfg(feature = "wasm_apps")] + let wasm_dir = context + .path + .path(notedeck::DataPathType::Cache) + .join("wasm_apps"); let mut chrome = Chrome::default(); if !app_args.iter().any(|arg| arg == "--no-columns-app") { @@ -188,6 +193,39 @@ impl Chrome { notedeck_nostrverse::NostrverseApp::demo(cc.wgpu_render_state.as_ref()), ))); + #[cfg(feature = "wasm_apps")] + { + tracing::info!("looking for WASM apps in: {}", wasm_dir.display()); + if wasm_dir.is_dir() { + if let Ok(entries) = std::fs::read_dir(&wasm_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().is_some_and(|e| e == "wasm") { + match notedeck_wasm::WasmApp::from_file(&path) { + Ok(app) => { + let name = app.name().to_string(); + tracing::info!( + "loaded WASM app '{}': {}", + name, + path.display() + ); + chrome.add_app(NotedeckApp::Other(name, Box::new(app))); + } + Err(e) => { + tracing::error!( + "failed to load WASM app {}: {e}", + path.display() + ); + } + } + } + } + } + } else { + tracing::info!("WASM apps directory not found: {}", wasm_dir.display()); + } + } + chrome.set_active(0); Ok(chrome) @@ -851,7 +889,9 @@ fn topdown_sidebar( tr!(loc, "Nostrverse", "Button to go to the Nostrverse app") } - NotedeckApp::Other(_) => tr!(loc, "Other", "Button to go to the Other app"), + NotedeckApp::Other(name, _) => { + tr!(loc, name.as_str(), "Button to go to a WASM app") + } }; StripBuilder::new(ui) @@ -861,52 +901,49 @@ fn topdown_sidebar( strip.strip(|b| { let resp = drawer_item( b, - |ui| { - match app { - NotedeckApp::Columns(_columns_app) => { - ui.add(app_images::columns_image()); - } + |ui| match app { + NotedeckApp::Columns(_columns_app) => { + ui.add(app_images::columns_image()); + } - NotedeckApp::Dave(dave) => { - dave_button( - dave.avatar_mut(), - ui, - Rect::from_center_size( - ui.available_rect_before_wrap().center(), - vec2(30.0, 30.0), - ), - ); - } + NotedeckApp::Dave(dave) => { + dave_button( + dave.avatar_mut(), + ui, + Rect::from_center_size( + ui.available_rect_before_wrap().center(), + vec2(30.0, 30.0), + ), + ); + } - #[cfg(feature = "dashboard")] - NotedeckApp::Dashboard(_columns_app) => { - ui.add(app_images::algo_image()); - } + #[cfg(feature = "dashboard")] + NotedeckApp::Dashboard(_columns_app) => { + ui.add(app_images::algo_image()); + } - #[cfg(feature = "messages")] - NotedeckApp::Messages(_dms) => { - ui.add(app_images::new_message_image()); - } + #[cfg(feature = "messages")] + NotedeckApp::Messages(_dms) => { + ui.add(app_images::new_message_image()); + } - #[cfg(feature = "clndash")] - NotedeckApp::ClnDash(_clndash) => { - clndash_button(ui); - } + #[cfg(feature = "clndash")] + NotedeckApp::ClnDash(_clndash) => { + clndash_button(ui); + } - #[cfg(feature = "notebook")] - NotedeckApp::Notebook(_notebook) => { - notebook_button(ui); - } + #[cfg(feature = "notebook")] + NotedeckApp::Notebook(_notebook) => { + notebook_button(ui); + } - #[cfg(feature = "nostrverse")] - NotedeckApp::Nostrverse(_nostrverse) => { - ui.add(app_images::universe_image()); - } + #[cfg(feature = "nostrverse")] + NotedeckApp::Nostrverse(_nostrverse) => { + ui.add(app_images::universe_image()); + } - NotedeckApp::Other(_other) => { - // app provides its own button rendering ui? - panic!("TODO: implement other apps") - } + NotedeckApp::Other(_name, _other) => { + ui.label("W"); } }, text, diff --git a/crates/notedeck_wasm/Cargo.toml b/crates/notedeck_wasm/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "notedeck_wasm" +version = { workspace = true } +edition = "2021" +license = "GPLv3" +description = "WASM mini-app runtime for Notedeck" + +[dependencies] +wasmer = "7" +notedeck = { workspace = true } +egui = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +wat = "1" +dirs = { workspace = true } diff --git a/crates/notedeck_wasm/api/notedeck_api.h b/crates/notedeck_wasm/api/notedeck_api.h @@ -0,0 +1,27 @@ +#ifndef NOTEDECK_API_H +#define NOTEDECK_API_H + +/* + * Notedeck WASM API — Stable Interface + * + * Stability guarantees: + * - Function signatures will never change once published + * - New functions may be added; existing ones will not be removed + * - All parameters are i32, f32, or (const char *, int) byte buffers + * - Extended versions use the _ex suffix if needed + * + * WASM module requirements: + * - Must export: void nd_update(void) + * - Must export: memory (1+ pages) + * - Optional exports: nd_app_name_ptr (i32), nd_app_name_len (i32) + */ + +/* Text & widgets */ +void nd_label(const char *text, int len); +void nd_heading(const char *text, int len); +int nd_button(const char *text, int len); /* returns 1 if clicked (prev frame) */ + +/* Layout */ +void nd_add_space(float pixels); + +#endif /* NOTEDECK_API_H */ diff --git a/crates/notedeck_wasm/examples/build_hello_wasm.rs b/crates/notedeck_wasm/examples/build_hello_wasm.rs @@ -0,0 +1,134 @@ +/// Compiles the hello WAT module to a .wasm file and places it in +/// the notedeck wasm_apps directory for testing. +/// +/// Usage: cargo run -p notedeck_wasm --example build_hello_wasm +fn main() { + let wat = r#"(module + (import "env" "nd_heading" (func $nd_heading (param i32 i32))) + (import "env" "nd_label" (func $nd_label (param i32 i32))) + (import "env" "nd_button" (func $nd_button (param i32 i32) (result i32))) + (import "env" "nd_add_space" (func $nd_add_space (param f32))) + + (memory (export "memory") 1) + + ;; Static strings + (data (i32.const 0) "Hello from WASM!") ;; 16 bytes + (data (i32.const 16) "Click me") ;; 8 bytes + (data (i32.const 24) "Clicks: ") ;; 8 bytes + (data (i32.const 32) "0123456789") ;; digit table + + ;; App metadata + (data (i32.const 500) "Hello WASM") + (global (export "nd_app_name_ptr") i32 (i32.const 500)) + (global (export "nd_app_name_len") i32 (i32.const 10)) + + ;; Global click counter + (global $count (mut i32) (i32.const 0)) + + ;; Simple itoa: writes decimal digits at ptr, returns length + (func $itoa (param $n i32) (param $ptr i32) (result i32) + (local $len i32) + (local $tmp i32) + (local $start i32) + (local $end i32) + (local $swap i32) + + ;; Handle zero + (if (i32.eqz (local.get $n)) + (then + (i32.store8 (local.get $ptr) (i32.const 48)) ;; '0' + (return (i32.const 1)) + ) + ) + + ;; Write digits in reverse + (local.set $tmp (local.get $n)) + (local.set $len (i32.const 0)) + (block $done + (loop $digits + (br_if $done (i32.eqz (local.get $tmp))) + (i32.store8 + (i32.add (local.get $ptr) (local.get $len)) + (i32.add (i32.const 48) + (i32.rem_u (local.get $tmp) (i32.const 10)))) + (local.set $tmp (i32.div_u (local.get $tmp) (i32.const 10))) + (local.set $len (i32.add (local.get $len) (i32.const 1))) + (br $digits) + ) + ) + + ;; Reverse the digits in place + (local.set $start (i32.const 0)) + (local.set $end (i32.sub (local.get $len) (i32.const 1))) + (block $rev_done + (loop $rev + (br_if $rev_done (i32.ge_u (local.get $start) (local.get $end))) + ;; swap + (local.set $swap + (i32.load8_u (i32.add (local.get $ptr) (local.get $start)))) + (i32.store8 + (i32.add (local.get $ptr) (local.get $start)) + (i32.load8_u (i32.add (local.get $ptr) (local.get $end)))) + (i32.store8 + (i32.add (local.get $ptr) (local.get $end)) + (local.get $swap)) + (local.set $start (i32.add (local.get $start) (i32.const 1))) + (local.set $end (i32.sub (local.get $end) (i32.const 1))) + (br $rev) + ) + ) + + (local.get $len) + ) + + (func (export "nd_update") + (local $num_len i32) + + ;; Heading + (call $nd_heading (i32.const 0) (i32.const 16)) + (call $nd_add_space (f32.const 8.0)) + + ;; Button + (if (call $nd_button (i32.const 16) (i32.const 8)) + (then + (global.set $count + (i32.add (global.get $count) (i32.const 1))) + ) + ) + (call $nd_add_space (f32.const 4.0)) + + ;; "Clicks: " prefix is at offset 24 (8 bytes) + ;; Write the number starting at offset 100 (scratch space) + (local.set $num_len + (call $itoa (global.get $count) (i32.const 100))) + + ;; Copy "Clicks: " to offset 200, then append the number + ;; offset 200: "Clicks: " + (memory.copy (i32.const 200) (i32.const 24) (i32.const 8)) + ;; offset 208: number digits + (memory.copy + (i32.const 208) + (i32.const 100) + (local.get $num_len)) + + ;; Label with total length = 8 + num_len + (call $nd_label + (i32.const 200) + (i32.add (i32.const 8) (local.get $num_len))) + ) + )"#; + + let wasm = wat::parse_str(wat).expect("failed to parse WAT"); + + // Write to the notedeck wasm_apps directory + let dir = dirs::data_dir() + .expect("data dir") + .join("notedeck") + .join("cache") + .join("wasm_apps"); + std::fs::create_dir_all(&dir).expect("create wasm_apps dir"); + + let path = dir.join("hello.wasm"); + std::fs::write(&path, &wasm).expect("write hello.wasm"); + println!("Wrote {} bytes to {}", wasm.len(), path.display()); +} diff --git a/crates/notedeck_wasm/examples/hello.c b/crates/notedeck_wasm/examples/hello.c @@ -0,0 +1,53 @@ +/* + * Minimal notedeck WASM app. + * + * Build: + * clang --target=wasm32 -nostdlib \ + * -Wl,--no-entry -Wl,--export=nd_update -Wl,--allow-undefined \ + * -o hello.wasm hello.c + */ + +#include "../api/notedeck_api.h" + +static int count = 0; + +/* simple int-to-string, returns length written */ +static int itoa_simple(int n, char *buf, int buf_size) { + if (buf_size < 2) return 0; + if (n == 0) { buf[0] = '0'; return 1; } + + int neg = 0; + if (n < 0) { neg = 1; n = -n; } + + int len = 0; + char tmp[16]; + while (n > 0 && len < 16) { + tmp[len++] = '0' + (n % 10); + n /= 10; + } + if (neg && len < 16) tmp[len++] = '-'; + + if (len > buf_size) len = buf_size; + for (int i = 0; i < len; i++) { + buf[i] = tmp[len - 1 - i]; + } + return len; +} + +void nd_update(void) { + nd_heading("Hello from WASM!", 16); + nd_add_space(8.0f); + + if (nd_button("Click me", 8)) { + count++; + } + + nd_add_space(4.0f); + + char buf[48]; + /* "Clicks: " prefix */ + buf[0]='C'; buf[1]='l'; buf[2]='i'; buf[3]='c'; + buf[4]='k'; buf[5]='s'; buf[6]=':'; buf[7]=' '; + int num_len = itoa_simple(count, buf + 8, 40); + nd_label(buf, 8 + num_len); +} diff --git a/crates/notedeck_wasm/src/commands.rs b/crates/notedeck_wasm/src/commands.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; + +pub enum UiCommand { + Label(String), + Heading(String), + Button(String), + AddSpace(f32), +} + +/// Render buffered commands into egui, returning button click events. +/// Keys are `button_key(text, occurrence)`. +pub fn render_commands(commands: &[UiCommand], ui: &mut egui::Ui) -> HashMap<String, bool> { + let mut events = HashMap::new(); + let mut button_occ: HashMap<&str, u32> = HashMap::new(); + + for cmd in commands { + match cmd { + UiCommand::Label(text) => { + ui.label(text.as_str()); + } + UiCommand::Heading(text) => { + ui.heading(text.as_str()); + } + UiCommand::Button(text) => { + let occ = button_occ.entry(text.as_str()).or_insert(0); + let key = button_key(text, *occ); + *occ += 1; + let clicked = ui.button(text.as_str()).clicked(); + events.insert(key, clicked); + } + UiCommand::AddSpace(px) => { + ui.add_space(*px); + } + } + } + + events +} + +pub fn button_key(text: &str, occurrence: u32) -> String { + if occurrence == 0 { + text.to_string() + } else { + format!("{}#{}", text, occurrence) + } +} diff --git a/crates/notedeck_wasm/src/host_fns.rs b/crates/notedeck_wasm/src/host_fns.rs @@ -0,0 +1,87 @@ +use crate::commands::{button_key, UiCommand}; +use std::collections::HashMap; +use wasmer::{FunctionEnv, FunctionEnvMut, Imports, Memory, MemoryView, Store}; + +pub struct HostEnv { + pub memory: Option<Memory>, + pub commands: Vec<UiCommand>, + pub button_events: HashMap<String, bool>, + pub button_occ: HashMap<String, u32>, +} + +impl HostEnv { + pub fn new() -> Self { + Self { + memory: None, + commands: Vec::new(), + button_events: HashMap::new(), + button_occ: HashMap::new(), + } + } +} + +/// Read a UTF-8 string from WASM linear memory. +fn read_wasm_str(view: &MemoryView, ptr: i32, len: i32) -> Option<String> { + let ptr = ptr as u64; + let len = len as usize; + let mut buf = vec![0u8; len]; + view.read(ptr, &mut buf).ok()?; + String::from_utf8(buf).ok() +} + +/// Register all host imports into the given store. +pub fn create_imports(store: &mut Store, env: &FunctionEnv<HostEnv>) -> Imports { + use wasmer::Function; + + fn nd_label(mut env: FunctionEnvMut<HostEnv>, ptr: i32, len: i32) { + let (data, store) = env.data_and_store_mut(); + let memory = data.memory.as_ref().expect("memory not set"); + let view = memory.view(&store); + if let Some(text) = read_wasm_str(&view, ptr, len) { + data.commands.push(UiCommand::Label(text)); + } + } + + fn nd_heading(mut env: FunctionEnvMut<HostEnv>, ptr: i32, len: i32) { + let (data, store) = env.data_and_store_mut(); + let memory = data.memory.as_ref().expect("memory not set"); + let view = memory.view(&store); + if let Some(text) = read_wasm_str(&view, ptr, len) { + data.commands.push(UiCommand::Heading(text)); + } + } + + fn nd_button(mut env: FunctionEnvMut<HostEnv>, ptr: i32, len: i32) -> i32 { + let (data, store) = env.data_and_store_mut(); + let memory = data.memory.as_ref().expect("memory not set"); + let view = memory.view(&store); + if let Some(text) = read_wasm_str(&view, ptr, len) { + let occ = data.button_occ.entry(text.clone()).or_insert(0); + let key = button_key(&text, *occ); + *occ += 1; + let clicked = data.button_events.get(&key).copied().unwrap_or(false); + data.commands.push(UiCommand::Button(text)); + if clicked { + 1 + } else { + 0 + } + } else { + 0 + } + } + + fn nd_add_space(mut env: FunctionEnvMut<HostEnv>, pixels: f32) { + let (data, _store) = env.data_and_store_mut(); + data.commands.push(UiCommand::AddSpace(pixels)); + } + + wasmer::imports! { + "env" => { + "nd_label" => Function::new_typed_with_env(store, env, nd_label), + "nd_heading" => Function::new_typed_with_env(store, env, nd_heading), + "nd_button" => Function::new_typed_with_env(store, env, nd_button), + "nd_add_space" => Function::new_typed_with_env(store, env, nd_add_space), + } + } +} diff --git a/crates/notedeck_wasm/src/lib.rs b/crates/notedeck_wasm/src/lib.rs @@ -0,0 +1,365 @@ +mod commands; +mod host_fns; + +use host_fns::HostEnv; +use notedeck::{AppContext, AppResponse}; +use wasmer::{FunctionEnv, Instance, Module, Store}; + +pub struct WasmApp { + store: Store, + instance: Instance, + env: FunctionEnv<HostEnv>, + name: String, +} + +impl WasmApp { + /// Load a WASM app from raw bytes. + pub fn from_bytes(wasm_bytes: &[u8]) -> Result<Self, Box<dyn std::error::Error>> { + let mut store = Store::default(); + let module = Module::new(&store, wasm_bytes)?; + + let host_env = HostEnv::new(); + let env = FunctionEnv::new(&mut store, host_env); + + let imports = host_fns::create_imports(&mut store, &env); + let instance = Instance::new(&mut store, &module, &imports)?; + + // Give host functions access to WASM linear memory. + let memory = instance.exports.get_memory("memory")?.clone(); + env.as_mut(&mut store).memory = Some(memory.clone()); + + // Read app name from exported globals, if present. + let name = + read_app_name(&instance, &mut store, &memory).unwrap_or_else(|| "WASM App".to_string()); + + Ok(Self { + store, + instance, + env, + name, + }) + } + + /// Load a WASM app from a file path. + pub fn from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> { + let bytes = std::fs::read(path)?; + Self::from_bytes(&bytes) + } + + /// The display name of this WASM app. + pub fn name(&self) -> &str { + &self.name + } +} + +/// Read app name from WASM exports: nd_app_name_ptr (i32) and nd_app_name_len (i32). +fn read_app_name( + instance: &Instance, + store: &mut Store, + memory: &wasmer::Memory, +) -> Option<String> { + let ptr_global = instance.exports.get_global("nd_app_name_ptr").ok()?; + let len_global = instance.exports.get_global("nd_app_name_len").ok()?; + + let ptr = ptr_global.get(store).i32()? as u64; + let len = len_global.get(store).i32()? as usize; + + if len == 0 { + return None; + } + + let view = memory.view(store); + let mut buf = vec![0u8; len]; + view.read(ptr, &mut buf).ok()?; + String::from_utf8(buf).ok() +} + +impl notedeck::App for WasmApp { + fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse { + // 1. Clear commands and reset occurrence counter + { + let data = self.env.as_mut(&mut self.store); + data.commands.clear(); + data.button_occ.clear(); + } + + // 2. Run WASM — host functions push commands + let nd_update = self + .instance + .exports + .get_function("nd_update") + .expect("WASM module must export nd_update"); + if let Err(e) = nd_update.call(&mut self.store, &[]) { + tracing::error!("WASM nd_update error: {e}"); + } + + // 3. Take commands and render with real UI + let cmds = { + let data = self.env.as_mut(&mut self.store); + std::mem::take(&mut data.commands) + }; + let new_events = commands::render_commands(&cmds, ui); + + // 4. Store events for next frame + self.env.as_mut(&mut self.store).button_events = new_events; + + AppResponse::none() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::commands::UiCommand; + + /// Helper: compile WAT to WASM bytes and load as WasmApp. + fn app_from_wat(wat: &str) -> WasmApp { + let wasm = wat::parse_str(wat).expect("valid WAT"); + WasmApp::from_bytes(&wasm).expect("load WASM module") + } + + /// Helper: run one frame of the WASM app inside a headless egui context. + /// Returns the commands that were generated. + fn run_update(app: &mut WasmApp) -> Vec<UiCommand> { + let mut result_cmds = Vec::new(); + let ctx = egui::Context::default(); + let _ = ctx.run(egui::RawInput::default(), |ctx| { + egui::CentralPanel::default().show(ctx, |ui| { + // Clear + reset + { + let data = app.env.as_mut(&mut app.store); + data.commands.clear(); + data.button_occ.clear(); + } + + // Run WASM + let nd_update = app + .instance + .exports + .get_function("nd_update") + .expect("nd_update"); + nd_update.call(&mut app.store, &[]).expect("nd_update ok"); + + // Grab commands before rendering + let cmds = { + let data = app.env.as_mut(&mut app.store); + std::mem::take(&mut data.commands) + }; + result_cmds = cmds + .iter() + .map(|c| match c { + UiCommand::Label(t) => UiCommand::Label(t.clone()), + UiCommand::Heading(t) => UiCommand::Heading(t.clone()), + UiCommand::Button(t) => UiCommand::Button(t.clone()), + UiCommand::AddSpace(px) => UiCommand::AddSpace(*px), + }) + .collect(); + + // Render and collect events + let new_events = commands::render_commands(&cmds, ui); + app.env.as_mut(&mut app.store).button_events = new_events; + }); + }); + result_cmds + } + + #[test] + fn load_empty_module() { + let app = app_from_wat( + r#"(module + (memory (export "memory") 1) + (func (export "nd_update")) + )"#, + ); + assert!(app.instance.exports.get_function("nd_update").is_ok()); + } + + #[test] + fn run_noop_update() { + let mut app = app_from_wat( + r#"(module + (memory (export "memory") 1) + (func (export "nd_update")) + )"#, + ); + let cmds = run_update(&mut app); + assert!(cmds.is_empty()); + } + + #[test] + fn call_nd_label() { + let mut app = app_from_wat( + r#"(module + (import "env" "nd_label" (func $nd_label (param i32 i32))) + (memory (export "memory") 1) + (data (i32.const 0) "hi") + (func (export "nd_update") + (call $nd_label (i32.const 0) (i32.const 2)) + ) + )"#, + ); + let cmds = run_update(&mut app); + assert_eq!(cmds.len(), 1); + assert!(matches!(&cmds[0], UiCommand::Label(t) if t == "hi")); + } + + #[test] + fn call_nd_heading() { + let mut app = app_from_wat( + r#"(module + (import "env" "nd_heading" (func $nd_heading (param i32 i32))) + (memory (export "memory") 1) + (data (i32.const 0) "Title") + (func (export "nd_update") + (call $nd_heading (i32.const 0) (i32.const 5)) + ) + )"#, + ); + let cmds = run_update(&mut app); + assert_eq!(cmds.len(), 1); + assert!(matches!(&cmds[0], UiCommand::Heading(t) if t == "Title")); + } + + #[test] + fn call_nd_button() { + let mut app = app_from_wat( + r#"(module + (import "env" "nd_button" (func $nd_button (param i32 i32) (result i32))) + (memory (export "memory") 1) + (data (i32.const 0) "Click") + (func (export "nd_update") + (drop (call $nd_button (i32.const 0) (i32.const 5))) + ) + )"#, + ); + let cmds = run_update(&mut app); + assert_eq!(cmds.len(), 1); + assert!(matches!(&cmds[0], UiCommand::Button(t) if t == "Click")); + } + + #[test] + fn call_nd_add_space() { + let mut app = app_from_wat( + r#"(module + (import "env" "nd_add_space" (func $nd_add_space (param f32))) + (memory (export "memory") 1) + (func (export "nd_update") + (call $nd_add_space (f32.const 10.0)) + ) + )"#, + ); + let cmds = run_update(&mut app); + assert_eq!(cmds.len(), 1); + assert!(matches!(&cmds[0], UiCommand::AddSpace(px) if (*px - 10.0).abs() < f32::EPSILON)); + } + + #[test] + fn call_multiple_host_fns() { + let mut app = app_from_wat( + r#"(module + (import "env" "nd_heading" (func $nd_heading (param i32 i32))) + (import "env" "nd_label" (func $nd_label (param i32 i32))) + (import "env" "nd_button" (func $nd_button (param i32 i32) (result i32))) + (import "env" "nd_add_space" (func $nd_add_space (param f32))) + (memory (export "memory") 1) + (data (i32.const 0) "Hello") + (data (i32.const 5) "World") + (data (i32.const 10) "Btn") + (func (export "nd_update") + (call $nd_heading (i32.const 0) (i32.const 5)) + (call $nd_add_space (f32.const 8.0)) + (call $nd_label (i32.const 5) (i32.const 5)) + (drop (call $nd_button (i32.const 10) (i32.const 3))) + ) + )"#, + ); + let cmds = run_update(&mut app); + assert_eq!(cmds.len(), 4); + assert!(matches!(&cmds[0], UiCommand::Heading(t) if t == "Hello")); + assert!(matches!(&cmds[1], UiCommand::AddSpace(_))); + assert!(matches!(&cmds[2], UiCommand::Label(t) if t == "World")); + assert!(matches!(&cmds[3], UiCommand::Button(t) if t == "Btn")); + } + + #[test] + fn button_returns_prev_frame_event() { + // Module that stores nd_button's return value in a global. + let mut app = app_from_wat( + r#"(module + (import "env" "nd_button" (func $nd_button (param i32 i32) (result i32))) + (memory (export "memory") 1) + (data (i32.const 0) "Click") + (global $result (mut i32) (i32.const -1)) + (global (export "btn_result") (mut i32) (i32.const -1)) + (func (export "nd_update") + (global.set $result (call $nd_button (i32.const 0) (i32.const 5))) + (global.set 1 (global.get $result)) + ) + )"#, + ); + + // Frame 1: no previous events, button returns 0 + run_update(&mut app); + let result = app + .instance + .exports + .get_global("btn_result") + .unwrap() + .get(&mut app.store) + .i32() + .unwrap(); + assert_eq!(result, 0, "first frame: button should return 0"); + + // Simulate a click by injecting an event + app.env + .as_mut(&mut app.store) + .button_events + .insert("Click".to_string(), true); + + // Frame 2: button should now return 1 + run_update(&mut app); + let result = app + .instance + .exports + .get_global("btn_result") + .unwrap() + .get(&mut app.store) + .i32() + .unwrap(); + assert_eq!( + result, 1, + "second frame: button should return 1 after click" + ); + } + + #[test] + fn app_name_from_exports() { + let app = app_from_wat( + r#"(module + (memory (export "memory") 1) + (data (i32.const 500) "Test App") + (global (export "nd_app_name_ptr") i32 (i32.const 500)) + (global (export "nd_app_name_len") i32 (i32.const 8)) + (func (export "nd_update")) + )"#, + ); + assert_eq!(app.name(), "Test App"); + } + + #[test] + fn app_name_defaults_when_missing() { + let app = app_from_wat( + r#"(module + (memory (export "memory") 1) + (func (export "nd_update")) + )"#, + ); + assert_eq!(app.name(), "WASM App"); + } + + #[test] + fn from_bytes_rejects_invalid_wasm() { + let result = WasmApp::from_bytes(b"not wasm"); + assert!(result.is_err()); + } +}