diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..7824b18 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[unstable] +# Remove once Rust makes this the default. +wasm_c_abi="spec" diff --git a/Cargo.lock b/Cargo.lock index 94671eb..fb9187f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -17,6 +17,37 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "anymap2" version = "0.13.0" @@ -25,15 +56,15 @@ checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -53,6 +84,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bitmaps" version = "2.1.0" @@ -70,24 +107,27 @@ checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.0.83" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" [[package]] name = "cfg-if" @@ -105,6 +145,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "equivalent" version = "1.0.1" @@ -119,18 +165,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -142,9 +188,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -152,44 +198,44 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.73", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -204,10 +250,34 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.2.11" +name = "gc-arena" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "3cd70cf88a32937834aae9614ff2569b5d9467fa0c42c5d7762fd94a8de88266" +dependencies = [ + "allocator-api2", + "gc-arena-derive", + "hashbrown", + "sptr", +] + +[[package]] +name = "gc-arena-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c612a69f5557a11046b77a7408d2836fe77077f842171cd211c5ef504bd3cddd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.73", + "synstructure", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -218,9 +288,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "gloo" @@ -251,7 +321,7 @@ dependencies = [ "gloo-dialogs 0.2.0", "gloo-events 0.2.0", "gloo-file 0.3.0", - "gloo-history 0.2.1", + "gloo-history 0.2.2", "gloo-net 0.4.0", "gloo-render 0.2.0", "gloo-storage 0.3.0", @@ -368,15 +438,15 @@ dependencies = [ [[package]] name = "gloo-history" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4022e82f5f9e03cb1251b13c0a967e0600e97aa179c617f6519bac40640160" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" dependencies = [ "getrandom", "gloo-events 0.2.0", "gloo-utils 0.2.0", "serde", - "serde-wasm-bindgen 0.6.1", + "serde-wasm-bindgen 0.6.5", "serde_urlencoded", "thiserror", "wasm-bindgen", @@ -566,26 +636,36 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.73", ] [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -608,9 +688,9 @@ dependencies = [ [[package]] name = "implicit-clone" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c5448a864f9abf124ef8bf2a3cc37eb9fd99fe2e804a8b3235d7357bca2c25" +checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84" dependencies = [ "implicit-clone-derive", "indexmap", @@ -623,24 +703,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9311685eb9a34808bbb0608ad2fcab9ae216266beca5848613e95553ac914e3b" dependencies = [ "quote", - "syn 2.0.39", + "syn 2.0.73", ] [[package]] name = "indexmap" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown", ] [[package]] -name = "itoa" -version = "1.0.9" +name = "itertools" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" @@ -653,31 +751,55 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minicrossterm" +version = "0.28.1" +source = "git+https://git.blastmud.org/blasthavers/minicrossterm.git?rev=494f89daef41162fbd89d5266e261018ed5ff6dc#494f89daef41162fbd89d5266e261018ed5ff6dc" +dependencies = [ + "bitflags", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -690,50 +812,90 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "ouroboros" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" +dependencies = [ + "heck", + "itertools 0.12.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.73", +] [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "piccolo" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003bf52de285e1ff1adcbc6572588db3849988ea660a2d55af3a2ffbc81f597f" +dependencies = [ + "ahash", + "allocator-api2", + "anyhow", + "gc-arena", + "hashbrown", + "rand", + "thiserror", +] [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.73", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -753,13 +915,22 @@ dependencies = [ ] [[package]] -name = "prettyplease" -version = "0.2.15" +name = "ppv-lite86" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.39", + "syn 2.0.73", ] [[package]] @@ -798,13 +969,26 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.73", + "version_check", + "yansi", +] + [[package]] name = "prokio" version = "0.1.0" @@ -824,18 +1008,42 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "rand_xoshiro" @@ -848,27 +1056,27 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.206" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284" dependencies = [ "serde_derive", ] @@ -886,9 +1094,9 @@ dependencies = [ [[package]] name = "serde-wasm-bindgen" -version = "0.6.1" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ba92964781421b6cef36bf0d7da26d201e96d84e1b10e7ae6ed416e516906d" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" dependencies = [ "js-sys", "serde", @@ -897,22 +1105,23 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.206" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.73", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -948,6 +1157,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "1.0.109" @@ -960,9 +1181,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "837a7e8026c6ce912ff01cefbe8cafc2f8010ac49682e2a3d9decc3bce1ecaaf" dependencies = [ "proc-macro2", "quote", @@ -970,30 +1191,41 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "1.0.50" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.73", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.73", ] [[package]] name = "tokio" -version = "1.34.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "pin-project-lite", @@ -1001,9 +1233,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -1012,9 +1244,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" @@ -1046,7 +1278,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.73", ] [[package]] @@ -1071,10 +1303,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] -name = "version_check" -version = "0.9.4" +name = "unicode-segmentation" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -1103,15 +1347,15 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.73", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -1137,7 +1381,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.73", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1160,13 +1404,38 @@ dependencies = [ [[package]] name = "winnow" -version = "0.5.19" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] +[[package]] +name = "worldwideportal" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "im", + "itertools 0.13.0", + "minicrossterm", + "nom", + "ouroboros", + "piccolo", + "thiserror", + "unicode-segmentation", + "unicode-width", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yew" version = "0.21.0" @@ -1204,15 +1473,26 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.73", ] [[package]] -name = "yew-test" -version = "0.1.0" +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "im", - "wasm-bindgen", - "web-sys", - "yew", + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.73", ] diff --git a/Cargo.toml b/Cargo.toml index 988db9b..9f12617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,15 @@ edition = "2021" [dependencies] im = "15.1.0" +itertools = "0.13.0" +nom = "7.1.3" +piccolo = "0.3.3" +unicode-segmentation = "1.11.0" +unicode-width = "0.1.13" wasm-bindgen = "0.2.92" web-sys = "0.3.69" yew = { version = "0.21.0", features = ["csr"] } +minicrossterm = { git = "https://git.blastmud.org/blasthavers/minicrossterm.git", rev = "494f89daef41162fbd89d5266e261018ed5ff6dc" } +thiserror = "1.0.63" +console_error_panic_hook = "0.1.7" +ouroboros = "0.18.4" diff --git a/src/command_handler.rs b/src/command_handler.rs new file mode 100644 index 0000000..61e45a4 --- /dev/null +++ b/src/command_handler.rs @@ -0,0 +1,57 @@ +use itertools::join; +use std::{cell::RefCell, rc::Rc}; +use yew::Callback; + +use crate::{ + echo_to_term_frame, lua_state::LuaState, parsing::parse_commands, RegisteredTermFrameLens, + TermFrame, +}; + +fn reentrant_command_handler( + lua_state: &mut LuaState, + frames: &RegisteredTermFrameLens, + term_frame: &TermFrame, + command_in: &str, +) { + web_sys::console::log_1(&"Inside command handler".into()); + echo_to_term_frame(frames, term_frame, "Hello World!\n"); + for command in parse_commands(command_in).commands { + match command.split_out_command() { + None => (), + Some((cmd, rest)) => { + if cmd == "##" { + match lua_state.execute(&join(rest.arguments.iter(), " ")) { + Ok(msg) => echo_to_term_frame(frames, term_frame, &msg).unwrap_or(()), + Err(msg) => echo_to_term_frame(frames, term_frame, &msg).unwrap_or(()), + } + } + } + } + } +} + +fn command_handler( + lua_state: &RefCell, + frames: &RegisteredTermFrameLens, + term_frame: &TermFrame, + command_in: String, +) { + match lua_state.try_borrow_mut() { + Err(_) => echo_to_term_frame( + frames, + term_frame, + "Attempt to re-enter command handler during processing.\n", + ) + .unwrap_or(()), // Ignore error handling error. + Ok(mut lua_state_m) => { + reentrant_command_handler(&mut lua_state_m, frames, term_frame, &command_in) + } + } +} + +pub fn make_command_handler_callback( + lua_state: Rc>, + frames: RegisteredTermFrameLens, +) -> Callback<(TermFrame, String), ()> { + Callback::from(move |(term, cmd)| command_handler(&lua_state, &frames, &term, cmd)) +} diff --git a/src/lineengine.rs b/src/lineengine.rs new file mode 100644 index 0000000..10b66a6 --- /dev/null +++ b/src/lineengine.rs @@ -0,0 +1,2 @@ +pub mod history; +pub mod line; diff --git a/src/lineengine/README.md b/src/lineengine/README.md new file mode 100644 index 0000000..2ec0bfe --- /dev/null +++ b/src/lineengine/README.md @@ -0,0 +1,10 @@ +The code is this directory is a hacked and cut back version of +https://github.com/zyansheep/rustyline-async + +In particular, we only use the history and line state machine part +of it, not the intended async interface of that crate (which is +not a good fit for our needs, and uses standard input). + +rustyline-async was released into the public domain under +the [Unlicense](https://unlicense.org/). Thank you to the original +authors! diff --git a/src/lineengine/history.rs b/src/lineengine/history.rs new file mode 100644 index 0000000..9805cf9 --- /dev/null +++ b/src/lineengine/history.rs @@ -0,0 +1,46 @@ +use std::collections::VecDeque; + +pub struct History { + pub entries: VecDeque, + pub max_size: usize, + current_position: Option, +} +impl Default for History { + fn default() -> Self { + Self { + entries: Default::default(), + max_size: 1000, + current_position: Default::default(), + } + } +} + +impl History { + // Find next history that matches a given string from an index + pub fn search_next(&mut self, _current: &str) -> Option<&str> { + if let Some(index) = &mut self.current_position { + if *index < self.entries.len() - 1 { + *index += 1; + } + Some(&self.entries[*index]) + } else if !self.entries.is_empty() { + self.current_position = Some(0); + Some(&self.entries[0]) + } else { + None + } + } + // Find previous history item that matches a given string from an index + pub fn search_previous(&mut self, _current: &str) -> Option<&str> { + if let Some(index) = &mut self.current_position { + if *index == 0 { + self.current_position = None; + return Some(""); + } + *index -= 1; + Some(&self.entries[*index]) + } else { + None + } + } +} diff --git a/src/lineengine/line.rs b/src/lineengine/line.rs new file mode 100644 index 0000000..6538d2c --- /dev/null +++ b/src/lineengine/line.rs @@ -0,0 +1,509 @@ +use std::io::{self, Write}; + +use minicrossterm::{ + cursor, + event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, TerminalState}, + terminal::{Clear, ClearType::*}, + QueueableCommand, +}; +use thiserror::Error; + +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + +use crate::lineengine::history::History; + +/// Error returned from [`readline()`][Readline::readline]. Such errors +/// generally require specific procedures to recover from. +#[derive(Debug, Error)] +pub enum ReadlineError { + /// An internal I/O error occurred + #[error(transparent)] + IO(#[from] io::Error), + + /// `readline()` was called after the [`SharedWriter`] was dropped and + /// everything written to the `SharedWriter` was already output + #[error("line writers closed")] + Closed, +} + +/// Events emitted by [`Readline::readline()`] +#[derive(Debug)] +pub enum ReadlineEvent { + /// The user entered a line of text + Line(String), + /// The user pressed Ctrl-D + Eof, + /// The user pressed Ctrl-C + Interrupted, +} + +#[derive(Default)] +pub struct LineState { + // Unicode Line + line: String, + // Index of grapheme in line + line_cursor_grapheme: usize, + // Column of grapheme in line + current_column: u16, + + cluster_buffer: String, // buffer for holding partial grapheme clusters as they come in + + prompt: String, + pub should_print_line_on_enter: bool, // After pressing enter, should we print the line just submitted? + pub should_print_line_on_control_c: bool, // After pressing control_c should we print the line just cancelled? + + last_line_length: usize, + last_line_completed: bool, + + term_size: (u16, u16), + + pub history: History, +} + +impl LineState { + pub fn new(prompt: String, term_size: (u16, u16)) -> Self { + let current_column = prompt.len() as u16; + Self { + prompt, + last_line_completed: true, + term_size, + current_column, + should_print_line_on_enter: true, + should_print_line_on_control_c: true, + + ..Default::default() + } + } + fn line_height(&self, pos: u16) -> u16 { + pos / self.term_size.0 // Gets the number of lines wrapped + } + /// Move from a position on the line to the start + fn move_to_beginning(&self, term: &mut impl Write, from: u16) -> io::Result<()> { + let move_up = self.line_height(from.saturating_sub(1)); + term.queue(cursor::MoveToColumn(0))?; + if move_up != 0 { + term.queue(cursor::MoveUp(move_up))?; + } + Ok(()) + } + /// Move from the start of the line to some position + fn move_from_beginning(&self, term: &mut impl Write, to: u16) -> io::Result<()> { + let line_height = self.line_height(to.saturating_sub(1)); + let line_remaining_len = to % self.term_size.0; // Get the remaining length + if line_height != 0 { + term.queue(cursor::MoveDown(line_height))?; + } + term.queue(cursor::MoveRight(line_remaining_len))?; + + Ok(()) + } + /// Move cursor by one unicode grapheme either left (negative) or right (positive) + fn move_cursor(&mut self, change: isize) -> io::Result<()> { + if change > 0 { + let count = self.line.graphemes(true).count(); + self.line_cursor_grapheme = + usize::min(self.line_cursor_grapheme + change as usize, count); + } else { + self.line_cursor_grapheme = + self.line_cursor_grapheme.saturating_sub((-change) as usize); + } + let (pos, str) = self.current_grapheme().unwrap_or((0, "")); + let pos = pos + str.len(); + self.current_column = + (self.prompt.len() + UnicodeWidthStr::width(&self.line[0..pos])) as u16; + + Ok(()) + } + fn current_grapheme(&self) -> Option<(usize, &str)> { + self.line + .grapheme_indices(true) + .take(self.line_cursor_grapheme) + .last() + } + fn next_grapheme(&self) -> Option<(usize, &str)> { + let total = self.line.grapheme_indices(true).count(); + if self.line_cursor_grapheme == total { + return None; + } + self.line + .grapheme_indices(true) + .take(self.line_cursor_grapheme + 1) + .last() + } + fn reset_cursor(&self, term: &mut impl Write) -> io::Result<()> { + self.move_to_beginning(term, self.current_column) + } + fn set_cursor(&self, term: &mut impl Write) -> io::Result<()> { + self.move_from_beginning(term, self.current_column) + } + /// Clear current line + pub fn clear(&self, term: &mut impl Write) -> io::Result<()> { + self.move_to_beginning(term, self.current_column)?; + term.queue(Clear(FromCursorDown))?; + Ok(()) + } + /// Render line + pub fn render(&self, term: &mut impl Write) -> io::Result<()> { + write!(term, "{}{}", self.prompt, self.line)?; + let line_len = self.prompt.len() + UnicodeWidthStr::width(&self.line[..]); + self.move_to_beginning(term, line_len as u16)?; + self.move_from_beginning(term, self.current_column)?; + Ok(()) + } + /// Clear line and render + pub fn clear_and_render(&self, term: &mut impl Write) -> io::Result<()> { + self.clear(term)?; + self.render(term)?; + Ok(()) + } + pub fn print_data(&mut self, data: &[u8], term: &mut impl Write) -> Result<(), ReadlineError> { + self.clear(term)?; + + // If last written data was not newline, restore the cursor + if !self.last_line_completed { + term.queue(cursor::MoveUp(1))? + .queue(cursor::MoveToColumn(0))? + .queue(cursor::MoveRight(self.last_line_length as u16))?; + } + + // Write data in a way that newlines also act as carriage returns + for line in data.split_inclusive(|b| *b == b'\n') { + term.write_all(line)?; + term.queue(cursor::MoveToColumn(0))?; + } + + self.last_line_completed = data.ends_with(b"\n"); // Set whether data ends with newline + + // If data does not end with newline, save the cursor and write newline for prompt + // Usually data does end in newline due to the buffering of SharedWriter, but sometimes it may not (i.e. if .flush() is called) + if !self.last_line_completed { + self.last_line_length += data.len(); + // Make sure that last_line_length wraps around when doing multiple writes + if self.last_line_length >= self.term_size.0 as usize { + self.last_line_length %= self.term_size.0 as usize; + writeln!(term)?; + } + writeln!(term)?; // Move to beginning of line and make new line + } else { + self.last_line_length = 0; + } + + term.queue(cursor::MoveToColumn(0))?; + + self.render(term)?; + Ok(()) + } + pub fn print(&mut self, string: &str, term: &mut impl Write) -> Result<(), ReadlineError> { + self.print_data(string.as_bytes(), term)?; + Ok(()) + } + pub fn update_prompt( + &mut self, + prompt: &str, + term: &mut impl Write, + ) -> Result<(), ReadlineError> { + self.clear(term)?; + self.prompt.clear(); + self.prompt.push_str(prompt); + // recalculates column + self.move_cursor(0)?; + self.render(term)?; + term.flush()?; + Ok(()) + } + pub fn handle_event( + &mut self, + event: Event, + term: &mut impl Write, + ) -> Result, ReadlineError> { + match event { + // Control Keys + Event::Key(KeyEvent { + code, + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + .. + }) => match code { + // End of transmission (CTRL-D) + KeyCode::Char('d') => { + writeln!(term)?; + self.clear(term)?; + return Ok(Some(ReadlineEvent::Eof)); + } + // End of text (CTRL-C) + KeyCode::Char('c') => { + if self.should_print_line_on_control_c { + self.print(&format!("{}{}", self.prompt, self.line), term)?; + } + + self.line.clear(); + self.move_cursor(-10000)?; + self.clear_and_render(term)?; + return Ok(Some(ReadlineEvent::Interrupted)); + } + // Clear all + KeyCode::Char('l') => { + term.queue(Clear(All))?.queue(cursor::MoveTo(0, 0))?; + self.clear_and_render(term)?; + } + // Clear to start + KeyCode::Char('u') => { + if let Some((pos, str)) = self.current_grapheme() { + let pos = pos + str.len(); + self.line.drain(0..pos); + self.move_cursor(-100000)?; + self.clear_and_render(term)?; + } + } + // Clear last word + KeyCode::Char('w') => { + let count = self.line.graphemes(true).count(); + let skip_count = count - self.line_cursor_grapheme; + let start = self + .line + .grapheme_indices(true) + .rev() + .skip(skip_count) + .skip_while(|(_, str)| *str == " ") + .find_map(|(pos, str)| if str == " " { Some(pos + 1) } else { None }) + .unwrap_or(0); + let end = self + .line + .grapheme_indices(true) + .nth(self.line_cursor_grapheme) + .map(|(end, _)| end); + let change = start as isize - self.line_cursor_grapheme as isize; + self.move_cursor(change)?; + if let Some(end) = end { + self.line.drain(start..end); + } else { + self.line.drain(start..); + } + self.clear_and_render(term)?; + } + // Move to beginning + #[cfg(feature = "emacs")] + KeyCode::Char('a') => { + self.reset_cursor(term)?; + self.move_cursor(-100000)?; + self.set_cursor(term)?; + } + // Move to end + #[cfg(feature = "emacs")] + KeyCode::Char('e') => { + self.reset_cursor(term)?; + self.move_cursor(100000)?; + self.set_cursor(term)?; + } + // Move cursor left to previous word + KeyCode::Left => { + self.reset_cursor(term)?; + let count = self.line.graphemes(true).count(); + let skip_count = count - self.line_cursor_grapheme; + if let Some((pos, _)) = self + .line + .grapheme_indices(true) + .rev() + .skip(skip_count) + .skip_while(|(_, str)| *str == " ") + .find(|(_, str)| *str == " ") + { + let change = pos as isize - self.line_cursor_grapheme as isize; + self.move_cursor(change + 1)?; + } else { + self.move_cursor(-100000)? + } + self.set_cursor(term)?; + } + // Move cursor right to next word + KeyCode::Right => { + self.reset_cursor(term)?; + if let Some((pos, _)) = self + .line + .grapheme_indices(true) + .skip(self.line_cursor_grapheme) + .skip_while(|(_, c)| *c == " ") + .find(|(_, c)| *c == " ") + { + let change = pos as isize - self.line_cursor_grapheme as isize; + self.move_cursor(change)?; + } else { + self.move_cursor(10000)?; + }; + self.set_cursor(term)?; + } + _ => {} + }, + // Other Modifiers (None, Shift, Control+Alt) + // All other modifiers must be considered because the match expression cannot match + // combined KeyModifiers. Control+Alt is used to reach certain special symbols on a lot + // of international keyboard layouts. + Event::Key(KeyEvent { + code, + modifiers: _, + kind: KeyEventKind::Press, + .. + }) => match code { + KeyCode::Enter => { + // Print line so you can see what commands you've typed + if self.should_print_line_on_enter { + self.print(&format!("{}{}\n", self.prompt, self.line), term)?; + } + + // Take line + let line = std::mem::take(&mut self.line); + + // Render new line from beginning + self.move_cursor(-100000)?; + self.clear_and_render(term)?; + + // Return line + return Ok(Some(ReadlineEvent::Line(line))); + } + // Delete character from line + KeyCode::Backspace => { + if let Some((pos, str)) = self.current_grapheme() { + self.clear(term)?; + + let len = pos + str.len(); + self.line.replace_range(pos..len, ""); + self.move_cursor(-1)?; + + self.render(term)?; + } + } + KeyCode::Delete => { + if let Some((pos, str)) = self.next_grapheme() { + self.clear(term)?; + + let len = pos + str.len(); + self.line.replace_range(pos..len, ""); + + self.render(term)?; + } + } + KeyCode::Left => { + self.reset_cursor(term)?; + self.move_cursor(-1)?; + self.set_cursor(term)?; + } + KeyCode::Right => { + self.reset_cursor(term)?; + self.move_cursor(1)?; + self.set_cursor(term)?; + } + KeyCode::Home => { + self.reset_cursor(term)?; + self.move_cursor(-100000)?; + self.set_cursor(term)?; + } + KeyCode::End => { + self.reset_cursor(term)?; + self.move_cursor(100000)?; + self.set_cursor(term)?; + } + KeyCode::Up => { + // search for next history item, replace line if found. + if let Some(line) = self.history.search_next(&self.line) { + self.line.clear(); + self.line += line; + self.clear(term)?; + self.move_cursor(100000)?; + self.render(term)?; + } + } + KeyCode::Down => { + // search for next history item, replace line if found. + if let Some(line) = self.history.search_previous(&self.line) { + self.line.clear(); + self.line += line; + self.clear(term)?; + self.move_cursor(100000)?; + self.render(term)?; + } + } + // Add character to line and output + KeyCode::Char(c) => { + self.clear(term)?; + let prev_len = self.cluster_buffer.graphemes(true).count(); + self.cluster_buffer.push(c); + let new_len = self.cluster_buffer.graphemes(true).count(); + + let (g_pos, g_str) = self.current_grapheme().unwrap_or((0, "")); + let pos = g_pos + g_str.len(); + + self.line.insert(pos, c); + + if prev_len != new_len { + self.move_cursor(1)?; + if prev_len > 0 { + if let Some((pos, str)) = + self.cluster_buffer.grapheme_indices(true).next() + { + let len = str.len(); + self.cluster_buffer.replace_range(pos..len, ""); + } + } + } + self.render(term)?; + } + _ => {} + }, + Event::Resize(x, y) => { + self.term_size = (x, y); + self.clear_and_render(term)?; + } + _ => {} + } + Ok(None) + } +} + +pub struct WritetermFunction { + f: Box, +} + +pub struct Readline { + term: TerminalState, + line_state: LineState, + write_term: WritetermFunction, +} + +impl Write for WritetermFunction { + fn write(&mut self, buf: &[u8]) -> io::Result { + (self.f)(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl Readline { + pub fn new(prompt: String, write_fn: Box, term_size: (u16, u16)) -> Self { + Self { + term: TerminalState::new(), + line_state: LineState::new(prompt, term_size), + write_term: WritetermFunction { f: write_fn }, + } + } + + pub fn handle_resize(&mut self, term_size: (u16, u16)) -> Result<(), ReadlineError> { + self.line_state.handle_event( + Event::Resize(term_size.0, term_size.1), + &mut self.write_term, + )?; + Ok(()) + } + + pub fn readline(&mut self, input: &[u8]) -> Result, ReadlineError> { + let evs = self.term.events_for_input(input)?; + let mut results: Vec = Vec::new(); + for ev in evs { + results.extend(self.line_state.handle_event(ev, &mut self.write_term)?); + } + Ok(results) + } +} diff --git a/src/lineengine/rustyline-async b/src/lineengine/rustyline-async new file mode 160000 index 0000000..d589463 --- /dev/null +++ b/src/lineengine/rustyline-async @@ -0,0 +1 @@ +Subproject commit d5894639257d1f0c881366b0cd7152f023fdcc76 diff --git a/src/lua_state.rs b/src/lua_state.rs new file mode 100644 index 0000000..dfcc1a5 --- /dev/null +++ b/src/lua_state.rs @@ -0,0 +1,55 @@ +use piccolo::{Callback, Closure, Context, Executor, IntoValue, Lua, StashedExecutor, Table}; + +use crate::{echo_to_term_frame, RegisteredTermFrameLens, TermFrame}; +use std::str; + +pub struct LuaState { + pub interp: Lua, + pub exec: StashedExecutor, +} + +impl LuaState { + pub fn setup(frames: RegisteredTermFrameLens) -> Result { + let mut interp = Lua::core(); + let exec: StashedExecutor = interp.enter(|ctx| { + let cmd_table = Table::new(&ctx); + cmd_table + .set( + ctx, + ctx.intern_static(b"echo_frame"), + echo_frame(ctx, frames), + ) + .map_err(|_| "Can't add command")?; + ctx.set_global(ctx.intern_static(b"commands").into_value(ctx), cmd_table) + .map(|_| ()) + .map_err(|_| "Can't set commands key".to_owned())?; + Ok::(ctx.stash(Executor::new(ctx))) + })?; + + Ok(LuaState { interp, exec }) + } + + pub fn execute(&mut self, command: &str) -> Result { + self.interp + .try_enter(|ctx| { + let closure = Closure::load(ctx, None, format!("return ({})", command).as_bytes()) + .or_else(|_| Closure::load(ctx, None, command.as_bytes()))?; + ctx.fetch(&self.exec).restart(ctx, closure.into(), ()); + Ok(()) + }) + .map_err(|err| format!("{}", err))?; + Ok("Blah".to_owned()) + } +} + +fn echo_frame(ctx: Context, frames: RegisteredTermFrameLens) -> Callback { + Callback::from_fn(&ctx, move |ctx, _ex, mut stack| { + let frame_no: u64 = stack.consume(ctx)?; + let message: piccolo::String = stack.consume(ctx)?; + let message_str = str::from_utf8(message.as_bytes()) + .map_err(|_| "Expected message to echo to be UTF-8.".into_value(ctx))?; + echo_to_term_frame(&frames, &TermFrame(frame_no), message_str) + .map_err(|m| m.into_value(ctx))?; + Ok(piccolo::CallbackReturn::Return) + }) +} diff --git a/src/main.rs b/src/main.rs index d40d8df..e2ae3b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,30 +1,49 @@ +use std::cell::RefCell; use std::rc::Rc; use yew::prelude::*; +pub mod command_handler; +pub mod lineengine; +pub mod lua_state; +pub mod parsing; pub mod term_view; +use crate::command_handler::*; +use crate::lua_state::LuaState; use crate::term_view::*; +#[derive(Properties)] +struct GlobalState { + frame_registry: RegisteredTermFrames, + lua_engine: LuaState, +} + +// Used only for yew. Always equal since we don't want it to +// actually look into GlobalState, changes there should never +// cause a re-render. +impl PartialEq for GlobalState { + fn eq(&self, _other: &Self) -> bool { + true + } +} + #[function_component(App)] fn app() -> Html { - let frames_handle: UseStateHandle> = - use_state(|| RegisteredTermFrames::new().into()); - let frames = RegisteredTermFrameLens { - get: (*frames_handle).clone(), - set: Callback::from(move |s| frames_handle.set(s)), - }; + let global = use_mut_ref(|| Global { + frame_registry: RegisteredTermFrames::new().into(), + lua_engine: LuaState::setup(frames.clone()).expect("Can create interpreter"), + }); + html! {
- - -
- - -
+ +
} } fn main() { + console_error_panic_hook::set_once(); + yew::Renderer::::new().render(); } diff --git a/src/parsing.rs b/src/parsing.rs new file mode 100644 index 0000000..986aa00 --- /dev/null +++ b/src/parsing.rs @@ -0,0 +1,231 @@ +use std::collections::VecDeque; + +use nom::{ + branch::alt, + character::complete::{anychar, char, none_of}, + combinator::{eof, map, recognize, value}, + multi::{many0_count, separated_list0}, + sequence::{preceded, separated_pair}, + IResult, +}; + +#[derive(PartialEq, Eq, Debug)] +pub struct ParseResult<'l> { + pub commands: Vec>, +} + +#[derive(PartialEq, Eq, Debug)] +pub struct ParsedCommand<'l> { + pub arguments: VecDeque<&'l str>, +} + +impl ParsedCommand<'_> { + pub fn split_out_command(&self) -> Option<(&str, Self)> { + let mut tmp_arguments = self.arguments.clone(); + loop { + match tmp_arguments.pop_front() { + None => return None, + Some(head) => { + let trimmed_cmd = head.trim(); + if !trimmed_cmd.is_empty() { + return Some(( + trimmed_cmd, + Self { + arguments: tmp_arguments, + }, + )); + } + } + } + } + } +} + +fn parse_string(input: &str) -> IResult<&str, ()> { + value( + (), + many0_count(alt(( + value((), preceded(char('\\'), anychar)), + value((), none_of("\"")), + ))), + )(input) +} + +fn parse_parenthetical(input: &str) -> IResult<&str, ()> { + value( + (), + many0_count(alt(( + value((), preceded(char('\\'), anychar)), + value( + (), + separated_pair( + char('{'), + parse_parenthetical, + alt((value((), eof), value((), char('}')))), + ), + ), + value( + (), + separated_pair( + char('"'), + parse_string, + alt((value((), eof), value((), char('"')))), + ), + ), + value((), none_of("}")), + ))), + )(input) +} + +fn parse_argument(input: &str) -> IResult<&str, ()> { + value( + (), + many0_count(alt(( + value((), preceded(char('\\'), anychar)), + value( + (), + separated_pair( + char('{'), + parse_parenthetical, + alt((value((), eof), value((), char('}')))), + ), + ), + value( + (), + separated_pair( + char('"'), + parse_string, + alt((value((), eof), value((), char('"')))), + ), + ), + value((), none_of(" ;")), + ))), + )(input) +} + +fn parse_command(input: &str) -> IResult<&str, ParsedCommand> { + map( + separated_list0(char(' '), recognize(parse_argument)), + |arguments| ParsedCommand { + arguments: arguments.into(), + }, + )(input) +} + +pub fn parse_commands(input: &str) -> ParseResult { + // Note that the core parser doesn't do things like skipping multiple whitespace, + separated_list0(preceded(char(';'), many0_count(char(' '))), parse_command)(input) + .map(|(_, commands)| ParseResult { commands }) + .unwrap_or_else(|_| ParseResult { commands: vec![] }) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_parse_commands() { + assert_eq!( + parse_commands(""), + ParseResult { + commands: vec![ParsedCommand { + arguments: vec![""] + }] + } + ); + assert_eq!( + parse_commands("north"), + ParseResult { + commands: vec![ParsedCommand { + arguments: vec!["north"] + }] + } + ); + assert_eq!( + parse_commands("north "), + ParseResult { + commands: vec![ParsedCommand { + // This is deliberate, ensures we can reconstruct the input. + arguments: vec!["north", ""] + }] + } + ); + assert_eq!( + parse_commands("#blah {x = 1 + 2; y = 3}; #home"), + ParseResult { + commands: vec![ + ParsedCommand { + arguments: vec!["#blah", "{x = 1 + 2; y = 3}"] + }, + ParsedCommand { + arguments: vec!["#home"] + } + ] + } + ); + assert_eq!( + parse_commands("#blah {x = 1 + 2"), + ParseResult { + commands: vec![ParsedCommand { + arguments: vec!["#blah", "{x = 1 + 2"] + },] + } + ); + assert_eq!( + parse_commands("#blah {x = 1} {y = 1}"), + ParseResult { + commands: vec![ParsedCommand { + arguments: vec!["#blah", "{x = 1}", "{y = 1}"] + }] + } + ); + assert_eq!( + parse_commands("#blah \"hello\" \"world\""), + ParseResult { + commands: vec![ParsedCommand { + arguments: vec!["#blah", "\"hello\"", "\"world\""] + }] + } + ); + assert_eq!( + parse_commands("#blah {x = \"}\"} {y = 1}"), + ParseResult { + commands: vec![ParsedCommand { + arguments: vec!["#blah", "{x = \"}\"}", "{y = 1}"] + }] + } + ); + assert_eq!( + parse_commands("#blah {x = \"}\"; a = \"{\"; y = {}; z = 1;} { q = 5 };"), + ParseResult { + commands: vec![ + ParsedCommand { + arguments: vec![ + "#blah", + "{x = \"}\"; a = \"{\"; y = {}; z = 1;}", + "{ q = 5 }" + ] + }, + ParsedCommand { + arguments: vec![""] + } + ] + } + ); + assert_eq!( + parse_commands("#blah {\\}\\}\\}} {y = 1}"), + ParseResult { + commands: vec![ParsedCommand { + arguments: vec!["#blah", "{\\}\\}\\}}", "{y = 1}"] + }] + } + ); + assert_eq!( + parse_commands("#blah \"This is a \\\"test\\\"\""), + ParseResult { + commands: vec![ParsedCommand { + arguments: vec!["#blah", "\"This is a \\\"test\\\"\""] + }] + } + ); + } +} diff --git a/src/term_view.rs b/src/term_view.rs index f5e0156..a648872 100644 --- a/src/term_view.rs +++ b/src/term_view.rs @@ -1,23 +1,57 @@ -use std::rc::Rc; +use std::{ + cell::RefCell, + rc::{Rc, Weak}, +}; use im::hashmap::*; use wasm_bindgen::prelude::*; use web_sys::{Element, Node}; use yew::prelude::*; +use crate::lineengine::line::{Readline, ReadlineEvent}; + #[wasm_bindgen] extern "C" { #[derive(PartialEq)] pub type Terminal; + // Sadly, can't do type parameters with wasm_bindgen. + pub type IEventString; + pub type IEventDims; + pub type IDisposable; + #[wasm_bindgen(constructor)] fn new() -> Terminal; #[wasm_bindgen(method)] fn open(this: &Terminal, element: &Element); #[wasm_bindgen(method)] - fn write(this: &Terminal, data: &str); + pub fn write(this: &Terminal, data: &str); // Todo: Can we do this with interfaces somehow? #[wasm_bindgen(method)] fn loadAddon(this: &Terminal, addon: &FitAddon); + + #[wasm_bindgen(method)] + fn onData(this: &Terminal, listener: &Closure) -> IDisposable; + #[wasm_bindgen(method)] + fn onResize(this: &Terminal, listener: &Closure) -> IDisposable; + + #[wasm_bindgen(method, getter)] + fn rows(this: &Terminal) -> u16; + #[wasm_bindgen(method, getter)] + fn cols(this: &Terminal) -> u16; + + pub type Dims; + #[wasm_bindgen(method, getter)] + fn rows(this: &Dims) -> u16; + #[wasm_bindgen(method, getter)] + fn cols(this: &Dims) -> u16; + + #[wasm_bindgen(method, setter)] + fn set_listener(this: &IEventString, handler: &Closure) -> IDisposable; + #[wasm_bindgen(method, setter)] + fn set_listener(this: &IEventDims, handler: &Closure) -> IDisposable; + #[wasm_bindgen(method)] + fn dispose(this: &IDisposable); + #[derive(PartialEq)] pub type FitAddon; } @@ -33,30 +67,40 @@ extern "C" { #[derive(Eq, Ord, Hash, PartialEq, PartialOrd, Clone)] pub struct TermFrame(pub u64); -#[derive(Properties, PartialEq)] +#[derive(Properties)] pub struct TermFrameData { pub id: TermFrame, pub term: Terminal, pub fit: FitAddon, pub node: Node, + pub readline: RefCell, + pub retained_closures: RefCell, Closure)>>, +} + +impl PartialEq for TermFrameData { + fn eq(&self, other: &Self) -> bool { + // Only the ID matters, the rest are just reference data. + self.id == other.id + } } pub type RegisteredTermFrames = HashMap>; #[derive(Properties, PartialEq, Clone)] pub struct RegisteredTermFrameLens { - pub get: Rc, + pub get: Callback<(), Rc>, pub set: Callback, ()>, } fn get_or_make_term_frame( frame: &TermFrame, frames: &RegisteredTermFrameLens, + handler: &Callback<(TermFrame, String), ()>, ) -> Rc { - if let Some(tfd) = frames.get.get(frame) { + if let Some(tfd) = frames.get.emit(()).get(frame) { return tfd.clone(); } - let mut new_frames: RegisteredTermFrames = (*frames.get).clone(); + let mut new_frames: RegisteredTermFrames = (*frames.get.emit(())).clone(); let term = Terminal::new(); let fit = FitAddon::new(); let element = web_sys::window() @@ -66,17 +110,69 @@ fn get_or_make_term_frame( element.set_class_name("hterminal"); term.open(&element); term.loadAddon(&fit); + fit.fit(); for i in 0..100 { term.write(&format!("{} Hello world\r\n", i)); } + let term_for_readline: Terminal = Terminal { obj: term.clone() }; + let initial_size = (term.cols(), term.rows()); let new_data: Rc = TermFrameData { id: frame.clone(), - term, + term: Terminal { obj: term.clone() }, fit, node: element.into(), + readline: Readline::new( + "".to_owned(), + Box::new(move |dat| { + term_for_readline + .write(std::str::from_utf8(dat).expect("readline tried to emit invalid UTF-8")) + }), + initial_size, + ) + .into(), + retained_closures: RefCell::new(None), } .into(); + + let data_for_resize: Weak = Rc::downgrade(&new_data); + let resize_closure = Closure::new(move |dims: Dims| match Weak::upgrade(&data_for_resize) { + None => {} + Some(r) => match r.readline.try_borrow_mut() { + Err(_) => {} + Ok(mut v) => v.handle_resize((dims.cols(), dims.rows())).unwrap_or(()), + }, + }); + term.onResize(&resize_closure); + + let cloned_handler = (*handler).clone(); + let emit_frame = frame.clone(); + let data_for_on_data: Weak = Rc::downgrade(&new_data); + + let data_closure = Closure::new(move |d: String| match Weak::upgrade(&data_for_on_data) { + None => {} + Some(r) => match r.readline.try_borrow_mut() { + Err(_) => {} + Ok(mut v) => { + for ev in v.readline(d.as_bytes()).expect("Readline failed") { + match ev { + ReadlineEvent::Line(l) => cloned_handler.emit((emit_frame.clone(), l)), + _ => {} + } + } + } + }, + }); + + term.onData(&data_closure); + new_data + .retained_closures + .replace(Some((data_closure, resize_closure))); + new_frames.insert(frame.clone(), new_data.clone()); + web_sys::console::log_2( + &"Setting frames to have length: ".into(), + &new_frames.iter().count().into(), + ); frames.set.emit(new_frames.into()); new_data } @@ -85,13 +181,32 @@ fn get_or_make_term_frame( pub struct TermViewProps { pub terminal: TermFrame, pub frames: RegisteredTermFrameLens, + pub handler: Callback<(TermFrame, String), ()>, } #[function_component(TermView)] pub fn term_view(props: &TermViewProps) -> Html { - let term = get_or_make_term_frame(&props.terminal, &props.frames); + let term = get_or_make_term_frame(&props.terminal, &props.frames, &props.handler); term.fit.fit(); html! { {Html::VRef(term.node.clone())} } } + +pub fn echo_to_term_frame( + frames: &RegisteredTermFrameLens, + frame_id: &TermFrame, + message: &str, +) -> Result<(), &'static str> { + let frame_val = frames.get.emit(()); + let frame: &TermFrameData = frame_val.get(frame_id).ok_or_else(|| { + web_sys::console::log_3( + &"Attempt to echo to frame that doesn't exist.".into(), + &frame_id.0.into(), + &frame_val.iter().count().into(), + ); + "Attempt to echo to frame that doesn't exist." + })?; + frame.term.write(message); + Ok(()) +}